2020-07-02 04:21:35 +03:00
|
|
|
# Moonraker - HTTP/Websocket API Server for Klipper
|
|
|
|
#
|
|
|
|
# Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com>
|
|
|
|
#
|
|
|
|
# This file may be distributed under the terms of the GNU GPLv3 license
|
|
|
|
import argparse
|
2020-07-27 21:56:23 +03:00
|
|
|
import sys
|
2020-07-02 04:21:35 +03:00
|
|
|
import importlib
|
|
|
|
import os
|
|
|
|
import time
|
|
|
|
import socket
|
|
|
|
import logging
|
|
|
|
import json
|
2020-08-06 03:44:21 +03:00
|
|
|
import confighelper
|
2020-08-19 16:14:30 +03:00
|
|
|
import utils
|
2020-08-08 00:27:01 +03:00
|
|
|
from tornado import gen, iostream
|
2020-07-02 04:21:35 +03:00
|
|
|
from tornado.ioloop import IOLoop, PeriodicCallback
|
|
|
|
from tornado.util import TimeoutError
|
|
|
|
from tornado.locks import Event
|
|
|
|
from app import MoonrakerApp
|
2020-08-19 16:14:30 +03:00
|
|
|
from utils import ServerError
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
INIT_MS = 1000
|
|
|
|
|
|
|
|
CORE_PLUGINS = [
|
2020-08-14 03:45:03 +03:00
|
|
|
'file_manager', 'klippy_apis', 'machine',
|
2020-07-02 04:21:35 +03:00
|
|
|
'temperature_store', 'shell_command']
|
|
|
|
|
|
|
|
class Sentinel:
|
|
|
|
pass
|
|
|
|
|
|
|
|
class Server:
|
|
|
|
error = ServerError
|
|
|
|
def __init__(self, args):
|
2020-08-06 03:44:21 +03:00
|
|
|
config = confighelper.get_configuration(self, args)
|
|
|
|
self.host = config.get('host', "0.0.0.0")
|
|
|
|
self.port = config.getint('port', 7125)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
# Event initialization
|
|
|
|
self.events = {}
|
|
|
|
|
|
|
|
# Klippy Connection Handling
|
2020-08-08 18:28:05 +03:00
|
|
|
self.klippy_address = config.get(
|
|
|
|
'klippy_uds_address', "/tmp/klippy_uds")
|
2020-08-15 22:22:17 +03:00
|
|
|
self.klippy_connection = KlippyConnection(
|
|
|
|
self.process_command, self.on_connection_closed)
|
2020-08-16 02:12:15 +03:00
|
|
|
self.init_list = []
|
2020-08-14 03:45:03 +03:00
|
|
|
self.klippy_state = "disconnected"
|
|
|
|
|
|
|
|
# XXX - currently moonraker maintains a superset of all
|
|
|
|
# subscriptions, the results of which are forwarded to all
|
|
|
|
# connected websockets. A better implementation would open a
|
|
|
|
# unique unix domain socket for each websocket client and
|
|
|
|
# allow Klipper to forward only those subscriptions back to
|
|
|
|
# correct client.
|
|
|
|
self.all_subscriptions = {}
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
# Server/IOLoop
|
|
|
|
self.server_running = False
|
2020-08-06 03:44:21 +03:00
|
|
|
self.moonraker_app = app = MoonrakerApp(config)
|
2020-07-02 04:21:35 +03:00
|
|
|
self.register_endpoint = app.register_local_handler
|
|
|
|
self.register_static_file_handler = app.register_static_file_handler
|
|
|
|
self.register_upload_handler = app.register_upload_handler
|
2020-08-08 00:27:01 +03:00
|
|
|
self.ioloop = IOLoop.current()
|
2020-08-06 03:44:21 +03:00
|
|
|
self.init_cb = PeriodicCallback(self._initialize, INIT_MS)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
# Setup remote methods accessable to Klippy. Note that all
|
|
|
|
# registered remote methods should be of the notification type,
|
|
|
|
# they do not return a response to Klippy after execution
|
|
|
|
self.pending_requests = {}
|
|
|
|
self.remote_methods = {}
|
|
|
|
self.register_remote_method(
|
|
|
|
'process_gcode_response', self._process_gcode_response)
|
|
|
|
self.register_remote_method(
|
|
|
|
'process_status_update', self._process_status_update)
|
|
|
|
|
2020-08-06 03:44:21 +03:00
|
|
|
# Plugin initialization
|
|
|
|
self.plugins = {}
|
2020-08-14 03:45:03 +03:00
|
|
|
self.klippy_apis = self.load_plugin(config, 'klippy_apis')
|
2020-08-06 03:44:21 +03:00
|
|
|
self._load_plugins(config)
|
|
|
|
|
2020-07-02 04:21:35 +03:00
|
|
|
def start(self):
|
|
|
|
logging.info(
|
2020-08-14 00:49:29 +03:00
|
|
|
f"Starting Moonraker on ({self.host}, {self.port})")
|
2020-07-02 04:21:35 +03:00
|
|
|
self.moonraker_app.listen(self.host, self.port)
|
|
|
|
self.server_running = True
|
2020-08-08 18:28:05 +03:00
|
|
|
self.ioloop.spawn_callback(self._connect_klippy)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
# ***** Plugin Management *****
|
2020-08-06 03:44:21 +03:00
|
|
|
def _load_plugins(self, config):
|
|
|
|
# load core plugins
|
|
|
|
for plugin in CORE_PLUGINS:
|
|
|
|
self.load_plugin(config, plugin)
|
|
|
|
|
|
|
|
# check for optional plugins
|
|
|
|
opt_sections = set(config.sections()) - \
|
|
|
|
set(['server', 'authorization', 'cmd_args'])
|
|
|
|
for section in opt_sections:
|
|
|
|
self.load_plugin(config[section], section, None)
|
|
|
|
|
|
|
|
def load_plugin(self, config, plugin_name, default=Sentinel):
|
2020-07-02 04:21:35 +03:00
|
|
|
if plugin_name in self.plugins:
|
|
|
|
return self.plugins[plugin_name]
|
|
|
|
# Make sure plugin exists
|
|
|
|
mod_path = os.path.join(
|
|
|
|
os.path.dirname(__file__), 'plugins', plugin_name + '.py')
|
|
|
|
if not os.path.exists(mod_path):
|
2020-08-11 19:59:47 +03:00
|
|
|
msg = f"Plugin ({plugin_name}) does not exist"
|
2020-08-03 03:48:34 +03:00
|
|
|
logging.info(msg)
|
|
|
|
if default == Sentinel:
|
|
|
|
raise ServerError(msg)
|
|
|
|
return default
|
2020-07-02 04:21:35 +03:00
|
|
|
module = importlib.import_module("plugins." + plugin_name)
|
|
|
|
try:
|
|
|
|
load_func = getattr(module, "load_plugin")
|
2020-08-06 03:44:21 +03:00
|
|
|
plugin = load_func(config)
|
2020-07-02 04:21:35 +03:00
|
|
|
except Exception:
|
2020-08-11 19:59:47 +03:00
|
|
|
msg = f"Unable to load plugin ({plugin_name})"
|
2020-08-03 03:48:34 +03:00
|
|
|
logging.info(msg)
|
2020-07-02 04:21:35 +03:00
|
|
|
if default == Sentinel:
|
|
|
|
raise ServerError(msg)
|
|
|
|
return default
|
|
|
|
self.plugins[plugin_name] = plugin
|
2020-08-11 19:59:47 +03:00
|
|
|
logging.info(f"Plugin ({plugin_name}) loaded")
|
2020-07-02 04:21:35 +03:00
|
|
|
return plugin
|
|
|
|
|
|
|
|
def lookup_plugin(self, plugin_name, default=Sentinel):
|
|
|
|
plugin = self.plugins.get(plugin_name, default)
|
|
|
|
if plugin == Sentinel:
|
2020-08-11 19:59:47 +03:00
|
|
|
raise ServerError(f"Plugin ({plugin_name}) not found")
|
2020-07-02 04:21:35 +03:00
|
|
|
return plugin
|
|
|
|
|
|
|
|
def register_event_handler(self, event, callback):
|
|
|
|
self.events.setdefault(event, []).append(callback)
|
|
|
|
|
|
|
|
def send_event(self, event, *args):
|
|
|
|
events = self.events.get(event, [])
|
|
|
|
for evt in events:
|
2020-08-08 00:27:01 +03:00
|
|
|
self.ioloop.spawn_callback(evt, *args)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
def register_remote_method(self, method_name, cb):
|
|
|
|
if method_name in self.remote_methods:
|
|
|
|
# XXX - may want to raise an exception here
|
2020-08-11 19:59:47 +03:00
|
|
|
logging.info(f"Remote method ({method_name}) already registered")
|
2020-07-02 04:21:35 +03:00
|
|
|
return
|
|
|
|
self.remote_methods[method_name] = cb
|
|
|
|
|
|
|
|
# ***** Klippy Connection *****
|
2020-08-08 18:28:05 +03:00
|
|
|
async def _connect_klippy(self):
|
2020-08-15 22:22:17 +03:00
|
|
|
ret = await self.klippy_connection.connect(self.klippy_address)
|
|
|
|
if not ret:
|
2020-08-08 18:28:05 +03:00
|
|
|
self.ioloop.call_later(1., self._connect_klippy)
|
|
|
|
return
|
2020-07-02 04:21:35 +03:00
|
|
|
# begin server iniialization
|
|
|
|
self.init_cb.start()
|
|
|
|
|
2020-08-15 22:22:17 +03:00
|
|
|
def process_command(self, cmd):
|
|
|
|
method = cmd.get('method', None)
|
|
|
|
if method is not None:
|
|
|
|
# This is a remote method called from klippy
|
|
|
|
cb = self.remote_methods.get(method, None)
|
|
|
|
if cb is not None:
|
|
|
|
params = cmd.get('params', {})
|
|
|
|
cb(**params)
|
|
|
|
else:
|
|
|
|
logging.info(f"Unknown method received: {method}")
|
|
|
|
return
|
|
|
|
# This is a response to a request, process
|
|
|
|
req_id = cmd.get('id', None)
|
|
|
|
request = self.pending_requests.pop(req_id, None)
|
|
|
|
if request is None:
|
|
|
|
logging.info(
|
|
|
|
f"No request matching request ID: {req_id}, "
|
|
|
|
f"response: {cmd}")
|
|
|
|
return
|
|
|
|
if 'result' in cmd:
|
|
|
|
result = cmd['result']
|
|
|
|
if not result:
|
|
|
|
result = "ok"
|
|
|
|
else:
|
|
|
|
err = cmd.get('error', "Malformed Klippy Response")
|
|
|
|
result = ServerError(err, 400)
|
|
|
|
request.notify(result)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
2020-08-15 22:22:17 +03:00
|
|
|
def on_connection_closed(self):
|
2020-08-16 02:12:15 +03:00
|
|
|
self.init_list = []
|
2020-08-14 03:45:03 +03:00
|
|
|
self.klippy_state = "disconnected"
|
2020-08-08 00:27:01 +03:00
|
|
|
self.init_cb.stop()
|
|
|
|
for request in self.pending_requests.values():
|
|
|
|
request.notify(ServerError("Klippy Disconnected", 503))
|
|
|
|
self.pending_requests = {}
|
|
|
|
logging.info("Klippy Connection Removed")
|
2020-08-16 03:09:17 +03:00
|
|
|
self.send_event("server:klippy_disconnect")
|
2020-08-08 18:28:05 +03:00
|
|
|
self.ioloop.call_later(1., self._connect_klippy)
|
2020-08-08 00:27:01 +03:00
|
|
|
|
2020-07-02 04:21:35 +03:00
|
|
|
async def _initialize(self):
|
2020-08-16 02:12:15 +03:00
|
|
|
await self._check_ready()
|
2020-07-02 04:21:35 +03:00
|
|
|
await self._request_endpoints()
|
2020-08-16 02:12:15 +03:00
|
|
|
# Subscribe to "webhooks"
|
|
|
|
# Register "webhooks" subscription
|
|
|
|
if "webhooks_sub" not in self.init_list:
|
|
|
|
try:
|
|
|
|
await self.klippy_apis.subscribe_objects({'webhooks': None})
|
|
|
|
except ServerError as e:
|
|
|
|
logging.info(f"{e}\nUnable to subscribe to webhooks object")
|
|
|
|
else:
|
|
|
|
logging.info("Webhooks Subscribed")
|
|
|
|
self.init_list.append("webhooks_sub")
|
|
|
|
# Subscribe to Gcode Output
|
|
|
|
if "gcode_output_sub" not in self.init_list:
|
|
|
|
try:
|
|
|
|
await self.klippy_apis.subscribe_gcode_output()
|
|
|
|
except ServerError as e:
|
|
|
|
logging.info(
|
|
|
|
f"{e}\nUnable to register gcode output subscription")
|
|
|
|
else:
|
|
|
|
logging.info("GCode Output Subscribed")
|
|
|
|
self.init_list.append("gcode_output_sub")
|
|
|
|
if "klippy_ready" in self.init_list:
|
2020-08-06 03:44:21 +03:00
|
|
|
# Moonraker is enabled in the Klippy module
|
|
|
|
# and Klippy is ready. We can stop the init
|
|
|
|
# procedure.
|
2020-07-02 04:21:35 +03:00
|
|
|
self.init_cb.stop()
|
|
|
|
|
|
|
|
async def _request_endpoints(self):
|
2020-08-14 03:45:03 +03:00
|
|
|
result = await self.klippy_apis.list_endpoints(default=None)
|
|
|
|
if result is None:
|
2020-08-12 15:43:37 +03:00
|
|
|
return
|
2020-08-14 03:45:03 +03:00
|
|
|
endpoints = result.get('endpoints', {})
|
2020-08-12 15:43:37 +03:00
|
|
|
for ep in endpoints:
|
|
|
|
self.moonraker_app.register_remote_handler(ep)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
2020-08-06 03:44:21 +03:00
|
|
|
async def _check_ready(self):
|
2020-08-26 14:34:29 +03:00
|
|
|
send_id = "identified" not in self.init_list
|
2020-08-12 15:43:37 +03:00
|
|
|
try:
|
2020-08-26 14:34:29 +03:00
|
|
|
result = await self.klippy_apis.get_klippy_info(send_id)
|
2020-08-12 15:43:37 +03:00
|
|
|
except ServerError as e:
|
2020-07-31 19:49:25 +03:00
|
|
|
logging.info(
|
2020-08-12 15:43:37 +03:00
|
|
|
f"{e}\nKlippy info request error. This indicates that\n"
|
2020-08-11 19:59:47 +03:00
|
|
|
f"Klippy may have experienced an error during startup.\n"
|
|
|
|
f"Please check klippy.log for more information")
|
2020-08-12 15:43:37 +03:00
|
|
|
return
|
2020-08-26 14:34:29 +03:00
|
|
|
if send_id:
|
|
|
|
self.init_list.append("identified")
|
2020-08-14 03:45:03 +03:00
|
|
|
# Update filemanager fixed paths
|
|
|
|
fixed_paths = {k: result[k] for k in
|
|
|
|
['klipper_path', 'python_path',
|
|
|
|
'log_file', 'config_file']}
|
|
|
|
file_manager = self.lookup_plugin('file_manager')
|
|
|
|
file_manager.update_fixed_paths(fixed_paths)
|
|
|
|
is_ready = result.get('state', "") == "ready"
|
2020-08-12 15:43:37 +03:00
|
|
|
if is_ready:
|
2020-08-16 02:12:15 +03:00
|
|
|
await self._verify_klippy_requirements()
|
|
|
|
logging.info("Klippy ready")
|
|
|
|
self.klippy_state = "ready"
|
|
|
|
self.init_list.append('klippy_ready')
|
2020-08-16 03:09:17 +03:00
|
|
|
self.send_event("server:klippy_ready")
|
2020-08-12 15:43:37 +03:00
|
|
|
else:
|
2020-08-14 03:45:03 +03:00
|
|
|
msg = result.get('state_message', "Klippy Not Ready")
|
2020-08-12 15:43:37 +03:00
|
|
|
logging.info("\n" + msg)
|
|
|
|
|
2020-08-16 02:12:15 +03:00
|
|
|
async def _verify_klippy_requirements(self):
|
|
|
|
result = await self.klippy_apis.get_object_list(default=None)
|
2020-08-14 03:45:03 +03:00
|
|
|
if result is None:
|
2020-08-16 02:12:15 +03:00
|
|
|
logging.info(
|
|
|
|
f"Unable to retreive Klipper Object List")
|
|
|
|
return
|
|
|
|
req_objs = set(["virtual_sdcard", "display_status", "pause_resume"])
|
|
|
|
missing_objs = req_objs - set(result)
|
|
|
|
if missing_objs:
|
|
|
|
err_str = ", ".join([f"[{o}]" for o in missing_objs])
|
|
|
|
logging.info(
|
|
|
|
f"\nWarning, unable to detect the following printer "
|
|
|
|
f"objects:\n{err_str}\nPlease add the the above sections "
|
|
|
|
f"to printer.cfg for full Moonraker functionality.")
|
|
|
|
if "virtual_sdcard" not in missing_objs:
|
|
|
|
# Update the gcode path
|
|
|
|
result = await self.klippy_apis.query_objects(
|
|
|
|
{'configfile': None}, default=None)
|
|
|
|
if result is None:
|
|
|
|
logging.info(f"Unable to set SD Card path")
|
2020-08-14 03:45:03 +03:00
|
|
|
else:
|
2020-08-16 02:12:15 +03:00
|
|
|
config = result.get('configfile', {}).get('config', {})
|
|
|
|
vsd_config = config.get('virtual_sdcard', {})
|
|
|
|
vsd_path = vsd_config.get('path', None)
|
|
|
|
if vsd_path is not None:
|
|
|
|
file_manager = self.lookup_plugin('file_manager')
|
|
|
|
file_manager.register_directory(
|
|
|
|
'gcodes', vsd_path, can_delete=True)
|
|
|
|
else:
|
|
|
|
logging.info(
|
|
|
|
"Configuration for [virtual_sdcard] not found,"
|
|
|
|
" unable to set SD Card path")
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
def _process_gcode_response(self, response):
|
|
|
|
self.send_event("server:gcode_response", response)
|
|
|
|
|
2020-08-14 03:45:03 +03:00
|
|
|
def _process_status_update(self, eventtime, status):
|
|
|
|
if 'webhooks' in status:
|
|
|
|
# XXX - process other states (startup, ready, error, etc)?
|
|
|
|
state = status['webhooks'].get('state', None)
|
|
|
|
if state is not None:
|
|
|
|
if state == "shutdown":
|
2020-08-16 02:12:15 +03:00
|
|
|
logging.info("Klippy has shutdown")
|
2020-08-16 03:09:17 +03:00
|
|
|
self.send_event("server:klippy_shutdown")
|
2020-08-14 03:45:03 +03:00
|
|
|
self.klippy_state = state
|
2020-07-02 04:21:35 +03:00
|
|
|
self.send_event("server:status_update", status)
|
|
|
|
|
2020-08-14 01:15:17 +03:00
|
|
|
async def make_request(self, rpc_method, params):
|
2020-08-14 03:45:03 +03:00
|
|
|
# XXX - This adds the "response_template" to a subscription
|
|
|
|
# request and tracks all subscriptions so that each
|
|
|
|
# client gets what its requesting. In the future we should
|
|
|
|
# track subscriptions per client and send clients only
|
|
|
|
# the data they are asking for.
|
|
|
|
if rpc_method == "objects/subscribe":
|
|
|
|
for obj, items in params.get('objects', {}).items():
|
|
|
|
if obj in self.all_subscriptions:
|
|
|
|
pi = self.all_subscriptions[obj]
|
|
|
|
if items is None or pi is None:
|
|
|
|
self.all_subscriptions[obj] = None
|
|
|
|
else:
|
|
|
|
uitems = list(set(pi) | set(items))
|
|
|
|
self.all_subscriptions[obj] = uitems
|
|
|
|
else:
|
|
|
|
self.all_subscriptions[obj] = items
|
|
|
|
params['objects'] = dict(self.all_subscriptions)
|
|
|
|
params['response_template'] = {'method': "process_status_update"}
|
|
|
|
|
2020-08-14 01:15:17 +03:00
|
|
|
base_request = BaseRequest(rpc_method, params)
|
2020-07-02 04:21:35 +03:00
|
|
|
self.pending_requests[base_request.id] = base_request
|
2020-08-08 00:27:01 +03:00
|
|
|
self.ioloop.spawn_callback(
|
2020-08-15 22:22:17 +03:00
|
|
|
self.klippy_connection.send_request, base_request)
|
2020-08-12 15:43:37 +03:00
|
|
|
result = await base_request.wait()
|
|
|
|
return result
|
2020-07-02 04:21:35 +03:00
|
|
|
|
2020-08-14 03:45:03 +03:00
|
|
|
async def _stop_server(self):
|
2020-07-02 04:21:35 +03:00
|
|
|
# XXX - Currently this function is not used.
|
|
|
|
# Should I expose functionality to shutdown
|
|
|
|
# or restart the server, or simply remove this?
|
|
|
|
logging.info(
|
|
|
|
"Shutting Down Webserver")
|
|
|
|
for plugin in self.plugins:
|
|
|
|
if hasattr(plugin, "close"):
|
|
|
|
await plugin.close()
|
2020-08-15 22:22:17 +03:00
|
|
|
self.klippy_connection.close()
|
2020-07-02 04:21:35 +03:00
|
|
|
if self.server_running:
|
|
|
|
self.server_running = False
|
|
|
|
await self.moonraker_app.close()
|
2020-08-08 00:27:01 +03:00
|
|
|
self.ioloop.stop()
|
2020-07-02 04:21:35 +03:00
|
|
|
|
2020-08-15 22:22:17 +03:00
|
|
|
class KlippyConnection:
|
|
|
|
def __init__(self, on_recd, on_close):
|
|
|
|
self.ioloop = IOLoop.current()
|
|
|
|
self.iostream = None
|
|
|
|
self.on_recd = on_recd
|
|
|
|
self.on_close = on_close
|
|
|
|
|
|
|
|
async def connect(self, address):
|
|
|
|
ksock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
|
|
|
|
kstream = iostream.IOStream(ksock)
|
|
|
|
try:
|
|
|
|
await kstream.connect(address)
|
|
|
|
except iostream.StreamClosedError:
|
|
|
|
return False
|
|
|
|
logging.info("Klippy Connection Established")
|
|
|
|
self.iostream = kstream
|
|
|
|
self.iostream.set_close_callback(self.on_close)
|
|
|
|
self.ioloop.spawn_callback(self._read_stream, self.iostream)
|
|
|
|
return True
|
|
|
|
|
|
|
|
async def _read_stream(self, stream):
|
|
|
|
while not stream.closed():
|
|
|
|
try:
|
|
|
|
data = await stream.read_until(b'\x03')
|
|
|
|
except iostream.StreamClosedError as e:
|
|
|
|
return
|
|
|
|
except Exception:
|
|
|
|
logging.exception("Klippy Stream Read Error")
|
|
|
|
continue
|
|
|
|
try:
|
|
|
|
decoded_cmd = json.loads(data[:-1])
|
|
|
|
self.on_recd(decoded_cmd)
|
|
|
|
except Exception:
|
|
|
|
logging.exception(
|
|
|
|
f"Error processing Klippy Host Response: {data.decode()}")
|
|
|
|
|
|
|
|
async def send_request(self, request):
|
|
|
|
if self.iostream is None:
|
|
|
|
request.notify(ServerError("Klippy Host not connected", 503))
|
|
|
|
return
|
|
|
|
data = json.dumps(request.to_dict()).encode() + b"\x03"
|
|
|
|
try:
|
|
|
|
await self.iostream.write(data)
|
|
|
|
except iostream.StreamClosedError:
|
|
|
|
request.notify(ServerError("Klippy Host not connected", 503))
|
|
|
|
|
|
|
|
def is_connected(self):
|
|
|
|
return self.iostream is not None and not self.iostream.closed()
|
|
|
|
|
|
|
|
def close(self):
|
|
|
|
if self.iostream is not None and \
|
|
|
|
not self.iostream.closed():
|
|
|
|
self.iostream.close()
|
|
|
|
|
2020-07-02 04:21:35 +03:00
|
|
|
# Basic WebRequest class, easily converted to dict for json encoding
|
|
|
|
class BaseRequest:
|
2020-08-14 01:15:17 +03:00
|
|
|
def __init__(self, rpc_method, params):
|
2020-07-02 04:21:35 +03:00
|
|
|
self.id = id(self)
|
2020-08-14 01:15:17 +03:00
|
|
|
self.rpc_method = rpc_method
|
|
|
|
self.params = params
|
2020-07-02 04:21:35 +03:00
|
|
|
self._event = Event()
|
|
|
|
self.response = None
|
|
|
|
|
|
|
|
async def wait(self):
|
2020-08-05 04:27:58 +03:00
|
|
|
# Log pending requests every 60 seconds
|
|
|
|
start_time = time.time()
|
|
|
|
while True:
|
|
|
|
timeout = time.time() + 60.
|
|
|
|
try:
|
|
|
|
await self._event.wait(timeout=timeout)
|
|
|
|
except TimeoutError:
|
|
|
|
pending_time = time.time() - start_time
|
2020-08-14 00:49:29 +03:00
|
|
|
logging.info(
|
2020-08-14 01:15:17 +03:00
|
|
|
f"Request '{self.rpc_method}' pending: "
|
2020-08-14 00:49:29 +03:00
|
|
|
f"{pending_time:.2f} seconds")
|
2020-08-05 04:27:58 +03:00
|
|
|
self._event.clear()
|
|
|
|
continue
|
|
|
|
break
|
2020-08-12 15:43:37 +03:00
|
|
|
if isinstance(self.response, ServerError):
|
|
|
|
raise self.response
|
2020-07-02 04:21:35 +03:00
|
|
|
return self.response
|
|
|
|
|
|
|
|
def notify(self, response):
|
|
|
|
self.response = response
|
|
|
|
self._event.set()
|
|
|
|
|
|
|
|
def to_dict(self):
|
2020-08-14 01:15:17 +03:00
|
|
|
return {'id': self.id, 'method': self.rpc_method,
|
|
|
|
'params': self.params}
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
def main():
|
|
|
|
# Parse start arguments
|
|
|
|
parser = argparse.ArgumentParser(
|
|
|
|
description="Moonraker - Klipper API Server")
|
|
|
|
parser.add_argument(
|
2020-08-06 03:44:21 +03:00
|
|
|
"-c", "--configfile", default="~/moonraker.conf",
|
|
|
|
metavar='<configfile>',
|
|
|
|
help="Location of moonraker configuration file")
|
2020-07-02 04:21:35 +03:00
|
|
|
parser.add_argument(
|
|
|
|
"-l", "--logfile", default="/tmp/moonraker.log", metavar='<logfile>',
|
|
|
|
help="log file name and location")
|
|
|
|
cmd_line_args = parser.parse_args()
|
|
|
|
|
|
|
|
# Setup Logging
|
|
|
|
log_file = os.path.normpath(os.path.expanduser(cmd_line_args.logfile))
|
|
|
|
cmd_line_args.logfile = log_file
|
2020-08-31 22:22:48 +03:00
|
|
|
ql = utils.setup_logging(log_file)
|
2020-07-02 04:21:35 +03:00
|
|
|
|
2020-07-27 21:56:23 +03:00
|
|
|
if sys.version_info < (3, 7):
|
2020-08-11 19:59:47 +03:00
|
|
|
msg = f"Moonraker requires Python 3.7 or above. " \
|
|
|
|
f"Detected Version: {sys.version}"
|
2020-07-27 21:56:23 +03:00
|
|
|
logging.info(msg)
|
|
|
|
print(msg)
|
2020-08-31 22:22:48 +03:00
|
|
|
ql.stop()
|
2020-07-27 21:56:23 +03:00
|
|
|
exit(1)
|
|
|
|
|
2020-07-02 04:21:35 +03:00
|
|
|
# Start IOLoop and Server
|
|
|
|
io_loop = IOLoop.current()
|
|
|
|
try:
|
|
|
|
server = Server(cmd_line_args)
|
|
|
|
except Exception:
|
|
|
|
logging.exception("Moonraker Error")
|
2020-08-31 22:22:48 +03:00
|
|
|
ql.stop()
|
2020-08-06 03:44:21 +03:00
|
|
|
exit(1)
|
2020-07-02 04:21:35 +03:00
|
|
|
try:
|
|
|
|
server.start()
|
|
|
|
io_loop.start()
|
|
|
|
except Exception:
|
|
|
|
logging.exception("Server Running Error")
|
|
|
|
io_loop.close(True)
|
|
|
|
logging.info("Server Shutdown")
|
2020-08-31 22:22:48 +03:00
|
|
|
ql.stop()
|
2020-07-02 04:21:35 +03:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == '__main__':
|
|
|
|
main()
|