| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | # Klippy WebHooks registration and server connection | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # Copyright (C) 2020 Eric Callahan <arksine.code@gmail.com> | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # This file may be distributed under the terms of the GNU GPLv3 license | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  | import logging, socket, os, sys, errno, json, collections | 
					
						
							| 
									
										
										
										
											2021-01-08 12:07:45 -05:00
										 |  |  | import gcode | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  | REQUEST_LOG_SIZE = 20 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | # Json decodes strings as unicode types in Python 2.x.  This doesn't | 
					
						
							|  |  |  | # play well with some parts of Klipper (particuarly displays), so we | 
					
						
							|  |  |  | # need to create an object hook. This solution borrowed from: | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | # https://stackoverflow.com/questions/956867/ | 
					
						
							|  |  |  | # | 
					
						
							|  |  |  | def byteify(data, ignore_dicts=False): | 
					
						
							|  |  |  |     if isinstance(data, unicode): | 
					
						
							|  |  |  |         return data.encode('utf-8') | 
					
						
							|  |  |  |     if isinstance(data, list): | 
					
						
							|  |  |  |         return [byteify(i, True) for i in data] | 
					
						
							|  |  |  |     if isinstance(data, dict) and not ignore_dicts: | 
					
						
							|  |  |  |         return {byteify(k, True): byteify(v, True) | 
					
						
							|  |  |  |                 for k, v in data.items()} | 
					
						
							|  |  |  |     return data | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-01-08 12:07:45 -05:00
										 |  |  | class WebRequestError(gcode.CommandError): | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |     def __init__(self, message,): | 
					
						
							|  |  |  |         Exception.__init__(self, message) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def to_dict(self): | 
					
						
							|  |  |  |         return { | 
					
						
							|  |  |  |             'error': 'WebRequestError', | 
					
						
							| 
									
										
										
										
											2021-01-18 04:37:41 +01:00
										 |  |  |             'message': str(self)} | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | class Sentinel: | 
					
						
							|  |  |  |     pass | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class WebRequest: | 
					
						
							|  |  |  |     error = WebRequestError | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |     def __init__(self, client_conn, request): | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |         self.client_conn = client_conn | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         base_request = json.loads(request, object_hook=byteify) | 
					
						
							|  |  |  |         if type(base_request) != dict: | 
					
						
							|  |  |  |             raise ValueError("Not a top-level dictionary") | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         self.id = base_request.get('id', None) | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         self.method = base_request.get('method') | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         self.params = base_request.get('params', {}) | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         if type(self.method) != str or type(self.params) != dict: | 
					
						
							|  |  |  |             raise ValueError("Invalid request type") | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         self.response = None | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         self.is_error = False | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |     def get_client_connection(self): | 
					
						
							|  |  |  |         return self.client_conn | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |     def get(self, item, default=Sentinel, types=None): | 
					
						
							|  |  |  |         value = self.params.get(item, default) | 
					
						
							|  |  |  |         if value is Sentinel: | 
					
						
							|  |  |  |             raise WebRequestError("Missing Argument [%s]" % (item,)) | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  |         if (types is not None and type(value) not in types | 
					
						
							|  |  |  |             and item in self.params): | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |             raise WebRequestError("Invalid Argument Type [%s]" % (item,)) | 
					
						
							|  |  |  |         return value | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_str(self, item, default=Sentinel): | 
					
						
							|  |  |  |         return self.get(item, default, types=(str,)) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |     def get_int(self, item, default=Sentinel): | 
					
						
							|  |  |  |         return self.get(item, default, types=(int,)) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |     def get_float(self, item, default=Sentinel): | 
					
						
							|  |  |  |         return float(self.get(item, default, types=(int, float))) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_dict(self, item, default=Sentinel): | 
					
						
							|  |  |  |         return self.get(item, default, types=(dict,)) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |     def get_method(self): | 
					
						
							|  |  |  |         return self.method | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def set_error(self, error): | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         self.is_error = True | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         self.response = error.to_dict() | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def send(self, data): | 
					
						
							|  |  |  |         if self.response is not None: | 
					
						
							|  |  |  |             raise WebRequestError("Multiple calls to send not allowed") | 
					
						
							|  |  |  |         self.response = data | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def finish(self): | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         if self.id is None: | 
					
						
							|  |  |  |             return None | 
					
						
							|  |  |  |         rtype = "result" | 
					
						
							|  |  |  |         if self.is_error: | 
					
						
							|  |  |  |             rtype = "error" | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         if self.response is None: | 
					
						
							|  |  |  |             # No error was set and the user never executed | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |             # send, default response is {} | 
					
						
							|  |  |  |             self.response = {} | 
					
						
							|  |  |  |         return {"id": self.id, rtype: self.response} | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  | class ServerSocket: | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |     def __init__(self, webhooks, printer): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							|  |  |  |         self.webhooks = webhooks | 
					
						
							|  |  |  |         self.reactor = printer.get_reactor() | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |         self.sock = self.fd_handle = None | 
					
						
							|  |  |  |         self.clients = {} | 
					
						
							| 
									
										
										
										
											2020-08-11 16:26:07 -04:00
										 |  |  |         start_args = printer.get_start_args() | 
					
						
							|  |  |  |         server_address = start_args.get('apiserver') | 
					
						
							|  |  |  |         is_fileinput = (start_args.get('debuginput') is not None) | 
					
						
							|  |  |  |         if not server_address or is_fileinput: | 
					
						
							|  |  |  |             # Do not enable server | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             return | 
					
						
							| 
									
										
										
										
											2020-08-11 16:26:07 -04:00
										 |  |  |         self._remove_socket_file(server_address) | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |         self.sock = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) | 
					
						
							|  |  |  |         self.sock.setblocking(0) | 
					
						
							| 
									
										
										
										
											2020-08-11 16:26:07 -04:00
										 |  |  |         self.sock.bind(server_address) | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |         self.sock.listen(1) | 
					
						
							|  |  |  |         self.fd_handle = self.reactor.register_fd( | 
					
						
							|  |  |  |             self.sock.fileno(), self._handle_accept) | 
					
						
							|  |  |  |         printer.register_event_handler( | 
					
						
							|  |  |  |             'klippy:disconnect', self._handle_disconnect) | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  |         printer.register_event_handler( | 
					
						
							|  |  |  |             "klippy:shutdown", self._handle_shutdown) | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _handle_accept(self, eventtime): | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |             sock, addr = self.sock.accept() | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         except socket.error: | 
					
						
							|  |  |  |             return | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |         sock.setblocking(0) | 
					
						
							|  |  |  |         client = ClientConnection(self, sock) | 
					
						
							|  |  |  |         self.clients[client.uid] = client | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _handle_disconnect(self): | 
					
						
							|  |  |  |         for client in list(self.clients.values()): | 
					
						
							|  |  |  |             client.close() | 
					
						
							|  |  |  |         if self.sock is not None: | 
					
						
							|  |  |  |             self.reactor.unregister_fd(self.fd_handle) | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 self.sock.close() | 
					
						
							|  |  |  |             except socket.error: | 
					
						
							|  |  |  |                 pass | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  |     def _handle_shutdown(self): | 
					
						
							|  |  |  |         for client in self.clients.values(): | 
					
						
							|  |  |  |             client.dump_request_log() | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |     def _remove_socket_file(self, file_path): | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             os.remove(file_path) | 
					
						
							|  |  |  |         except OSError: | 
					
						
							|  |  |  |             if os.path.exists(file_path): | 
					
						
							|  |  |  |                 logging.exception( | 
					
						
							|  |  |  |                     "webhooks: Unable to delete socket file '%s'" | 
					
						
							|  |  |  |                     % (file_path)) | 
					
						
							|  |  |  |                 raise | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def pop_client(self, client_id): | 
					
						
							|  |  |  |         self.clients.pop(client_id, None) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | class ClientConnection: | 
					
						
							|  |  |  |     def __init__(self, server, sock): | 
					
						
							|  |  |  |         self.printer = server.printer | 
					
						
							|  |  |  |         self.webhooks = server.webhooks | 
					
						
							|  |  |  |         self.reactor = server.reactor | 
					
						
							|  |  |  |         self.server = server | 
					
						
							|  |  |  |         self.uid = id(self) | 
					
						
							|  |  |  |         self.sock = sock | 
					
						
							| 
									
										
										
										
											2020-06-23 07:53:30 -04:00
										 |  |  |         self.fd_handle = self.reactor.register_fd( | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |             self.sock.fileno(), self.process_received) | 
					
						
							| 
									
										
										
										
											2020-08-08 15:04:55 -04:00
										 |  |  |         self.partial_data = self.send_buffer = "" | 
					
						
							|  |  |  |         self.is_sending_data = False | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  |         self.set_client_info("?", "New connection") | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  |         self.request_log = collections.deque([], REQUEST_LOG_SIZE) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def dump_request_log(self): | 
					
						
							|  |  |  |         out = [] | 
					
						
							|  |  |  |         out.append("Dumping %d requests for client %d" | 
					
						
							|  |  |  |                    % (len(self.request_log), self.uid,)) | 
					
						
							|  |  |  |         for eventtime, request in self.request_log: | 
					
						
							|  |  |  |             out.append("Received %f: %s" % (eventtime, request)) | 
					
						
							|  |  |  |         logging.info("\n".join(out)) | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def set_client_info(self, client_info, state_msg=None): | 
					
						
							|  |  |  |         if state_msg is None: | 
					
						
							|  |  |  |             state_msg = "Client info %s" % (repr(client_info),) | 
					
						
							|  |  |  |         logging.info("webhooks client %s: %s", self.uid, state_msg) | 
					
						
							|  |  |  |         log_id = "webhooks %s" % (self.uid,) | 
					
						
							|  |  |  |         if client_info is None: | 
					
						
							|  |  |  |             self.printer.set_rollover_info(log_id, None, log=False) | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         rollover_msg = "webhooks client %s: %s" % (self.uid, repr(client_info)) | 
					
						
							|  |  |  |         self.printer.set_rollover_info(log_id, rollover_msg, log=False) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |     def close(self): | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  |         if self.fd_handle is None: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.set_client_info(None, "Disconnected") | 
					
						
							|  |  |  |         self.reactor.unregister_fd(self.fd_handle) | 
					
						
							|  |  |  |         self.fd_handle = None | 
					
						
							|  |  |  |         try: | 
					
						
							|  |  |  |             self.sock.close() | 
					
						
							|  |  |  |         except socket.error: | 
					
						
							|  |  |  |             pass | 
					
						
							|  |  |  |         self.server.pop_client(self.uid) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |     def is_closed(self): | 
					
						
							|  |  |  |         return self.fd_handle is None | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |     def process_received(self, eventtime): | 
					
						
							|  |  |  |         try: | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |             data = self.sock.recv(4096) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         except socket.error as e: | 
					
						
							|  |  |  |             # If bad file descriptor allow connection to be | 
					
						
							|  |  |  |             # closed by the data check | 
					
						
							|  |  |  |             if e.errno == errno.EBADF: | 
					
						
							|  |  |  |                 data = '' | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 return | 
					
						
							|  |  |  |         if data == '': | 
					
						
							|  |  |  |             # Socket Closed | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |             self.close() | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             return | 
					
						
							|  |  |  |         requests = data.split('\x03') | 
					
						
							|  |  |  |         requests[0] = self.partial_data + requests[0] | 
					
						
							|  |  |  |         self.partial_data = requests.pop() | 
					
						
							|  |  |  |         for req in requests: | 
					
						
							| 
									
										
										
										
											2021-08-21 11:30:01 -04:00
										 |  |  |             self.request_log.append((eventtime, req)) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             try: | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |                 web_request = WebRequest(self, req) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             except Exception: | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |                 logging.exception("webhooks: Error decoding Server Request %s" | 
					
						
							|  |  |  |                                   % (req)) | 
					
						
							| 
									
										
										
										
											2020-07-06 05:50:16 -04:00
										 |  |  |                 continue | 
					
						
							|  |  |  |             self.reactor.register_callback( | 
					
						
							| 
									
										
										
										
											2020-07-13 15:27:26 -04:00
										 |  |  |                 lambda e, s=self, wr=web_request: s._process_request(wr)) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-07-06 05:50:16 -04:00
										 |  |  |     def _process_request(self, web_request): | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         try: | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |             func = self.webhooks.get_callback(web_request.get_method()) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             func(web_request) | 
					
						
							| 
									
										
										
										
											2020-09-04 11:49:43 -04:00
										 |  |  |         except self.printer.command_error as e: | 
					
						
							| 
									
										
										
										
											2021-01-18 04:37:41 +01:00
										 |  |  |             web_request.set_error(WebRequestError(str(e))) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         except Exception as e: | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |             msg = ("Internal Error on WebRequest: %s" | 
					
						
							|  |  |  |                    % (web_request.get_method())) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             logging.exception(msg) | 
					
						
							| 
									
										
										
										
											2021-01-18 04:37:41 +01:00
										 |  |  |             web_request.set_error(WebRequestError(str(e))) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |             self.printer.invoke_shutdown(msg) | 
					
						
							|  |  |  |         result = web_request.finish() | 
					
						
							| 
									
										
										
										
											2020-08-11 22:09:46 -04:00
										 |  |  |         if result is None: | 
					
						
							|  |  |  |             return | 
					
						
							|  |  |  |         self.send(result) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def send(self, data): | 
					
						
							| 
									
										
										
										
											2021-07-25 12:17:54 -04:00
										 |  |  |         self.send_buffer += json.dumps(data, separators=(',', ':')) + "\x03" | 
					
						
							| 
									
										
										
										
											2020-08-08 15:04:55 -04:00
										 |  |  |         if not self.is_sending_data: | 
					
						
							|  |  |  |             self.is_sending_data = True | 
					
						
							|  |  |  |             self.reactor.register_callback(self._do_send) | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _do_send(self, eventtime): | 
					
						
							|  |  |  |         retries = 10 | 
					
						
							|  |  |  |         while self.send_buffer: | 
					
						
							|  |  |  |             try: | 
					
						
							|  |  |  |                 sent = self.sock.send(self.send_buffer) | 
					
						
							|  |  |  |             except socket.error as e: | 
					
						
							|  |  |  |                 if e.errno == errno.EBADF or e.errno == errno.EPIPE \ | 
					
						
							|  |  |  |                         or not retries: | 
					
						
							|  |  |  |                     sent = 0 | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |                 else: | 
					
						
							| 
									
										
										
										
											2020-08-08 15:04:55 -04:00
										 |  |  |                     retries -= 1 | 
					
						
							|  |  |  |                     waketime = self.reactor.monotonic() + .001 | 
					
						
							|  |  |  |                     self.reactor.pause(waketime) | 
					
						
							|  |  |  |                     continue | 
					
						
							|  |  |  |             retries = 10 | 
					
						
							|  |  |  |             if sent > 0: | 
					
						
							|  |  |  |                 self.send_buffer = self.send_buffer[sent:] | 
					
						
							|  |  |  |             else: | 
					
						
							|  |  |  |                 logging.info( | 
					
						
							|  |  |  |                     "webhooks: Error sending server data,  closing socket") | 
					
						
							|  |  |  |                 self.close() | 
					
						
							|  |  |  |                 break | 
					
						
							|  |  |  |         self.is_sending_data = False | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | class WebHooks: | 
					
						
							|  |  |  |     def __init__(self, printer): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							|  |  |  |         self._endpoints = {"list_endpoints": self._handle_list_endpoints} | 
					
						
							| 
									
										
										
										
											2020-10-28 08:37:12 -04:00
										 |  |  |         self._remote_methods = {} | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         self.register_endpoint("info", self._handle_info_request) | 
					
						
							|  |  |  |         self.register_endpoint("emergency_stop", self._handle_estop_request) | 
					
						
							| 
									
										
										
										
											2020-10-28 08:37:12 -04:00
										 |  |  |         self.register_endpoint("register_remote_method", | 
					
						
							|  |  |  |                                self._handle_rpc_registration) | 
					
						
							| 
									
										
										
										
											2020-08-08 06:12:01 -04:00
										 |  |  |         self.sconn = ServerSocket(self, printer) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def register_endpoint(self, path, callback): | 
					
						
							|  |  |  |         if path in self._endpoints: | 
					
						
							|  |  |  |             raise WebRequestError("Path already registered to an endpoint") | 
					
						
							|  |  |  |         self._endpoints[path] = callback | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def _handle_list_endpoints(self, web_request): | 
					
						
							| 
									
										
										
										
											2020-12-01 11:07:36 -05:00
										 |  |  |         web_request.send({'endpoints': list(self._endpoints.keys())}) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _handle_info_request(self, web_request): | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  |         client_info = web_request.get_dict('client_info', None) | 
					
						
							|  |  |  |         if client_info is not None: | 
					
						
							|  |  |  |             web_request.get_client_connection().set_client_info(client_info) | 
					
						
							| 
									
										
										
										
											2020-08-11 16:40:07 -04:00
										 |  |  |         state_message, state = self.printer.get_state_message() | 
					
						
							| 
									
										
										
										
											2020-08-25 14:49:43 -04:00
										 |  |  |         src_path = os.path.dirname(__file__) | 
					
						
							|  |  |  |         klipper_path = os.path.normpath(os.path.join(src_path, "..")) | 
					
						
							| 
									
										
										
										
											2020-08-11 16:40:07 -04:00
										 |  |  |         response = {'state': state, 'state_message': state_message, | 
					
						
							|  |  |  |                     'hostname': socket.gethostname(), | 
					
						
							|  |  |  |                     'klipper_path': klipper_path, 'python_path': sys.executable} | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |         start_args = self.printer.get_start_args() | 
					
						
							| 
									
										
										
										
											2020-08-11 16:40:07 -04:00
										 |  |  |         for sa in ['log_file', 'config_file', 'software_version', 'cpu_info']: | 
					
						
							|  |  |  |             response[sa] = start_args.get(sa) | 
					
						
							|  |  |  |         web_request.send(response) | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  |     def _handle_estop_request(self, web_request): | 
					
						
							| 
									
										
										
										
											2020-08-04 16:03:20 -04:00
										 |  |  |         self.printer.invoke_shutdown("Shutdown due to webhooks request") | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-28 08:37:12 -04:00
										 |  |  |     def _handle_rpc_registration(self, web_request): | 
					
						
							|  |  |  |         template = web_request.get_dict('response_template') | 
					
						
							|  |  |  |         method = web_request.get_str('remote_method') | 
					
						
							|  |  |  |         new_conn = web_request.get_client_connection() | 
					
						
							|  |  |  |         logging.info("webhooks: registering remote method '%s' " | 
					
						
							|  |  |  |                      "for connection id: %d" % (method, id(new_conn))) | 
					
						
							|  |  |  |         self._remote_methods.setdefault(method, {})[new_conn] = template | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-06-19 06:15:36 -04:00
										 |  |  |     def get_connection(self): | 
					
						
							|  |  |  |         return self.sconn | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  |     def get_callback(self, path): | 
					
						
							|  |  |  |         cb = self._endpoints.get(path, None) | 
					
						
							|  |  |  |         if cb is None: | 
					
						
							|  |  |  |             msg = "webhooks: No registered callback for path '%s'" % (path) | 
					
						
							|  |  |  |             logging.info(msg) | 
					
						
							|  |  |  |             raise WebRequestError(msg) | 
					
						
							|  |  |  |         return cb | 
					
						
							| 
									
										
										
										
											2020-08-04 16:00:23 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 21:33:50 -04:00
										 |  |  |     def get_status(self, eventtime): | 
					
						
							|  |  |  |         state_message, state = self.printer.get_state_message() | 
					
						
							| 
									
										
										
										
											2020-08-11 21:35:56 -04:00
										 |  |  |         return {'state': state, 'state_message': state_message} | 
					
						
							| 
									
										
										
										
											2020-08-04 17:18:36 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-10-28 08:37:12 -04:00
										 |  |  |     def call_remote_method(self, method, **kwargs): | 
					
						
							|  |  |  |         if method not in self._remote_methods: | 
					
						
							|  |  |  |             raise self.printer.command_error( | 
					
						
							|  |  |  |                 "Remote method '%s' not registered" % (method)) | 
					
						
							|  |  |  |         conn_map = self._remote_methods[method] | 
					
						
							|  |  |  |         valid_conns = {} | 
					
						
							|  |  |  |         for conn, template in conn_map.items(): | 
					
						
							|  |  |  |             if not conn.is_closed(): | 
					
						
							|  |  |  |                 valid_conns[conn] = template | 
					
						
							|  |  |  |                 out = {'params': kwargs} | 
					
						
							|  |  |  |                 out.update(template) | 
					
						
							|  |  |  |                 conn.send(out) | 
					
						
							|  |  |  |         if not valid_conns: | 
					
						
							|  |  |  |             del self._remote_methods[method] | 
					
						
							|  |  |  |             raise self.printer.command_error( | 
					
						
							|  |  |  |                 "No active connections for method '%s'" % (method)) | 
					
						
							|  |  |  |         self._remote_methods[method] = valid_conns | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  | class GCodeHelper: | 
					
						
							|  |  |  |     def __init__(self, printer): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							|  |  |  |         self.gcode = printer.lookup_object("gcode") | 
					
						
							|  |  |  |         # Output subscription tracking | 
					
						
							|  |  |  |         self.is_output_registered = False | 
					
						
							|  |  |  |         self.clients = {} | 
					
						
							|  |  |  |         # Register webhooks | 
					
						
							|  |  |  |         wh = printer.lookup_object('webhooks') | 
					
						
							|  |  |  |         wh.register_endpoint("gcode/help", self._handle_help) | 
					
						
							|  |  |  |         wh.register_endpoint("gcode/script", self._handle_script) | 
					
						
							|  |  |  |         wh.register_endpoint("gcode/restart", self._handle_restart) | 
					
						
							|  |  |  |         wh.register_endpoint("gcode/firmware_restart", | 
					
						
							|  |  |  |                              self._handle_firmware_restart) | 
					
						
							|  |  |  |         wh.register_endpoint("gcode/subscribe_output", | 
					
						
							|  |  |  |                              self._handle_subscribe_output) | 
					
						
							|  |  |  |     def _handle_help(self, web_request): | 
					
						
							|  |  |  |         web_request.send(self.gcode.get_command_help()) | 
					
						
							|  |  |  |     def _handle_script(self, web_request): | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         self.gcode.run_script(web_request.get_str('script')) | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |     def _handle_restart(self, web_request): | 
					
						
							|  |  |  |         self.gcode.run_script('restart') | 
					
						
							|  |  |  |     def _handle_firmware_restart(self, web_request): | 
					
						
							|  |  |  |         self.gcode.run_script('firmware_restart') | 
					
						
							|  |  |  |     def _output_callback(self, msg): | 
					
						
							|  |  |  |         for cconn, template in list(self.clients.items()): | 
					
						
							|  |  |  |             if cconn.is_closed(): | 
					
						
							|  |  |  |                 del self.clients[cconn] | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             tmp = dict(template) | 
					
						
							|  |  |  |             tmp['params'] = {'response': msg} | 
					
						
							|  |  |  |             cconn.send(tmp) | 
					
						
							|  |  |  |     def _handle_subscribe_output(self, web_request): | 
					
						
							|  |  |  |         cconn = web_request.get_client_connection() | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         template = web_request.get_dict('response_template', {}) | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |         self.clients[cconn] = template | 
					
						
							|  |  |  |         if not self.is_output_registered: | 
					
						
							|  |  |  |             self.gcode.register_output_handler(self._output_callback) | 
					
						
							|  |  |  |             self.is_output_registered = True | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-04 18:01:15 -04:00
										 |  |  | SUBSCRIPTION_REFRESH_TIME = .25 | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  | class QueryStatusHelper: | 
					
						
							| 
									
										
										
										
											2020-08-11 16:43:39 -04:00
										 |  |  |     def __init__(self, printer): | 
					
						
							|  |  |  |         self.printer = printer | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |         self.clients = {} | 
					
						
							|  |  |  |         self.pending_queries = [] | 
					
						
							|  |  |  |         self.query_timer = None | 
					
						
							|  |  |  |         self.last_query = {} | 
					
						
							| 
									
										
										
										
											2020-08-04 18:01:15 -04:00
										 |  |  |         # Register webhooks | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |         webhooks = printer.lookup_object('webhooks') | 
					
						
							|  |  |  |         webhooks.register_endpoint("objects/list", self._handle_list) | 
					
						
							|  |  |  |         webhooks.register_endpoint("objects/query", self._handle_query) | 
					
						
							|  |  |  |         webhooks.register_endpoint("objects/subscribe", self._handle_subscribe) | 
					
						
							|  |  |  |     def _handle_list(self, web_request): | 
					
						
							|  |  |  |         objects = [n for n, o in self.printer.lookup_objects() | 
					
						
							|  |  |  |                    if hasattr(o, 'get_status')] | 
					
						
							|  |  |  |         web_request.send({'objects': objects}) | 
					
						
							|  |  |  |     def _do_query(self, eventtime): | 
					
						
							|  |  |  |         last_query = self.last_query | 
					
						
							|  |  |  |         query = self.last_query = {} | 
					
						
							|  |  |  |         msglist = self.pending_queries | 
					
						
							|  |  |  |         self.pending_queries = [] | 
					
						
							|  |  |  |         msglist.extend(self.clients.values()) | 
					
						
							|  |  |  |         # Generate get_status() info for each client | 
					
						
							|  |  |  |         for cconn, subscription, send_func, template in msglist: | 
					
						
							|  |  |  |             is_query = cconn is None | 
					
						
							|  |  |  |             if not is_query and cconn.is_closed(): | 
					
						
							|  |  |  |                 del self.clients[cconn] | 
					
						
							|  |  |  |                 continue | 
					
						
							|  |  |  |             # Query each requested printer object | 
					
						
							|  |  |  |             cquery = {} | 
					
						
							|  |  |  |             for obj_name, req_items in subscription.items(): | 
					
						
							|  |  |  |                 res = query.get(obj_name, None) | 
					
						
							|  |  |  |                 if res is None: | 
					
						
							|  |  |  |                     po = self.printer.lookup_object(obj_name, None) | 
					
						
							|  |  |  |                     if po is None or not hasattr(po, 'get_status'): | 
					
						
							|  |  |  |                         res = query[obj_name] = {} | 
					
						
							| 
									
										
										
										
											2020-08-04 18:01:15 -04:00
										 |  |  |                     else: | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |                         res = query[obj_name] = po.get_status(eventtime) | 
					
						
							|  |  |  |                 if req_items is None: | 
					
						
							|  |  |  |                     req_items = list(res.keys()) | 
					
						
							|  |  |  |                     if req_items: | 
					
						
							|  |  |  |                         subscription[obj_name] = req_items | 
					
						
							|  |  |  |                 lres = last_query.get(obj_name, {}) | 
					
						
							|  |  |  |                 cres = {} | 
					
						
							|  |  |  |                 for ri in req_items: | 
					
						
							|  |  |  |                     rd = res.get(ri, None) | 
					
						
							| 
									
										
										
										
											2020-08-16 15:39:30 -04:00
										 |  |  |                     if is_query or rd != lres.get(ri): | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |                         cres[ri] = rd | 
					
						
							|  |  |  |                 if cres or is_query: | 
					
						
							|  |  |  |                     cquery[obj_name] = cres | 
					
						
							|  |  |  |             # Send data | 
					
						
							|  |  |  |             if cquery or is_query: | 
					
						
							|  |  |  |                 tmp = dict(template) | 
					
						
							|  |  |  |                 tmp['params'] = {'eventtime': eventtime, 'status': cquery} | 
					
						
							|  |  |  |                 send_func(tmp) | 
					
						
							|  |  |  |         if not query: | 
					
						
							|  |  |  |             # Unregister timer if there are no longer any subscriptions | 
					
						
							|  |  |  |             reactor = self.printer.get_reactor() | 
					
						
							|  |  |  |             reactor.unregister_timer(self.query_timer) | 
					
						
							|  |  |  |             self.query_timer = None | 
					
						
							|  |  |  |             return reactor.NEVER | 
					
						
							|  |  |  |         return eventtime + SUBSCRIPTION_REFRESH_TIME | 
					
						
							|  |  |  |     def _handle_query(self, web_request, is_subscribe=False): | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         objects = web_request.get_dict('objects') | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |         # Validate subscription format | 
					
						
							|  |  |  |         for k, v in objects.items(): | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |             if type(k) != str or (v is not None and type(v) != list): | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |                 raise web_request.error("Invalid argument") | 
					
						
							|  |  |  |             if v is not None: | 
					
						
							|  |  |  |                 for ri in v: | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |                     if type(ri) != str: | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |                         raise web_request.error("Invalid argument") | 
					
						
							|  |  |  |         # Add to pending queries | 
					
						
							|  |  |  |         cconn = web_request.get_client_connection() | 
					
						
							| 
									
										
										
										
											2020-08-14 15:18:27 -04:00
										 |  |  |         template = web_request.get_dict('response_template', {}) | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |         if is_subscribe and cconn in self.clients: | 
					
						
							|  |  |  |             del self.clients[cconn] | 
					
						
							|  |  |  |         reactor = self.printer.get_reactor() | 
					
						
							|  |  |  |         complete = reactor.completion() | 
					
						
							|  |  |  |         self.pending_queries.append((None, objects, complete.complete, {})) | 
					
						
							|  |  |  |         # Start timer if needed | 
					
						
							|  |  |  |         if self.query_timer is None: | 
					
						
							|  |  |  |             qt = reactor.register_timer(self._do_query, reactor.NOW) | 
					
						
							|  |  |  |             self.query_timer = qt | 
					
						
							|  |  |  |         # Wait for data to be queried | 
					
						
							|  |  |  |         msg = complete.wait() | 
					
						
							|  |  |  |         web_request.send(msg['params']) | 
					
						
							|  |  |  |         if is_subscribe: | 
					
						
							|  |  |  |             self.clients[cconn] = (cconn, objects, cconn.send, template) | 
					
						
							|  |  |  |     def _handle_subscribe(self, web_request): | 
					
						
							|  |  |  |         self._handle_query(web_request, is_subscribe=True) | 
					
						
							| 
									
										
										
										
											2020-08-04 18:01:15 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2020-08-04 16:00:23 -04:00
										 |  |  | def add_early_printer_objects(printer): | 
					
						
							|  |  |  |     printer.add_object('webhooks', WebHooks(printer)) | 
					
						
							| 
									
										
										
										
											2020-08-11 21:21:41 -04:00
										 |  |  |     GCodeHelper(printer) | 
					
						
							| 
									
										
										
										
											2020-08-11 21:31:09 -04:00
										 |  |  |     QueryStatusHelper(printer) |