Multiple accounts¶
AccountPool hands out one logged-in client per account and refuses to lease
the same account twice. Prefer leased(): it releases the account and closes
the client even if your code raises.
from empire_core import AccountPool, PoolExhaustedError
from empire_core.accounts import AccountRegistry
registry = AccountRegistry()
registry.load(file_path="accounts.json")
pool = AccountPool(registry)
try:
with pool.leased(tag="scanning") as client:
result = client.map.scan_kingdom()
except PoolExhaustedError:
... # no candidate account was free
leased() takes a username to lease one account, or a tag to lease any
free account carrying it (tags match case-insensitively). PoolExhaustedError
means nothing was tried: no account matched, or every match was busy.
LoginError means every candidate was tried and each one failed to log in; the
cause is chained onto it.
The pool is safe to use from several threads.
Keeping clients logged in¶
By default every lease logs in and every release closes the client. A consumer
that leases per request pays a login each time, and frequent logins risk a
login cooldown. Make the pool with keep_alive=True to keep released clients
logged in instead:
pool = AccountPool(registry, keep_alive=True)
with pool.leased(username="scanner1") as client: # logs in
...
with pool.leased(username="scanner1") as client: # same client, no login
...
pool.release_all() # closes the kept clients too
A kept client pings the server every 60 seconds, as the game client does, so an
idle session stays open. If its session dropped while it waited, the next lease
closes it and logs in a fresh client; a cooldown on that login moves the lease
on to the next candidate as usual. release(client, logout=True) closes a
client even in a keep_alive pool.
A lease you hold for the whole process never reaches either path. Set
client.keep_session = True on it and the client logs a dropped session in
again by itself, honouring login cooldowns (see
Keeping the session):
client = pool.lease(username="watcher1") # None when the account is not available
if client is not None:
client.keep_session = True
client.on_session_restored(lambda: print("watcher1 is back"))
client.on_session_lost(lambda error: print("watcher1 is gone:", error))
A release closes the client or keeps it as before, and closing ends any
re-login. A keep_alive pool keeps a client released while it logs back in,
and leaves it to finish: until client.is_restoring_session turns False its
account is not available, so a lease moves on to the next candidate, and one
for that username alone returns None (pool.leased() raises
PoolExhaustedError). A restore that gives up leaves the client logged out,
and the next lease closes it and logs in a fresh one. To log in afresh without
waiting, close the client yourself: pool.get_client(username).close(). While a
held client waits out a cooldown, client.remaining_login_cooldown() says for
how long: the seconds the server last named, counting down. A lease that
fails chains its last failure to the LoginError it raises; a
LoginCooldownError there carries the seconds.
The next lease gets the very client object you held, so let go of your handle
when you release it. pool.leased() releases only its own lease, but a manual
pool.release(client) cannot tell your handle from the next leaseholder's:
call it once per lease. Releasing a client the account is not leased with
changes nothing and logs a warning.
Callbacks outlive the lease
The next leaseholder gets the client as you left it. Callbacks you
registered on it (client.on_disconnect, client.state.on_incoming_attack
and the like) keep firing during their lease. Remove them before the
with block ends, or release with logout=True. Streams of
client.listen() are the exception: release ends them.
What the client announced stays announced, also across close() and
login(): an attack or occupation announced to you is not announced again
to the next leaseholder. It reads client.state.get_announced_attacks() and
get_occupations(), or calls client.state.reannounce(movement_id),
with when_listed=True while the movement list has not come yet (see Movements).
Release drops the when_listed calls you left waiting, so they do not
fire during the next lease.
Where accounts come from¶
registry.load() reads the file you name plus every EMPIRE_ACCOUNT_*
environment variable:
A .env file is read only if you opt in with
registry.load(load_env_file=True): importing the library never changes your
environment.
Keep accounts.json private
It holds passwords in plain text. Keep it out of version control and
chmod 600 it; the library warns when it is group- or world-readable.
Scanning with several accounts¶
The server limits the request rate per account, so map scans go faster spread
over several accounts. Lease them and hand them to scan_kingdom_with, which
gives each its own thread and moves a dropped client's chunks to the others:
from contextlib import ExitStack
from empire_core import Kingdom
from empire_core.map import scan_kingdom_with
with ExitStack() as leases:
clients = [leases.enter_context(pool.leased(tag="scanner")) for _ in range(4)]
result = scan_kingdom_with(clients, Kingdom.GREEN)
See Map scanning for what it returns and how far threads in one process go.
Runnable example¶
"""Drive several accounts through an AccountPool.
The pool hands out one logged-in client per account and refuses to hand the same
account to two callers at once. Prefer the ``leased()`` context manager: it
releases the account (and closes the client) even if your code raises, which the
manual lease/release pair only does if you write the ``try/finally`` yourself.
Accounts come from the AccountRegistry the pool is given: ``registry.load()``
reads the file you name plus every ``EMPIRE_ACCOUNT_*`` environment variable. A
``.env`` file is NOT read unless you opt in with
``registry.load(load_env_file=True)`` — importing empire_core never mutates the
process environment as a side effect.
``accounts.json`` holds passwords in plain text: keep it out of version control
and ``chmod 600`` it.
python examples/account_pool.py [accounts.json]
"""
import logging
import sys
from empire_core import AccountPool, EmpireError, LoginError, PoolExhaustedError
from empire_core.accounts import AccountRegistry
logging.basicConfig(level=logging.INFO, format="%(levelname)s %(name)s: %(message)s")
logging.getLogger("websocket").setLevel(logging.WARNING)
def main() -> int:
accounts_file = sys.argv[1] if len(sys.argv) > 1 else "accounts.json"
registry = AccountRegistry()
registry.load(file_path=accounts_file)
# Each pool has its own registry: a second pool can use a completely
# different account set in the same process.
pool = AccountPool(registry)
print(f"{len(pool)} account(s) configured, {pool.available_count} available")
if not len(pool):
print(f"No accounts found in {accounts_file!r} or the environment.", file=sys.stderr)
return 2
try:
# Lease any free account.
with pool.leased() as client:
print(f"leased {client.username}: {len(client.castle.get_all())} castle(s)")
# Or restrict to accounts carrying a tag (matched case-insensitively).
try:
with pool.leased(tag="scanner") as client:
print(f"leased scanner {client.username}")
except PoolExhaustedError as e:
# Nothing was tried: no account matched, or all matches were busy or
# inactive. Distinct from LoginError, which means every candidate was
# tried and every one failed - back off rather than check credentials.
print(f"no scanner available: {e}")
except LoginError as e:
# Raised once every candidate account has failed. The underlying cause -
# bad credentials, a login cooldown, a bug - is chained onto it.
print(f"pool could not log anything in: {e} (cause: {e.__cause__!r})", file=sys.stderr)
return 1
except EmpireError as e:
print(f"{type(e).__name__}: {e}", file=sys.stderr)
return 1
finally:
# Belt and braces: closes anything still leased.
pool.release_all()
return 0
if __name__ == "__main__":
sys.exit(main())
API: AccountPool,
AccountRegistry