electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit e6020975a51f8f0d9e739d860454b2ade5b80e1f
parent 69da96feb4843ebb2ca9f84b128eb9af829b5a43
Author: Neil Booth <kyuupichan@gmail.com>
Date:   Sun, 31 Jan 2016 14:40:22 +0900

daemon: prevent races by opening with O_EXCL

Rework the way the daemon and server are created.  If we might
want to create a daemon, try to grab the lockfile with O_EXCL.
If we got it, create a server and daemon, otherwise try and
connect to the server.  If connecting to the server fails (including
after sleeping a little in case the server was recently created),
remove the lockfile and start over.

Other fix: shut down network gracefully on daemon exit.

Fixes #1638

Diffstat:
Mlib/daemon.py | 157+++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------
1 file changed, 110 insertions(+), 47 deletions(-)

diff --git a/lib/daemon.py b/lib/daemon.py @@ -19,6 +19,7 @@ import ast import os import sys +import time import jsonrpclib from jsonrpclib.SimpleJSONRPCServer import SimpleJSONRPCServer, SimpleJSONRPCRequestHandler @@ -49,7 +50,6 @@ def get_daemon(config): except: pass - class RequestHandler(SimpleJSONRPCRequestHandler): def do_OPTIONS(self): @@ -66,7 +66,7 @@ class RequestHandler(SimpleJSONRPCRequestHandler): class Daemon(DaemonThread): - def __init__(self, config): + def __init__(self, config, server): DaemonThread.__init__(self) self.config = config if config.get('offline'): @@ -78,18 +78,15 @@ class Daemon(DaemonThread): self.wallets = {} self.wallet = None self.cmd_runner = Commands(self.config, self.wallet, self.network) - host = config.get('rpchost', 'localhost') - port = config.get('rpcport', 0) - self.server = SimpleJSONRPCServer((host, port), requestHandler=RequestHandler, logRequests=False) - with open(lockfile(config), 'w') as f: - f.write(repr(self.server.socket.getsockname())) - self.server.timeout = 0.1 + self.server = server + # Setup server + server.timeout = 0.1 for cmdname in known_commands: - self.server.register_function(getattr(self.cmd_runner, cmdname), cmdname) - self.server.register_function(self.run_cmdline, 'run_cmdline') - self.server.register_function(self.ping, 'ping') - self.server.register_function(self.run_daemon, 'daemon') - self.server.register_function(self.run_gui, 'gui') + server.register_function(getattr(self.cmd_runner, cmdname), cmdname) + server.register_function(self.run_cmdline, 'run_cmdline') + server.register_function(self.ping, 'ping') + server.register_function(self.run_daemon, 'daemon') + server.register_function(self.run_gui, 'gui') def ping(self): return True @@ -178,11 +175,16 @@ class Daemon(DaemonThread): def run(self): while self.is_running(): self.server.handle_request() - os.unlink(lockfile(self.config)) - - def stop(self): for k, wallet in self.wallets.items(): wallet.stop_threads() + if self.network: + self.print_error("shutting down network") + self.network.stop() + self.network.join() + + def stop(self): + self.print_error("stopping, removing lockfile") + Daemon.remove_lockfile(Daemon.lockfile(self.config)) DaemonThread.stop(self) def init_gui(self, config, plugins): @@ -194,19 +196,78 @@ class Daemon(DaemonThread): self.gui.main() @staticmethod - def gui_command(config, config_options, plugins): - server = get_daemon(config) - if server is not None: - return server.gui(config_options) + def lockfile(config): + return os.path.join(config.path, 'daemon') - daemon = Daemon(config) + @staticmethod + def remove_lockfile(lockfile): + os.unlink(lockfile) + + @staticmethod + def get_fd_or_server(lockfile): + '''If create is True, tries to create the lockfile, using O_EXCL to + prevent races. If it succeeds it returns the FD. + + Otherwise try and connect to the server specified in the lockfile. + If this succeeds, the server is returned. Otherwise remove the + lockfile and try again.''' + while True: + try: + return os.open(lockfile, os.O_CREAT | os.O_EXCL | os.O_WRONLY) + except OSError: + pass + server = Daemon.get_server(lockfile) + if server is not None: + return server + # Couldn't connect; remove lockfile and try again. + Daemon.remove_lockfile(lockfile) + + @staticmethod + def get_server(lockfile): + while True: + create_time = None + try: + with open(lockfile) as f: + (host, port), create_time = ast.literal_eval(f.read()) + server = jsonrpclib.Server('http://%s:%d' % (host, port)) + # Test daemon is running + server.ping() + return server + except: + pass + if not create_time or create_time < time.time() - 1.0: + return None + # Sleep a bit and try again; it might have just been started + time.sleep(1.0) + + @staticmethod + def create_daemon(config, fd): + '''Create a daemon and server when they don't exist.''' + host = config.get('rpchost', 'localhost') + port = config.get('rpcport', 0) + server = SimpleJSONRPCServer((host, port), logRequests=False, + requestHandler=RequestHandler) + os.write(fd, repr((server.socket.getsockname(), time.time()))) + os.close(fd) + + daemon = Daemon(config, server) daemon.start() - daemon.init_gui(config, plugins) - sys.exit(0) + return daemon + + @staticmethod + def gui_command(config, config_options, plugins): + lockfile = Daemon.lockfile(config) + fd = Daemon.get_fd_or_server(lockfile) + if isinstance(fd, int): + daemon = Daemon.create_daemon(config, fd) + daemon.init_gui(config, plugins) + sys.exit(0) + server = fd + return server.gui(config_options) @staticmethod def cmdline_command(config, config_options): - server = get_daemon(config) + server = get_server(Daemon.lockfile(config)) if server is not None: return False, server.run_cmdline(config_options) @@ -214,29 +275,31 @@ class Daemon(DaemonThread): @staticmethod def daemon_command(config, config_options): - server = get_daemon(config) - if server is not None: - return server.daemon(config_options) - - subcommand = config.get('subcommand') - if subcommand in ['status', 'stop']: - print_msg("Daemon not running") - sys.exit(1) - elif subcommand == 'start': + lockfile = Daemon.lockfile(config) + fd = Daemon.get_fd_or_server(lockfile) + if isinstance(fd, int): + subcommand = config.get('subcommand') + if subcommand != 'start': + if subcommand in ['status', 'stop']: + print_msg("Daemon not running") + else: + print_msg("syntax: electrum daemon <start|status|stop>") + os.close(fd) + Daemon.remove_lockfile(lockfile) + sys.exit(1) pid = os.fork() - if pid == 0: - daemon = Daemon(config) - if config.get('websocket_server'): - from electrum import websockets - websockets.WebSocketServer(config, daemon.network).start() - if config.get('requests_dir'): - check_www_dir(config.get('requests_dir')) - daemon.start() - daemon.join() - sys.exit(0) - else: + if pid: print_stderr("starting daemon (PID %d)" % pid) sys.exit(0) - else: - print_msg("syntax: electrum daemon <start|status|stop>") - sys.exit(1) + daemon = Daemon.create_daemon(config, fd) + if config.get('websocket_server'): + from electrum import websockets + websockets.WebSocketServer(config, daemon.network).start() + if config.get('requests_dir'): + check_www_dir(config.get('requests_dir')) + daemon.join() + sys.exit(0) + + server = fd + if server is not None: + return server.daemon(config_options)