def _execute(self, options, args): """Start test server.""" out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): self.logger.error("Missing '{0}' folder?".format(out_dir)) else: self.serve_pidfile = os.path.abspath('nikolaserve.pid') os.chdir(out_dir) if '[' in options['address']: options['address'] = options['address'].strip('[').strip(']') ipv6 = True OurHTTP = IPv6Server elif options['ipv6']: ipv6 = True OurHTTP = IPv6Server else: ipv6 = False OurHTTP = HTTPServer httpd = OurHTTP((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() if ipv6: server_url = "http://[{0}]:{1}/".format(*sa) else: server_url = "http://{0}:{1}/".format(*sa) self.logger.info("Serving on {0} ...".format(server_url)) if options['browser']: # Some browsers fail to load 0.0.0.0 (Issue #2755) if sa[0] == '0.0.0.0': server_url = "http://127.0.0.1:{1}/".format(*sa) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open(server_url) if options['detach']: self.detached = True OurHTTPRequestHandler.quiet = True try: pid = os.fork() if pid == 0: signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() else: with open(self.serve_pidfile, 'w') as fh: fh.write('{0}\n'.format(pid)) self.logger.info("Detached with PID {0}. Run `kill {0}` or `kill $(cat nikolaserve.pid)` to stop the server.".format(pid)) except AttributeError: if os.name == 'nt': self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") else: raise else: self.detached = False try: self.dns_sd = dns_sd(options['port'], (options['ipv6'] or '::' in options['address'])) signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() except KeyboardInterrupt: self.shutdown() return 130
def _execute(self, options, args): """Start test server.""" self.logger = get_logger('serve', STDERR_HANDLER) out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): self.logger.error("Missing '{0}' folder?".format(out_dir)) else: os.chdir(out_dir) if '[' in options['address']: options['address'] = options['address'].strip('[').strip(']') ipv6 = True OurHTTP = IPv6Server elif options['ipv6']: ipv6 = True OurHTTP = IPv6Server else: ipv6 = False OurHTTP = HTTPServer httpd = OurHTTP((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() self.logger.info("Serving HTTP on {0} port {1}...".format(*sa)) if options['browser']: if ipv6: server_url = "http://[{0}]:{1}/".format(*sa) else: server_url = "http://{0}:{1}/".format(*sa) self.logger.info( "Opening {0} in the default web browser...".format( server_url)) webbrowser.open(server_url) if options['detach']: OurHTTPRequestHandler.quiet = True try: pid = os.fork() if pid == 0: httpd.serve_forever() else: self.logger.info( "Detached with PID {0}. Run `kill {0}` to stop the server." .format(pid)) except AttributeError as e: if os.name == 'nt': self.logger.warning( "Detaching is not available on Windows, server is running in the foreground." ) else: raise e else: try: self.dns_sd = dns_sd( options['port'], (options['ipv6'] or '::' in options['address'])) httpd.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() return 130
def _execute(self, options, args): """Start test server.""" self.logger = get_logger('serve') out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): self.logger.error("Missing '{0}' folder?".format(out_dir)) else: self.serve_pidfile = os.path.abspath('nikolaserve.pid') os.chdir(out_dir) if '[' in options['address']: options['address'] = options['address'].strip('[').strip(']') ipv6 = True OurHTTP = IPv6Server elif options['ipv6']: ipv6 = True OurHTTP = IPv6Server else: ipv6 = False OurHTTP = HTTPServer httpd = OurHTTP((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() self.logger.info("Serving HTTP on {0} port {1}...".format(*sa)) if options['browser']: if ipv6: server_url = "http://[{0}]:{1}/".format(*sa) elif sa[0] == '0.0.0.0': server_url = "http://127.0.0.1:{1}/".format(*sa) else: server_url = "http://{0}:{1}/".format(*sa) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open(server_url) if options['detach']: self.detached = True OurHTTPRequestHandler.quiet = True try: pid = os.fork() if pid == 0: signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() else: with open(self.serve_pidfile, 'w') as fh: fh.write('{0}\n'.format(pid)) self.logger.info("Detached with PID {0}. Run `kill {0}` or `kill $(cat nikolaserve.pid)` to stop the server.".format(pid)) except AttributeError: if os.name == 'nt': self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") else: raise else: self.detached = False try: self.dns_sd = dns_sd(options['port'], (options['ipv6'] or '::' in options['address'])) signal.signal(signal.SIGTERM, self.shutdown) httpd.serve_forever() except KeyboardInterrupt: self.shutdown() return 130
def _execute(self, options, args): """Start test server.""" self.logger = get_logger('serve', STDERR_HANDLER) out_dir = self.site.config['OUTPUT_FOLDER'] if not os.path.isdir(out_dir): self.logger.error("Missing '{0}' folder?".format(out_dir)) else: os.chdir(out_dir) if '[' in options['address']: options['address'] = options['address'].strip('[').strip(']') ipv6 = True OurHTTP = IPv6Server elif options['ipv6']: ipv6 = True OurHTTP = IPv6Server else: ipv6 = False OurHTTP = HTTPServer httpd = OurHTTP((options['address'], options['port']), OurHTTPRequestHandler) sa = httpd.socket.getsockname() self.logger.info("Serving HTTP on {0} port {1}...".format(*sa)) if options['browser']: if ipv6: server_url = "http://[{0}]:{1}/".format(*sa) else: server_url = "http://{0}:{1}/".format(*sa) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open(server_url) if options['detach']: OurHTTPRequestHandler.quiet = True try: pid = os.fork() if pid == 0: httpd.serve_forever() else: self.logger.info("Detached with PID {0}. Run `kill {0}` to stop the server.".format(pid)) except AttributeError as e: if os.name == 'nt': self.logger.warning("Detaching is not available on Windows, server is running in the foreground.") else: raise e else: try: self.dns_sd = dns_sd(options['port'], (options['ipv6'] or '::' in options['address'])) httpd.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() return 130
def _execute(self, options, args): """Start the watcher.""" self.sockets = [] self.rebuild_queue = asyncio.Queue() self.last_rebuild = datetime.datetime.now() if aiohttp is None and Observer is None: req_missing(['aiohttp', 'watchdog'], 'use the "auto" command') elif aiohttp is None: req_missing(['aiohttp'], 'use the "auto" command') elif Observer is None: req_missing(['watchdog'], 'use the "auto" command') if sys.argv[0].endswith('__main__.py'): self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build'] else: self.nikola_cmd = [sys.argv[0], 'build'] if self.site.configuration_filename != 'conf.py': self.nikola_cmd.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date (synchronously) self.logger.info("Rebuilding the site...") subprocess.call(self.nikola_cmd) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) for item in self.site.config['IMAGE_FOLDERS']: watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = '0.0.0.0' host = options['address'].strip('[').strip(']') or dhost # Set up asyncio server webapp = web.Application() webapp.router.add_get('/livereload.js', self.serve_livereload_js) webapp.router.add_get('/robots.txt', self.serve_robots_txt) webapp.router.add_route('*', '/livereload', self.websocket_handler) resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder) webapp.router.register_resource(resource) # Prepare asyncio event loop # Required for subprocessing to work loop = asyncio.get_event_loop() # Set debug setting loop.set_debug(self.site.debug) # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] if self.has_server: handler = webapp.make_handler() srv = loop.run_until_complete(loop.create_server(handler, host, port)) self.wd_observer = Observer() # Watch output folders and trigger reloads if self.has_server: self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): self.wd_observer.schedule(NikolaEventHandler(self.run_nikola_build, loop), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.run_nikola_build, loop), _conf_dn, recursive=False) self.wd_observer.start() win_sleeper = None # https://bugs.python.org/issue23057 (fixed in Python 3.8) if sys.platform == 'win32' and sys.version_info < (3, 8): win_sleeper = asyncio.ensure_future(windows_ctrlc_workaround()) if not self.has_server: self.logger.info("Watching for changes...") # Run the event loop forever (no server mode). try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) loop.run_forever() except KeyboardInterrupt: pass finally: if win_sleeper: win_sleeper.cancel() self.wd_observer.stop() self.wd_observer.join() loop.close() return host, port = srv.sockets[0].getsockname() if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Serving on {0} ...".format(server_url)) if browser: self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open('http://{0}:{1}'.format(host, port)) # Run the event loop forever and handle shutdowns. try: # Run rebuild queue queue_future = asyncio.ensure_future(self.run_rebuild_queue()) self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) loop.run_forever() except KeyboardInterrupt: pass finally: self.logger.info("Server is shutting down.") if win_sleeper: win_sleeper.cancel() if self.dns_sd: self.dns_sd.Reset() queue_future.cancel() srv.close() loop.run_until_complete(srv.wait_closed()) loop.run_until_complete(webapp.shutdown()) loop.run_until_complete(handler.shutdown(5.0)) loop.run_until_complete(webapp.cleanup()) self.wd_observer.stop() self.wd_observer.join() loop.close()
def _execute(self, options, args): """Start the watcher.""" self.logger = get_logger('auto', STDERR_HANDLER) LRSocket.logger = self.logger if WebSocket is object and watchdog is None: req_missing(['ws4py', 'watchdog'], 'use the "auto" command') elif WebSocket is object: req_missing(['ws4py'], 'use the "auto" command') elif watchdog is None: req_missing(['watchdog'], 'use the "auto" command') self.cmd_arguments = ['nikola', 'build'] if self.site.configuration_filename != 'conf.py': self.cmd_arguments.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date subprocess.call(self.cmd_arguments) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Do not duplicate entries -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/', 'plugins/', ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = None host = options['address'].strip('[').strip(']') or dhost # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] # Instantiate global observer observer = Observer() if self.has_server: # Watch output folders and trigger reloads observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) try: self.logger.info("Watching files for changes...") observer.start() except KeyboardInterrupt: pass parent = self class Mixed(WebSocketWSGIApplication): """A class that supports WS and HTTP protocols on the same port.""" def __call__(self, environ, start_response): if environ.get('HTTP_UPGRADE') is None: return parent.serve_static(environ, start_response) return super(Mixed, self).__call__(environ, start_response) if self.has_server: ws = make_server(host, port, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=Mixed(handler_cls=LRSocket)) ws.initialize_websockets_manager() self.logger.info("Serving HTTP on {0} port {1}...".format( host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info( "Opening {0} in the default web browser...".format( server_url)) # Yes, this is racy webbrowser.open('http://{0}:{1}'.format(host, port)) try: self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) ws.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15) else: # Workaround: can’t have nothing running (instant exit) # but also can’t join threads (no way to exit) # The joys of threading. try: while True: time.sleep(1) except KeyboardInterrupt: self.logger.info("Shutting down.") # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15)
def _execute(self, options, args): """Start the watcher.""" self.logger = get_logger('auto', STDERR_HANDLER) LRSocket.logger = self.logger if WebSocket is object and watchdog is None: req_missing(['ws4py', 'watchdog'], 'use the "auto" command') elif WebSocket is object: req_missing(['ws4py'], 'use the "auto" command') elif watchdog is None: req_missing(['watchdog'], 'use the "auto" command') self.cmd_arguments = ['nikola', 'build'] if self.site.configuration_filename != 'conf.py': self.cmd_arguments.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date subprocess.call(self.cmd_arguments) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Do not duplicate entries -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = None host = options['address'].strip('[').strip(']') or dhost # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] # Instantiate global observer observer = Observer() if self.has_server: # Watch output folders and trigger reloads observer.schedule(OurWatchHandler(self.do_refresh), out_folder, recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): observer.schedule(OurWatchHandler(self.do_rebuild), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) observer.schedule(ConfigWatchHandler(_conf_fn, self.do_rebuild), _conf_dn, recursive=False) try: self.logger.info("Watching files for changes...") observer.start() except KeyboardInterrupt: pass parent = self class Mixed(WebSocketWSGIApplication): """A class that supports WS and HTTP protocols on the same port.""" def __call__(self, environ, start_response): if environ.get('HTTP_UPGRADE') is None: return parent.serve_static(environ, start_response) return super(Mixed, self).__call__(environ, start_response) if self.has_server: ws = make_server( host, port, server_class=WSGIServer, handler_class=WebSocketWSGIRequestHandler, app=Mixed(handler_cls=LRSocket) ) ws.initialize_websockets_manager() self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Opening {0} in the default web browser...".format(server_url)) # Yes, this is racy webbrowser.open('http://{0}:{1}'.format(host, port)) try: self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) ws.serve_forever() except KeyboardInterrupt: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15) else: # Workaround: can’t have nothing running (instant exit) # but also can’t join threads (no way to exit) # The joys of threading. try: while True: time.sleep(1) except KeyboardInterrupt: self.logger.info("Shutting down.") # This is a hack, but something is locking up in a futex # and exit() doesn't work. os.kill(os.getpid(), 15)
def _execute(self, options, args): """Start the watcher.""" self.sockets = [] self.rebuild_queue = asyncio.Queue() self.last_rebuild = datetime.datetime.now() if aiohttp is None and Observer is None: req_missing(['aiohttp', 'watchdog'], 'use the "auto" command') elif aiohttp is None: req_missing(['aiohttp'], 'use the "auto" command') elif Observer is None: req_missing(['watchdog'], 'use the "auto" command') if sys.argv[0].endswith('__main__.py'): self.nikola_cmd = [sys.executable, '-m', 'nikola', 'build'] else: self.nikola_cmd = [sys.argv[0], 'build'] if self.site.configuration_filename != 'conf.py': self.nikola_cmd.append('--conf=' + self.site.configuration_filename) # Run an initial build so we are up-to-date (synchronously) self.logger.info("Rebuilding the site...") subprocess.call(self.nikola_cmd) port = options and options.get('port') self.snippet = '''<script>document.write('<script src="http://' + (location.host || 'localhost').split(':')[0] + ':{0}/livereload.js?snipver=1"></' + 'script>')</script> </head>'''.format(port) # Deduplicate entries by using a set -- otherwise, multiple rebuilds are triggered watched = set([ 'templates/' ] + [get_theme_path(name) for name in self.site.THEMES]) for item in self.site.config['post_pages']: watched.add(os.path.dirname(item[0])) for item in self.site.config['FILES_FOLDERS']: watched.add(item) for item in self.site.config['GALLERY_FOLDERS']: watched.add(item) for item in self.site.config['LISTINGS_FOLDERS']: watched.add(item) for item in self.site.config['IMAGE_FOLDERS']: watched.add(item) for item in self.site._plugin_places: watched.add(item) # Nikola itself (useful for developers) watched.add(pkg_resources.resource_filename('nikola', '')) out_folder = self.site.config['OUTPUT_FOLDER'] if options and options.get('browser'): browser = True else: browser = False if options['ipv6']: dhost = '::' else: dhost = '0.0.0.0' host = options['address'].strip('[').strip(']') or dhost # Set up asyncio server webapp = web.Application() webapp.router.add_get('/livereload.js', self.serve_livereload_js) webapp.router.add_get('/robots.txt', self.serve_robots_txt) webapp.router.add_route('*', '/livereload', self.websocket_handler) resource = IndexHtmlStaticResource(True, self.snippet, '', out_folder) webapp.router.register_resource(resource) # Prepare asyncio event loop # Required for subprocessing to work loop = asyncio.get_event_loop() # Set debug setting loop.set_debug(self.site.debug) # Server can be disabled (Issue #1883) self.has_server = not options['no-server'] if self.has_server: handler = webapp.make_handler() srv = loop.run_until_complete(loop.create_server(handler, host, port)) self.wd_observer = Observer() # Watch output folders and trigger reloads if self.has_server: self.wd_observer.schedule(NikolaEventHandler(self.reload_page, loop), 'output/', recursive=True) # Watch input folders and trigger rebuilds for p in watched: if os.path.exists(p): self.wd_observer.schedule(NikolaEventHandler(self.run_nikola_build, loop), p, recursive=True) # Watch config file (a bit of a hack, but we need a directory) _conf_fn = os.path.abspath(self.site.configuration_filename or 'conf.py') _conf_dn = os.path.dirname(_conf_fn) self.wd_observer.schedule(ConfigEventHandler(_conf_fn, self.run_nikola_build, loop), _conf_dn, recursive=False) self.wd_observer.start() if not self.has_server: self.logger.info("Watching for changes...") # Run the event loop forever (no server mode). try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) loop.run_forever() except KeyboardInterrupt: pass finally: self.wd_observer.stop() self.wd_observer.join() loop.close() return host, port = srv.sockets[0].getsockname() self.logger.info("Serving HTTP on {0} port {1}...".format(host, port)) if browser: if options['ipv6'] or '::' in host: server_url = "http://[{0}]:{1}/".format(host, port) else: server_url = "http://{0}:{1}/".format(host, port) self.logger.info("Opening {0} in the default web browser...".format(server_url)) webbrowser.open('http://{0}:{1}'.format(host, port)) # Run the event loop forever and handle shutdowns. try: # Run rebuild queue loop.run_until_complete(self.run_rebuild_queue()) self.dns_sd = dns_sd(port, (options['ipv6'] or '::' in host)) loop.run_forever() except KeyboardInterrupt: pass finally: self.logger.info("Server is shutting down.") if self.dns_sd: self.dns_sd.Reset() srv.close() self.rebuild_queue.put((None, None)) loop.run_until_complete(srv.wait_closed()) loop.run_until_complete(webapp.shutdown()) loop.run_until_complete(handler.shutdown(5.0)) loop.run_until_complete(webapp.cleanup()) self.wd_observer.stop() self.wd_observer.join() loop.close()