diff --git a/moonraker/components/application.py b/moonraker/components/application.py index 6ee776f..dc69e01 100644 --- a/moonraker/components/application.py +++ b/moonraker/components/application.py @@ -65,10 +65,11 @@ if TYPE_CHECKING: from io import BufferedReader from .authorization import Authorization from .template import TemplateFactory, JinjaTemplate - MessageDelgate = Optional[tornado.httputil.HTTPMessageDelegate] + MessageDelgate = Optional[HTTPMessageDelegate] AuthComp = Optional[Authorization] APICallback = Callable[[WebRequest], Coroutine] +# mypy: disable-error-code="attr-defined,name-defined" # 50 MiB Max Standard Body Size MAX_BODY_SIZE = 50 * 1024 * 1024 @@ -1010,6 +1011,7 @@ class FileUploadHandler(AuthorizedRequestHandler): for name, value in form_args.items(): debug_msg += f"\n{name}: {value}" debug_msg += f"\nChecksum: {calc_chksum}" + form_args["current_user"] = self.current_user logging.debug(debug_msg) logging.info(f"Processing Uploaded File: {self._file.multipart_filename}") try: diff --git a/moonraker/components/file_manager/file_manager.py b/moonraker/components/file_manager/file_manager.py index fada4e1..de33602 100644 --- a/moonraker/components/file_manager/file_manager.py +++ b/moonraker/components/file_manager/file_manager.py @@ -880,7 +880,8 @@ class FileManager: 'start_print': start_print, 'unzip_ufp': unzip_ufp, 'ext': f_ext, - "is_link": os.path.islink(dest_path) + "is_link": os.path.islink(dest_path), + "user": upload_args.get("current_user") } async def _finish_gcode_upload( @@ -901,10 +902,11 @@ class FileManager: started: bool = False queued: bool = False if upload_info['start_print']: + user: Optional[Dict[str, Any]] = upload_info.get("user") if can_start: kapis: APIComp = self.server.lookup_component('klippy_apis') try: - await kapis.start_print(upload_info['filename']) + await kapis.start_print(upload_info['filename'], user=user) except self.server.error: # Attempt to start print failed pass @@ -913,7 +915,7 @@ class FileManager: if self.queue_gcodes and not started: job_queue: JobQueue = self.server.lookup_component('job_queue') await job_queue.queue_job( - upload_info['filename'], check_exists=False) + upload_info['filename'], check_exists=False, user=user) queued = True self.fs_observer.on_item_create("gcodes", upload_info["dest_path"]) result = dict(self._sched_changed_event( diff --git a/moonraker/components/history.py b/moonraker/components/history.py index cd2319a..c0e8680 100644 --- a/moonraker/components/history.py +++ b/moonraker/components/history.py @@ -65,6 +65,8 @@ class History: "server:klippy_shutdown", self._handle_shutdown) self.server.register_event_handler( "job_state:state_changed", self._on_job_state_changed) + self.server.register_event_handler( + "klippy_apis:job_start_complete", self._on_job_requested) self.server.register_notification("history:history_changed") self.server.register_endpoint( @@ -88,6 +90,7 @@ class History: self.current_job: Optional[PrinterJob] = None self.current_job_id: Optional[str] = None + self.job_user: str = "No User" self.job_paused: bool = False self.next_job_id: int = 0 self.cached_job_ids = self.history_ns.keys().result() @@ -249,6 +252,12 @@ class History: # `CLEAR_PAUSE/SDCARD_RESET_FILE` workflow self.finish_job("cancelled", prev_stats) + def _on_job_requested(self, user: Optional[Dict[str, Any]]) -> None: + username = (user or {}).get("username", "No User") + self.job_user = username + if self.current_job is not None: + self.current_job.user = username + def _handle_shutdown(self) -> None: jstate: JobState = self.server.lookup_component("job_state") last_ps = jstate.get_last_stats() @@ -265,6 +274,7 @@ class History: job_id = f"{self.next_job_id:06X}" self.current_job = job self.current_job_id = job_id + self.current_job.user = self.job_user self.grab_job_metadata() for field in self.auxiliary_fields: field.tracker.reset() @@ -296,6 +306,7 @@ class History: # Print stats have been reset, do not update this job with them pstats = {} + self.current_job.user = self.job_user self.current_job.finish(status, pstats) # Regrab metadata incase metadata wasn't parsed yet due to file upload self.grab_job_metadata() @@ -310,6 +321,7 @@ class History: self.send_history_event("finished") self.current_job = None self.current_job_id = None + self.job_user = "No User" async def get_job(self, job_id: Union[int, str] @@ -425,6 +437,7 @@ class PrinterJob: self.start_time = time.time() self.total_duration: float = 0. self.auxiliary_data: List[Dict[str, Any]] = [] + self.user: str = "No User" self.update_from_ps(data) def finish(self, diff --git a/moonraker/components/job_queue.py b/moonraker/components/job_queue.py index 279b3ca..a5ae53b 100644 --- a/moonraker/components/job_queue.py +++ b/moonraker/components/job_queue.py @@ -135,7 +135,9 @@ class JobQueue: raise self.server.error( "Queue State Changed during Transition Gcode") self._set_queue_state("starting") - await kapis.start_print(filename, wait_klippy_started=True) + await kapis.start_print( + filename, wait_klippy_started=True, user=job.user + ) except self.server.error: logging.exception(f"Error Loading print: {filename}") self._set_queue_state("paused") @@ -165,7 +167,8 @@ class JobQueue: async def queue_job(self, filenames: Union[str, List[str]], check_exists: bool = True, - reset: bool = False + reset: bool = False, + user: Optional[Dict[str, Any]] = None ) -> None: async with self.lock: # Make sure that the file exists @@ -178,7 +181,7 @@ class JobQueue: if reset: self.queued_jobs.clear() for fname in filenames: - queued_job = QueuedJob(fname) + queued_job = QueuedJob(fname, user) self.queued_jobs[queued_job.job_id] = queued_job self._send_queue_event(action="jobs_added") @@ -224,6 +227,7 @@ class JobQueue: else: qs = "ready" if self.automatic else "paused" self._set_queue_state(qs) + def _job_map_to_list(self) -> List[Dict[str, Any]]: cur_time = time.time() return [job.as_dict(cur_time) for @@ -261,7 +265,8 @@ class JobQueue: files = web_request.get_list('filenames') reset = web_request.get_boolean("reset", False) # Validate that all files exist before queueing - await self.queue_job(files, reset=reset) + user = web_request.get_current_user() + await self.queue_job(files, reset=reset, user=user) elif req_type == RequestType.DELETE: if web_request.get_boolean("all", False): await self.delete_job([], all=True) @@ -319,14 +324,19 @@ class JobQueue: await self.pause_queue() class QueuedJob: - def __init__(self, filename: str) -> None: + def __init__(self, filename: str, user: Optional[Dict[str, Any]] = None) -> None: self.filename = filename self.job_id = f"{id(self):016X}" self.time_added = time.time() + self._user = user def __str__(self) -> str: return self.filename + @property + def user(self) -> Optional[Dict[str, Any]]: + return self._user + def as_dict(self, cur_time: float) -> Dict[str, Any]: return { 'filename': self.filename, diff --git a/moonraker/components/klippy_apis.py b/moonraker/components/klippy_apis.py index b24e65b..c11c324 100644 --- a/moonraker/components/klippy_apis.py +++ b/moonraker/components/klippy_apis.py @@ -89,7 +89,8 @@ class KlippyAPI(APITransport): async def _gcode_start_print(self, web_request: WebRequest) -> str: filename: str = web_request.get_str('filename') - return await self.start_print(filename) + user = web_request.get_current_user() + return await self.start_print(filename, user=user) async def _gcode_restart(self, web_request: WebRequest) -> str: return await self.do_restart("RESTART") @@ -123,7 +124,10 @@ class KlippyAPI(APITransport): return result async def start_print( - self, filename: str, wait_klippy_started: bool = False + self, + filename: str, + wait_klippy_started: bool = False, + user: Optional[Dict[str, Any]] = None ) -> str: # WARNING: Do not call this method from within the following # event handlers when "wait_klippy_started" is set to True: @@ -139,7 +143,9 @@ class KlippyAPI(APITransport): if wait_klippy_started: await self.klippy.wait_started() logging.info(f"Requesting Job Start, filename = {filename}") - return await self.run_gcode(script) + ret = await self.run_gcode(script) + self.server.send_event("klippy_apis:job_start_complete", user) + return ret async def pause_print( self, default: Union[Sentinel, _T] = Sentinel.MISSING diff --git a/moonraker/components/octoprint_compat.py b/moonraker/components/octoprint_compat.py index fca83c7..bd68e0a 100644 --- a/moonraker/components/octoprint_compat.py +++ b/moonraker/components/octoprint_compat.py @@ -388,9 +388,10 @@ class OctoPrintCompat: except self.server.error: pstate = "not_avail" started: bool = False + user = web_request.get_current_user() if pstate not in ["printing", "paused", "not_avail"]: try: - await self.klippy_apis.start_print(filename) + await self.klippy_apis.start_print(filename, user=user) except self.server.error: started = False else: @@ -400,7 +401,7 @@ class OctoPrintCompat: if fmgr.upload_queue_enabled(): job_queue: JobQueue = self.server.lookup_component( 'job_queue') - await job_queue.queue_job(filename, check_exists=False) + await job_queue.queue_job(filename, check_exists=False, user=user) logging.debug(f"Job '{filename}' queued via OctoPrint API") else: raise self.server.error("Conflict", 409) diff --git a/moonraker/components/simplyprint.py b/moonraker/components/simplyprint.py index 6d7d91c..e2c3d3c 100644 --- a/moonraker/components/simplyprint.py +++ b/moonraker/components/simplyprint.py @@ -1598,7 +1598,7 @@ class PrintHandler: kapi: KlippyAPI = self.server.lookup_component("klippy_apis") data = {"state": "started"} try: - await kapi.start_print(pending) + await kapi.start_print(pending, user={"username": "SimplyPrint"}) except Exception: logging.exception("Print Failed to start") data["state"] = "error"