history: report user in job history

When possible record the name of the user that requested the
job.  The klippy_api's component now takes an optional user
argument in its "start_print" method.  This user is broadcast
in an event after a print request successfully returns.

Signed-off-by:  Eric Callahan <arksine.code@gmail.com>
This commit is contained in:
Eric Callahan 2024-04-25 11:52:32 -04:00
parent 1dfbffb422
commit 531028ef4f
7 changed files with 49 additions and 15 deletions

View File

@ -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:

View File

@ -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(

View File

@ -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,

View File

@ -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,

View File

@ -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

View File

@ -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)

View File

@ -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"