meerschaum.jobs
Higher-level utilities for managing meerschaum.utils.daemon.Daemon.
1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Higher-level utilities for managing `meerschaum.utils.daemon.Daemon`. 7""" 8 9import meerschaum as mrsm 10from meerschaum.utils.typing import Dict, Optional, List, SuccessTuple 11 12from meerschaum.jobs import _native 13from meerschaum.jobs._Job import Job, StopMonitoringLogs 14from meerschaum.jobs._Executor import Executor 15from meerschaum.jobs._access import ( 16 filter_visible_jobs, 17 can_see_job, 18 job_access, 19 ownership_properties, 20 get_team_jobs, 21 move_team_jobs, 22) 23 24__all__ = ( 25 'Job', 26 'StopMonitoringLogs', 27 'systemd', 28 'get_jobs', 29 'get_filtered_jobs', 30 'get_restart_jobs', 31 'get_running_jobs', 32 'get_stopped_jobs', 33 'get_paused_jobs', 34 'get_restart_jobs', 35 'get_executor_keys_from_context', 36 'make_executor', 37 'Executor', 38 'check_restart_jobs', 39 'start_check_jobs_thread', 40 'stop_check_jobs_thread', 41 'filter_visible_jobs', 42 'can_see_job', 43 'job_access', 44 'ownership_properties', 45 'get_team_jobs', 46 'move_team_jobs', 47 'report_lap', 48) 49 50executor_types: List[str] = ['api', 'local'] 51 52 53#: Set by the worker while it runs a plugin action for the Rust host (`report_lap`). 54_lap_reporter = None 55 56 57def report_lap( 58 success: bool = True, 59 message: str = '', 60 next_in: Optional[float] = None, 61) -> SuccessTuple: 62 """ 63 Record a lap of a plugin action that repeats its work by itself (its function takes `loop`, 64 so `--loop` is left to it): the job's health then shows how its laps go, and the Overview's 65 Next runs when the next one is due. 66 67 Call it once at the end of each iteration. Outside a job (a foreground run, a script) it does 68 nothing. An action without a `loop` parameter does not need it: with `--loop` or `-s`, the 69 host runs it in laps and records them itself. 70 71 Parameters 72 ---------- 73 success: bool, default True 74 Whether this lap succeeded (a failed lap counts toward `consecutive_failures`). 75 76 message: str, default '' 77 What the lap did, shown with the job's health. 78 79 next_in: Optional[float], default None 80 Seconds until the next lap begins (the action's own `min_seconds`). It sets the cadence 81 the job is expected at (stalled after three times it) and `job.health['next_run']`. 82 83 Returns 84 ------- 85 A `SuccessTuple`: whether the lap was recorded. 86 87 Examples 88 -------- 89 >>> import time 90 >>> import meerschaum as mrsm 91 >>> def heartbeat_ack(loop: bool = False, min_seconds: float = 20, **kwargs): 92 ... while True: 93 ... success, msg = ack_once() 94 ... mrsm.jobs.report_lap(success, msg, next_in=min_seconds) 95 ... if not loop: 96 ... return success, msg 97 ... time.sleep(min_seconds) 98 """ 99 reporter = _lap_reporter 100 if reporter is None: 101 return True, "Not running in a job's plugin action: the lap was not recorded." 102 lap = {'success': bool(success), 'message': str(message or '')} 103 if next_in is not None: 104 lap['next_in'] = float(next_in) 105 value = reporter(lap) 106 if isinstance(value, (list, tuple)) and len(value) == 2: 107 return bool(value[0]), str(value[1]) 108 return False, f"Unexpected answer from the host: {value!r}" 109 110 111def _rows_to_jobs(rows) -> Dict[str, Job]: 112 """Build the `Job`s of a native listing.""" 113 jobs = {} 114 for row in rows: 115 keys = row['executor_keys'] 116 jobs[row['name']] = Job( 117 row['name'], 118 None if keys == 'local' else row['sysargs'], 119 executor_keys=keys, 120 ) 121 return jobs 122 123 124def get_jobs( 125 executor_keys: Optional[str] = None, 126 include_hidden: bool = False, 127 combine_local_and_systemd: bool = True, 128 debug: bool = False, 129) -> Dict[str, Job]: 130 """ 131 Return a dictionary of the existing jobs. 132 133 Parameters 134 ---------- 135 executor_keys: Optional[str], default None 136 If provided, return remote jobs on the given API instance. 137 Otherwise return local jobs. 138 139 include_hidden: bool, default False 140 If `True`, include jobs with the `hidden` attribute. 141 142 Returns 143 ------- 144 A dictionary mapping job names to jobs. 145 """ 146 from meerschaum.utils.warnings import warn 147 native = _native.core() 148 try: 149 rows, warnings = native.jobs_get_jobs( 150 executor_keys, 151 include_hidden, 152 combine_local_and_systemd, 153 api_attributes=_native.api_attributes(executor_keys), 154 ) 155 except RuntimeError as e: 156 ### An unreachable or unauthorized executor is not "no jobs". 157 warn(f"Could not get the jobs of executor '{executor_keys}': {e}", stack=False) 158 return {} 159 160 _native.emit_warnings(warnings) 161 return _rows_to_jobs(rows) 162 163 164def get_filtered_jobs( 165 executor_keys: Optional[str] = None, 166 filter_list: Optional[List[str]] = None, 167 include_hidden: bool = False, 168 combine_local_and_systemd: bool = True, 169 warn: bool = False, 170 debug: bool = False, 171) -> Dict[str, Job]: 172 """ 173 Return a list of jobs filtered by the user. 174 """ 175 from meerschaum.utils.warnings import warn as _warn 176 native = _native.core() 177 try: 178 rows, listing_warnings, missing = native.jobs_get_filtered_jobs( 179 executor_keys, 180 filter_list or [], 181 include_hidden, 182 combine_local_and_systemd, 183 api_attributes=_native.api_attributes(executor_keys), 184 ) 185 except RuntimeError as e: 186 _warn(f"Could not get the jobs of executor '{executor_keys}': {e}", stack=False) 187 return {} 188 189 _native.emit_warnings(listing_warnings) 190 if warn: 191 _native.emit_warnings(missing) 192 return _rows_to_jobs(rows) 193 194 195def get_restart_jobs( 196 executor_keys: Optional[str] = None, 197 jobs: Optional[Dict[str, Job]] = None, 198 include_hidden: bool = False, 199 combine_local_and_systemd: bool = True, 200 debug: bool = False, 201) -> Dict[str, Job]: 202 """ 203 Return jobs which were created with `--restart` or `--loop`. 204 """ 205 if jobs is None: 206 jobs = get_jobs( 207 executor_keys, 208 include_hidden=include_hidden, 209 combine_local_and_systemd=combine_local_and_systemd, 210 debug=debug, 211 ) 212 213 return { 214 name: job 215 for name, job in jobs.items() 216 if job.restart 217 } 218 219 220def get_running_jobs( 221 executor_keys: Optional[str] = None, 222 jobs: Optional[Dict[str, Job]] = None, 223 include_hidden: bool = False, 224 combine_local_and_systemd: bool = True, 225 debug: bool = False, 226) -> Dict[str, Job]: 227 """ 228 Return a dictionary of running jobs. 229 """ 230 if jobs is None: 231 jobs = get_jobs( 232 executor_keys, 233 include_hidden=include_hidden, 234 combine_local_and_systemd=combine_local_and_systemd, 235 debug=debug, 236 ) 237 238 return { 239 name: job 240 for name, job in jobs.items() 241 if job.status == 'running' 242 } 243 244 245def get_paused_jobs( 246 executor_keys: Optional[str] = None, 247 jobs: Optional[Dict[str, Job]] = None, 248 include_hidden: bool = False, 249 combine_local_and_systemd: bool = True, 250 debug: bool = False, 251) -> Dict[str, Job]: 252 """ 253 Return a dictionary of paused jobs. 254 """ 255 if jobs is None: 256 jobs = get_jobs( 257 executor_keys, 258 include_hidden=include_hidden, 259 combine_local_and_systemd=combine_local_and_systemd, 260 debug=debug, 261 ) 262 263 return { 264 name: job 265 for name, job in jobs.items() 266 if job.status == 'paused' 267 } 268 269 270def get_stopped_jobs( 271 executor_keys: Optional[str] = None, 272 jobs: Optional[Dict[str, Job]] = None, 273 include_hidden: bool = False, 274 combine_local_and_systemd: bool = True, 275 debug: bool = False, 276) -> Dict[str, Job]: 277 """ 278 Return a dictionary of stopped jobs. 279 """ 280 if jobs is None: 281 jobs = get_jobs( 282 executor_keys, 283 include_hidden=include_hidden, 284 combine_local_and_systemd=combine_local_and_systemd, 285 debug=debug, 286 ) 287 288 return { 289 name: job 290 for name, job in jobs.items() 291 if job.status == 'stopped' 292 } 293 294 295def make_executor(cls): 296 """ 297 Register a class as an `Executor`. 298 """ 299 import re 300 from meerschaum.connectors import make_connector 301 suffix_regex = r'executor$' 302 typ = re.sub(suffix_regex, '', cls.__name__.lower()) 303 if typ not in executor_types: 304 executor_types.append(typ) 305 return make_connector(cls, _is_executor=True) 306 307 308def check_restart_jobs( 309 executor_keys: Optional[str] = 'local', 310 jobs: Optional[Dict[str, Job]] = None, 311 include_hidden: bool = True, 312 silent: bool = False, 313 debug: bool = False, 314) -> SuccessTuple: 315 """ 316 Restart any stopped jobs which were created with `--restart`. 317 318 Parameters 319 ---------- 320 executor_keys: Optional[str], default None 321 If provided, check jobs on the given remote API instance. 322 Otherwise check local jobs. 323 324 include_hidden: bool, default True 325 If `True`, include hidden jobs in the check. 326 327 silent: bool, default False 328 If `True`, do not print the restart success message. 329 """ 330 from meerschaum.utils.misc import items_str 331 332 if jobs is None: 333 jobs = get_jobs( 334 executor_keys, 335 include_hidden=include_hidden, 336 combine_local_and_systemd=False, 337 debug=debug, 338 ) 339 340 if not jobs: 341 return True, "No jobs to restart." 342 343 results = {} 344 for name, job in jobs.items(): 345 check_success, check_msg = job.check_restart() 346 results[job.name] = (check_success, check_msg) 347 if not silent: 348 mrsm.pprint((check_success, check_msg)) 349 350 success_names = [name for name, (check_success, check_msg) in results.items() if check_success] 351 fail_names = [name for name, (check_success, check_msg) in results.items() if not check_success] 352 success = len(success_names) == len(jobs) 353 msg = ( 354 ( 355 "Successfully restarted job" 356 + ('s' if len(success_names) != 1 else '') 357 + ' ' + items_str(success_names) + '.' 358 ) 359 if success 360 else ( 361 "Failed to restart job" 362 + ('s' if len(success_names) != 1 else '') 363 + ' ' + items_str(fail_names) + '.' 364 ) 365 ) 366 return success, msg 367 368 369def _check_restart_jobs_against_lock(*args, **kwargs): 370 import fcntl 371 import meerschaum.config.paths as paths 372 paths.CHECK_JOBS_LOCK_PATH.parent.mkdir(parents=True, exist_ok=True) 373 with open(paths.CHECK_JOBS_LOCK_PATH, 'a+', encoding='utf-8') as lock_file: 374 fcntl.flock(lock_file, fcntl.LOCK_EX) 375 try: 376 check_restart_jobs(*args, **kwargs) 377 finally: 378 fcntl.flock(lock_file, fcntl.LOCK_UN) 379 380 381_check_loop_stop_thread = None 382def start_check_jobs_thread(): 383 """ 384 Start a thread to regularly monitor jobs. 385 """ 386 import atexit 387 from functools import partial 388 from meerschaum.utils.threading import RepeatTimer 389 from meerschaum._internal.static import STATIC_CONFIG 390 391 global _check_loop_stop_thread 392 sleep_seconds = STATIC_CONFIG['jobs']['check_restart_seconds'] 393 394 _check_loop_stop_thread = RepeatTimer( 395 sleep_seconds, 396 partial( 397 _check_restart_jobs_against_lock, 398 silent=True, 399 ) 400 ) 401 _check_loop_stop_thread.daemon = True 402 atexit.register(stop_check_jobs_thread) 403 404 _check_loop_stop_thread.start() 405 return _check_loop_stop_thread 406 407 408def stop_check_jobs_thread(): 409 """ 410 Stop the job monitoring thread. 411 """ 412 import meerschaum.config.paths as paths 413 from meerschaum.utils.warnings import warn 414 if _check_loop_stop_thread is None: 415 return 416 417 _check_loop_stop_thread.cancel() 418 419 try: 420 if paths.CHECK_JOBS_LOCK_PATH.exists(): 421 paths.CHECK_JOBS_LOCK_PATH.unlink() 422 except Exception as e: 423 warn(f"Failed to remove check jobs lock file:\n{e}") 424 425 426def get_executor_keys_from_context() -> str: 427 """ 428 Return the configured executor (`meerschaum:executor`). 429 Otherwise, on a host with `systemd`, return `'systemd'` (for any root directory) unless 430 Meerschaum runs from a source checkout or a development build. 431 Otherwise return `'local'`. 432 """ 433 return _native.core().jobs_executor_keys_from_context() 434 435 436def _install_healthcheck_job() -> SuccessTuple: 437 """ 438 Install the systemd job which checks local jobs. 439 """ 440 from meerschaum.config import get_config 441 442 enable_healthcheck = get_config('system', 'experimental', 'systemd_healthcheck') 443 if not enable_healthcheck: 444 return False, "Local healthcheck is disabled." 445 446 if get_executor_keys_from_context() != 'systemd': 447 return False, "Not running systemd." 448 449 job = Job( 450 '.local-healthcheck', 451 ['restart', 'jobs', '-e', 'local', '--loop', '--min-seconds', '60'], 452 executor_keys='systemd', 453 ) 454 return job.start()
90class Job: 91 """ 92 Manage a job, locally, with `systemd`, or remotely via the API. 93 """ 94 95 def __init__( 96 self, 97 name: str, 98 sysargs: Union[List[str], str, None] = None, 99 env: Optional[Dict[str, str]] = None, 100 executor_keys: Optional[str] = None, 101 delete_after_completion: bool = False, 102 refresh_seconds: Union[int, float, None] = None, 103 _properties: Optional[Dict[str, Any]] = None, 104 _status_hook: Optional[Callable[[], str]] = None, 105 _result_hook: Optional[Callable[[], SuccessTuple]] = None, 106 _externally_managed: bool = False, 107 ): 108 """ 109 Create a new job. 110 111 Parameters 112 ---------- 113 name: str 114 The name of the job to be created. 115 This will also be used as the Daemon ID. 116 117 sysargs: Union[List[str], str, None], default None 118 The sysargs of the command to be executed, e.g. 'start api'. 119 120 env: Optional[Dict[str, str]], default None 121 If provided, set these environment variables in the job's process. 122 123 executor_keys: Optional[str], default None 124 If provided, execute the job remotely on an API instance, e.g. 'api:main' 125 (or with `'systemd'`). Defaults to `'local'`, or the context's executor. 126 127 delete_after_completion: bool, default False 128 If `True`, delete this job when it has finished executing. 129 130 refresh_seconds: Union[int, float, None], default None 131 The number of seconds to sleep between refreshes. 132 Defaults to the configured value `system.cli.refresh_seconds`. 133 134 _properties: Optional[Dict[str, Any]], default None 135 If provided, use this to patch the daemon's properties. 136 """ 137 native = _native.core() 138 self._core = native.Job( 139 name, 140 sysargs, 141 env=env, 142 executor_keys=executor_keys, 143 delete_after_completion=delete_after_completion, 144 refresh_seconds=refresh_seconds, 145 properties=_properties, 146 externally_managed=_externally_managed, 147 api_attributes=_native.api_attributes(executor_keys), 148 ) 149 _native.emit_warnings(self._core.take_warnings()) 150 self.name = name 151 self.executor_keys = self._core.executor_keys 152 self.refresh_seconds = ( 153 refresh_seconds 154 if refresh_seconds is not None 155 else mrsm.get_config('system', 'cli', 'refresh_seconds') 156 ) 157 if _status_hook is not None: 158 self._status_hook = _status_hook 159 160 if _result_hook is not None: 161 self._result_hook = _result_hook 162 163 self._externally_managed = _externally_managed 164 165 def _warn(self) -> None: 166 """Show the warnings the last native operation produced.""" 167 _native.emit_warnings(self._core.take_warnings()) 168 169 @staticmethod 170 def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job: 171 """ 172 Build a `Job` from the PID of a running Meerschaum process. 173 174 Parameters 175 ---------- 176 pid: int 177 The PID of the process. 178 179 executor_keys: Optional[str], default None 180 The executor keys to assign to the job. 181 """ 182 import os 183 try: 184 os.kill(pid, 0) 185 except ProcessLookupError as e: 186 warn(f"Process with PID {pid} does not exist.", stack=False) 187 raise e 188 except PermissionError: 189 pass 190 191 native = _native.core() 192 daemon_ids = native.jobs_daemon_ids() 193 for daemon_id in daemon_ids: 194 job = Job(daemon_id, executor_keys=executor_keys) 195 if job.pid == pid: 196 return job 197 198 cmdline_path = pathlib.Path('/proc') / str(pid) / 'cmdline' 199 if not cmdline_path.exists(): 200 raise EnvironmentError(f"Is process {pid} a Meerschaum job?") 201 202 command_args = [ 203 arg 204 for arg in cmdline_path.read_bytes().decode('utf-8', errors='replace').split('\0') 205 if arg 206 ] 207 # During startup the PID may change from the supervisor to its worker between 208 # the caller's PID read and the registry lookup above. Scope its persistent marker 209 # to this registry: another root may have a different Job with the same name. 210 if '--jobs-dir' in command_args: 211 import meerschaum.config.paths as paths 212 index = command_args.index('--jobs-dir') + 1 213 if ( 214 index >= len(command_args) 215 or pathlib.Path(command_args[index]).resolve() != paths.DAEMON_RESOURCES_PATH.resolve() 216 ): 217 raise EnvironmentError(f"Process {pid} belongs to another Meerschaum jobs directory.") 218 for arg in command_args: 219 if arg.startswith("daemon_id='") and arg.endswith("'"): 220 daemon_id = arg[len("daemon_id='"):-1] 221 if daemon_id in daemon_ids: 222 return Job(daemon_id, executor_keys=executor_keys) 223 if '__mrsm_job_supervisor__' in command_args or '__mrsm_run_daemon__' in command_args: 224 if '--daemon-id' in command_args: 225 index = command_args.index('--daemon-id') + 1 226 if index < len(command_args) and command_args[index] in daemon_ids: 227 return Job(command_args[index], executor_keys=executor_keys) 228 229 if len(command_args) > 1 and command_args[1] == '-c': 230 raise EnvironmentError(f"Is process {pid} a running job?") 231 232 mrsm_ix = 0 233 for i, arg in enumerate(command_args): 234 if 'mrsm' in arg or 'meerschaum' in arg.lower(): 235 mrsm_ix = i 236 break 237 238 sysargs = command_args[mrsm_ix + 1:] 239 name = None 240 for flag in ('--name', '--job-name'): 241 if flag in sysargs and sysargs.index(flag) + 1 < len(sysargs): 242 name = sysargs[sysargs.index(flag) + 1] 243 break 244 if name is None: 245 name = native.jobs_new_daemon_name() 246 return Job(name, sysargs, executor_keys=executor_keys) 247 248 def start(self, debug: bool = False) -> SuccessTuple: 249 """ 250 Start the job's daemon. 251 """ 252 result = self._core.start() 253 self._warn() 254 return result 255 256 def stop( 257 self, 258 timeout_seconds: Union[int, float, None] = None, 259 debug: bool = False, 260 ) -> SuccessTuple: 261 """ 262 Stop the job's daemon. 263 """ 264 result = self._core.stop(timeout_seconds) 265 self._warn() 266 return result 267 268 def pause( 269 self, 270 timeout_seconds: Union[int, float, None] = None, 271 debug: bool = False, 272 ) -> SuccessTuple: 273 """ 274 Pause the job's daemon. 275 """ 276 result = self._core.pause(timeout_seconds) 277 self._warn() 278 return result 279 280 def resume( 281 self, 282 timeout_seconds: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """ 286 Resume a paused job. 287 """ 288 result = self._core.resume(timeout_seconds) 289 self._warn() 290 return result 291 292 def delete(self, debug: bool = False) -> SuccessTuple: 293 """ 294 Delete the job and its daemon. 295 """ 296 result = self._core.delete() 297 self._warn() 298 return result 299 300 def is_running(self) -> bool: 301 """ 302 Determine whether the job's daemon is running. 303 """ 304 return self.status == 'running' 305 306 def exists(self, debug: bool = False) -> bool: 307 """ 308 Determine whether the job exists. 309 """ 310 return self._core.exists() 311 312 def get_logs(self) -> Union[str, None]: 313 """ 314 Return the output text of the job's daemon. 315 """ 316 return self._core.get_logs() 317 318 def monitor_logs( 319 self, 320 callback_function: Callable[[str], None] = _default_stdout_callback, 321 input_callback_function: Optional[Callable[[], str]] = None, 322 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 323 stop_event: Optional[asyncio.Event] = None, 324 stop_on_exit: bool = False, 325 strip_timestamps: bool = False, 326 accept_input: bool = True, 327 debug: bool = False, 328 _wait_if_stopped: bool = True, 329 ): 330 """ 331 Monitor the job's log files and execute a callback on new lines. 332 333 Parameters 334 ---------- 335 callback_function: Callable[[str], None], default partial(print, end='') 336 The callback to execute as new data comes in. 337 Defaults to printing the output directly to `stdout`. 338 339 input_callback_function: Optional[Callable[[], str]], default None 340 If provided, execute this callback when the daemon is blocking on stdin. 341 Defaults to `sys.stdin.readline()`. 342 343 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 344 If provided, execute this callback when the daemon stops. 345 The job's SuccessTuple will be passed to the callback. 346 347 stop_event: Optional[asyncio.Event], default None 348 If provided, stop monitoring when this event is set. 349 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 350 from within `callback_function` to stop monitoring. 351 352 stop_on_exit: bool, default False 353 If `True`, stop monitoring when the job stops. 354 355 strip_timestamps: bool, default False 356 If `True`, remove leading timestamps from lines. 357 358 accept_input: bool, default True 359 If `True`, accept input when the daemon blocks on stdin. 360 """ 361 return self._monitor( 362 callback_function, 363 input_callback_function, 364 stop_callback_function, 365 stop_event, 366 stop_on_exit, 367 strip_timestamps, 368 accept_input, 369 _wait_if_stopped, 370 loop=None, 371 stop_flag=None, 372 ) 373 374 async def monitor_logs_async( 375 self, 376 callback_function: Callable[[str], None] = _default_stdout_callback, 377 input_callback_function: Optional[Callable[[], str]] = None, 378 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 379 stop_event: Optional[asyncio.Event] = None, 380 stop_on_exit: bool = False, 381 strip_timestamps: bool = False, 382 accept_input: bool = True, 383 debug: bool = False, 384 _wait_if_stopped: bool = True, 385 ): 386 """ 387 Monitor the job's log files and await a callback on new lines. 388 389 Parameters 390 ---------- 391 callback_function: Callable[[str], None], default _default_stdout_callback 392 The callback to execute as new data comes in. 393 Defaults to printing the output directly to `stdout`. 394 395 input_callback_function: Optional[Callable[[], str]], default None 396 If provided, execute this callback when the daemon is blocking on stdin. 397 Defaults to `sys.stdin.readline()`. 398 399 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 400 If provided, execute this callback when the daemon stops. 401 The job's SuccessTuple will be passed to the callback. 402 403 stop_event: Optional[asyncio.Event], default None 404 If provided, stop monitoring when this event is set. 405 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 406 from within `callback_function` to stop monitoring. 407 408 stop_on_exit: bool, default False 409 If `True`, stop monitoring when the job stops. 410 411 strip_timestamps: bool, default False 412 If `True`, remove leading timestamps from lines. 413 414 accept_input: bool, default True 415 If `True`, accept input when the daemon blocks on stdin. 416 """ 417 loop = asyncio.get_running_loop() 418 stop_flag = _native.core().StopFlag() 419 run = functools.partial( 420 self._monitor, 421 callback_function, 422 input_callback_function, 423 stop_callback_function, 424 stop_event, 425 stop_on_exit, 426 strip_timestamps, 427 accept_input, 428 _wait_if_stopped, 429 loop=loop, 430 stop_flag=stop_flag, 431 ) 432 try: 433 await loop.run_in_executor(None, run) 434 except asyncio.CancelledError: 435 stop_flag.set() 436 raise 437 finally: 438 stop_flag.set() 439 440 def _monitor( 441 self, 442 callback_function, 443 input_callback_function, 444 stop_callback_function, 445 stop_event, 446 stop_on_exit, 447 strip_timestamps, 448 accept_input, 449 wait_if_stopped, 450 loop, 451 stop_flag, 452 ): 453 """ 454 Follow the logs with the native monitor, running the callbacks on this thread's helper 455 (coroutine functions are awaited on `loop`, or on a private loop when there is none). 456 """ 457 def sync(function): 458 if function is None or not inspect.iscoroutinefunction(function): 459 return function 460 461 def call(*args): 462 coroutine = function(*args) 463 if loop is not None: 464 return asyncio.run_coroutine_threadsafe(coroutine, loop).result() 465 return asyncio.run(coroutine) 466 467 return call 468 469 def default_input_callback_function(): 470 from meerschaum.utils.prompt import prompt 471 prompt_kwargs = self.get_prompt_kwargs() 472 if prompt_kwargs: 473 answer = prompt(**prompt_kwargs) 474 return answer + '\n' 475 return sys.stdin.readline() 476 477 input_callback = sync(input_callback_function) or default_input_callback_function 478 self._core.monitor_logs( 479 sync(callback_function), 480 input_callback=input_callback if accept_input else None, 481 stop_callback=sync(stop_callback_function), 482 stop_event=stop_event, 483 stop_flag=stop_flag, 484 stop_on_exit=stop_on_exit, 485 strip_timestamps=strip_timestamps, 486 accept_input=accept_input, 487 wait_if_stopped=wait_if_stopped, 488 stop_exception=StopMonitoringLogs, 489 clear_callback=_clear_screen, 490 warn=lambda message: warn(message), 491 ) 492 493 def is_blocking_on_stdin(self, debug: bool = False) -> bool: 494 """ 495 Return whether a job's daemon is blocking on stdin. 496 """ 497 return self._core.is_blocking_on_stdin() 498 499 def get_prompt_kwargs(self, debug: bool = False) -> Dict[str, Any]: 500 """ 501 Return the kwargs to the blocking `prompt()`, if available. 502 """ 503 return self._core.get_prompt_kwargs() 504 505 def write_stdin(self, data): 506 """ 507 Write to a job's daemon's `stdin`. 508 """ 509 self._core.write_stdin(data) 510 511 @property 512 def executor(self) -> Union[Executor, None]: 513 """ 514 If the job is not local, return the connector to its executor 515 (the remote API instance, `systemd`, a plugin's executor). 516 """ 517 if self.executor_keys == 'local': 518 return None 519 from meerschaum.jobs._native import is_python_executor_type 520 if is_python_executor_type(self.executor_keys.split(':', maxsplit=1)[0]): 521 return mrsm.get_connector(self.executor_keys) 522 ### An executor type of a native (Rust) plugin: no Python connector exists. 523 from meerschaum.jobs._NativeExecutor import NativeExecutor 524 return NativeExecutor(self.executor_keys) 525 526 @property 527 def status(self) -> str: 528 """ 529 Return the running status of the job's daemon. 530 """ 531 if '_status_hook' in self.__dict__: 532 return self._status_hook() 533 534 return self._core.status 535 536 @property 537 def pid(self) -> Union[int, None]: 538 """ 539 Return the PID of the job's dameon. 540 """ 541 return self._core.pid 542 543 @property 544 def restart(self) -> bool: 545 """ 546 Return whether to restart a stopped job. 547 """ 548 return self._core.restarts 549 550 @property 551 def result(self) -> SuccessTuple: 552 """ 553 Return the `SuccessTuple` when the job has terminated. 554 """ 555 if '_result_hook' in self.__dict__: 556 if self.is_running(): 557 return True, f"{self} is running." 558 return self._result_hook() 559 560 return self._core.result 561 562 @property 563 def sysargs(self) -> List[str]: 564 """ 565 Return the sysargs to use for the Daemon. 566 """ 567 return self._core.sysargs 568 569 @property 570 def health(self) -> Dict[str, Any]: 571 """ 572 Return how the job's laps go (a job that repeats its work: `--loop`, `-s`, `--listen`). 573 574 Returns 575 ------- 576 A dictionary with the keys: 577 578 - `state`: `'healthy'` (the last lap succeeded), `'failing'` (the last lap failed), 579 `'stalled'` (no lap for three times the expected interval, and at least a minute; 580 an hour without a sync for `--listen`; at least an hour for a first lap), 581 `'stopped'` (not running, or paused) or `'unknown'` (nothing recorded yet, e.g. 582 `'first sync in progress'`). 583 - `last_success`, `last_lap`: ISO-8601 UTC timestamps (or `None`). 584 - `consecutive_failures`: laps that failed in a row. 585 - `expected_interval_seconds`: how often laps are expected (`--min-seconds`, the 586 schedule's period, an hour for `--listen`), or `None`. 587 - `next_run`: while a running job waits for its next lap (`--min-seconds`, a cooldown 588 after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); 589 otherwise `None`. 590 - `message`: what to show next to the state, e.g. `'last good sync 2 min ago'`. 591 592 Examples 593 -------- 594 >>> job = Job('weather', 'sync pipes -c plugin:noaa --loop') 595 >>> success, msg = job.start() 596 >>> job.health['state'] # Before its first lap ends. 597 'unknown' 598 """ 599 return self._core.health() 600 601 @property 602 def owner(self) -> Union[str, None]: 603 """ 604 Return the user who started this job through the API server or the web console 605 (`None` for a job created locally: the CLI, Compose, systemd or Python). A job started 606 by another job has that job's owner. For an `api:` job, the server's record. 607 608 Examples 609 -------- 610 >>> Job('nightly', executor_keys='api:main').owner 611 'alice' 612 """ 613 return self._core.access().get('owner', None) 614 615 @property 616 def team(self) -> Union[str, None]: 617 """ 618 Return the team this job belongs to: the primary team of the user who started it through 619 the API server or the web console (a job started by another job: that job's team). 620 `None` means Everyone (every local job). 621 622 Members of the team (and administrators) see the job and, with the job permissions, 623 control it. Renaming the team renames it here; deleting the team moves the job to 624 Everyone. 625 """ 626 return self._core.access().get('team', None) 627 628 def get_daemon_properties(self) -> Dict[str, Any]: 629 """ 630 Return the `properties` dictionary for the job's daemon. 631 """ 632 return self._core.get_daemon_properties() 633 634 @property 635 def daemon(self) -> 'Daemon': 636 """ 637 Return the daemon which this job manages. 638 """ 639 from meerschaum.utils.daemon import Daemon 640 if self.executor_keys == 'local' and self.exists(): 641 return Daemon(daemon_id=self.name) 642 643 return Daemon( 644 daemon_id=self.name, 645 sysargs=self.sysargs, 646 label=shlex.join(self.sysargs), 647 properties=self.get_daemon_properties(), 648 ) 649 650 @property 651 def began(self) -> Union[datetime, None]: 652 """ 653 The datetime when the job began running. 654 """ 655 return _parse_dt(self._core.began) 656 657 @property 658 def ended(self) -> Union[datetime, None]: 659 """ 660 The datetime when the job stopped running. 661 """ 662 return _parse_dt(self._core.ended) 663 664 @property 665 def paused(self) -> Union[datetime, None]: 666 """ 667 The datetime when the job was suspended while running. 668 """ 669 return _parse_dt(self._core.paused) 670 671 @property 672 def stop_time(self) -> Union[datetime, None]: 673 """ 674 Return the timestamp when the job was manually stopped. 675 """ 676 return _parse_dt(self._core.stop_time) 677 678 @property 679 def hidden(self) -> bool: 680 """ 681 Return a bool indicating whether this job should be displayed. 682 """ 683 return self._core.hidden 684 685 def check_restart(self) -> SuccessTuple: 686 """ 687 If `restart` is `True` and the daemon is not running, 688 restart the job. 689 Do not restart if the job was manually stopped. 690 """ 691 result = self._core.check_restart() 692 self._warn() 693 return result 694 695 @property 696 def label(self) -> str: 697 """ 698 Return the job's Daemon label (joined sysargs). 699 """ 700 return self._core.label 701 702 @property 703 def _externally_managed_file(self) -> pathlib.Path: 704 """ 705 Return the path to the externally managed file. 706 """ 707 return pathlib.Path(self._core.paths()['externally_managed']) 708 709 def _set_externally_managed(self): 710 """ 711 Set this job as externally managed. 712 """ 713 self._externally_managed = True 714 self._core.set_externally_managed() 715 716 @property 717 def _is_externally_managed(self) -> bool: 718 """ 719 Return whether this job is externally managed. 720 """ 721 return self._core.is_externally_managed 722 723 @property 724 def env(self) -> Dict[str, str]: 725 """ 726 Return the environment variables to set for the job's process. 727 """ 728 return self._core.env 729 730 @property 731 def delete_after_completion(self) -> bool: 732 """ 733 Return whether this job is configured to delete itself after completion. 734 """ 735 return self._core.delete_after_completion 736 737 def __str__(self) -> str: 738 return self._core.describe() 739 740 def __repr__(self) -> str: 741 return str(self) 742 743 def __hash__(self) -> int: 744 return hash(self.name)
Manage a job, locally, with systemd, or remotely via the API.
95 def __init__( 96 self, 97 name: str, 98 sysargs: Union[List[str], str, None] = None, 99 env: Optional[Dict[str, str]] = None, 100 executor_keys: Optional[str] = None, 101 delete_after_completion: bool = False, 102 refresh_seconds: Union[int, float, None] = None, 103 _properties: Optional[Dict[str, Any]] = None, 104 _status_hook: Optional[Callable[[], str]] = None, 105 _result_hook: Optional[Callable[[], SuccessTuple]] = None, 106 _externally_managed: bool = False, 107 ): 108 """ 109 Create a new job. 110 111 Parameters 112 ---------- 113 name: str 114 The name of the job to be created. 115 This will also be used as the Daemon ID. 116 117 sysargs: Union[List[str], str, None], default None 118 The sysargs of the command to be executed, e.g. 'start api'. 119 120 env: Optional[Dict[str, str]], default None 121 If provided, set these environment variables in the job's process. 122 123 executor_keys: Optional[str], default None 124 If provided, execute the job remotely on an API instance, e.g. 'api:main' 125 (or with `'systemd'`). Defaults to `'local'`, or the context's executor. 126 127 delete_after_completion: bool, default False 128 If `True`, delete this job when it has finished executing. 129 130 refresh_seconds: Union[int, float, None], default None 131 The number of seconds to sleep between refreshes. 132 Defaults to the configured value `system.cli.refresh_seconds`. 133 134 _properties: Optional[Dict[str, Any]], default None 135 If provided, use this to patch the daemon's properties. 136 """ 137 native = _native.core() 138 self._core = native.Job( 139 name, 140 sysargs, 141 env=env, 142 executor_keys=executor_keys, 143 delete_after_completion=delete_after_completion, 144 refresh_seconds=refresh_seconds, 145 properties=_properties, 146 externally_managed=_externally_managed, 147 api_attributes=_native.api_attributes(executor_keys), 148 ) 149 _native.emit_warnings(self._core.take_warnings()) 150 self.name = name 151 self.executor_keys = self._core.executor_keys 152 self.refresh_seconds = ( 153 refresh_seconds 154 if refresh_seconds is not None 155 else mrsm.get_config('system', 'cli', 'refresh_seconds') 156 ) 157 if _status_hook is not None: 158 self._status_hook = _status_hook 159 160 if _result_hook is not None: 161 self._result_hook = _result_hook 162 163 self._externally_managed = _externally_managed
Create a new job.
Parameters
- name (str): The name of the job to be created. This will also be used as the Daemon ID.
- sysargs (Union[List[str], str, None], default None): The sysargs of the command to be executed, e.g. 'start api'.
- env (Optional[Dict[str, str]], default None): If provided, set these environment variables in the job's process.
- executor_keys (Optional[str], default None):
If provided, execute the job remotely on an API instance, e.g. 'api:main'
(or with
'systemd'). Defaults to'local', or the context's executor. - delete_after_completion (bool, default False):
If
True, delete this job when it has finished executing. - refresh_seconds (Union[int, float, None], default None):
The number of seconds to sleep between refreshes.
Defaults to the configured value
system.cli.refresh_seconds. - _properties (Optional[Dict[str, Any]], default None): If provided, use this to patch the daemon's properties.
169 @staticmethod 170 def from_pid(pid: int, executor_keys: Optional[str] = None) -> Job: 171 """ 172 Build a `Job` from the PID of a running Meerschaum process. 173 174 Parameters 175 ---------- 176 pid: int 177 The PID of the process. 178 179 executor_keys: Optional[str], default None 180 The executor keys to assign to the job. 181 """ 182 import os 183 try: 184 os.kill(pid, 0) 185 except ProcessLookupError as e: 186 warn(f"Process with PID {pid} does not exist.", stack=False) 187 raise e 188 except PermissionError: 189 pass 190 191 native = _native.core() 192 daemon_ids = native.jobs_daemon_ids() 193 for daemon_id in daemon_ids: 194 job = Job(daemon_id, executor_keys=executor_keys) 195 if job.pid == pid: 196 return job 197 198 cmdline_path = pathlib.Path('/proc') / str(pid) / 'cmdline' 199 if not cmdline_path.exists(): 200 raise EnvironmentError(f"Is process {pid} a Meerschaum job?") 201 202 command_args = [ 203 arg 204 for arg in cmdline_path.read_bytes().decode('utf-8', errors='replace').split('\0') 205 if arg 206 ] 207 # During startup the PID may change from the supervisor to its worker between 208 # the caller's PID read and the registry lookup above. Scope its persistent marker 209 # to this registry: another root may have a different Job with the same name. 210 if '--jobs-dir' in command_args: 211 import meerschaum.config.paths as paths 212 index = command_args.index('--jobs-dir') + 1 213 if ( 214 index >= len(command_args) 215 or pathlib.Path(command_args[index]).resolve() != paths.DAEMON_RESOURCES_PATH.resolve() 216 ): 217 raise EnvironmentError(f"Process {pid} belongs to another Meerschaum jobs directory.") 218 for arg in command_args: 219 if arg.startswith("daemon_id='") and arg.endswith("'"): 220 daemon_id = arg[len("daemon_id='"):-1] 221 if daemon_id in daemon_ids: 222 return Job(daemon_id, executor_keys=executor_keys) 223 if '__mrsm_job_supervisor__' in command_args or '__mrsm_run_daemon__' in command_args: 224 if '--daemon-id' in command_args: 225 index = command_args.index('--daemon-id') + 1 226 if index < len(command_args) and command_args[index] in daemon_ids: 227 return Job(command_args[index], executor_keys=executor_keys) 228 229 if len(command_args) > 1 and command_args[1] == '-c': 230 raise EnvironmentError(f"Is process {pid} a running job?") 231 232 mrsm_ix = 0 233 for i, arg in enumerate(command_args): 234 if 'mrsm' in arg or 'meerschaum' in arg.lower(): 235 mrsm_ix = i 236 break 237 238 sysargs = command_args[mrsm_ix + 1:] 239 name = None 240 for flag in ('--name', '--job-name'): 241 if flag in sysargs and sysargs.index(flag) + 1 < len(sysargs): 242 name = sysargs[sysargs.index(flag) + 1] 243 break 244 if name is None: 245 name = native.jobs_new_daemon_name() 246 return Job(name, sysargs, executor_keys=executor_keys)
Build a Job from the PID of a running Meerschaum process.
Parameters
- pid (int): The PID of the process.
- executor_keys (Optional[str], default None): The executor keys to assign to the job.
248 def start(self, debug: bool = False) -> SuccessTuple: 249 """ 250 Start the job's daemon. 251 """ 252 result = self._core.start() 253 self._warn() 254 return result
Start the job's daemon.
256 def stop( 257 self, 258 timeout_seconds: Union[int, float, None] = None, 259 debug: bool = False, 260 ) -> SuccessTuple: 261 """ 262 Stop the job's daemon. 263 """ 264 result = self._core.stop(timeout_seconds) 265 self._warn() 266 return result
Stop the job's daemon.
268 def pause( 269 self, 270 timeout_seconds: Union[int, float, None] = None, 271 debug: bool = False, 272 ) -> SuccessTuple: 273 """ 274 Pause the job's daemon. 275 """ 276 result = self._core.pause(timeout_seconds) 277 self._warn() 278 return result
Pause the job's daemon.
280 def resume( 281 self, 282 timeout_seconds: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """ 286 Resume a paused job. 287 """ 288 result = self._core.resume(timeout_seconds) 289 self._warn() 290 return result
Resume a paused job.
292 def delete(self, debug: bool = False) -> SuccessTuple: 293 """ 294 Delete the job and its daemon. 295 """ 296 result = self._core.delete() 297 self._warn() 298 return result
Delete the job and its daemon.
300 def is_running(self) -> bool: 301 """ 302 Determine whether the job's daemon is running. 303 """ 304 return self.status == 'running'
Determine whether the job's daemon is running.
306 def exists(self, debug: bool = False) -> bool: 307 """ 308 Determine whether the job exists. 309 """ 310 return self._core.exists()
Determine whether the job exists.
312 def get_logs(self) -> Union[str, None]: 313 """ 314 Return the output text of the job's daemon. 315 """ 316 return self._core.get_logs()
Return the output text of the job's daemon.
318 def monitor_logs( 319 self, 320 callback_function: Callable[[str], None] = _default_stdout_callback, 321 input_callback_function: Optional[Callable[[], str]] = None, 322 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 323 stop_event: Optional[asyncio.Event] = None, 324 stop_on_exit: bool = False, 325 strip_timestamps: bool = False, 326 accept_input: bool = True, 327 debug: bool = False, 328 _wait_if_stopped: bool = True, 329 ): 330 """ 331 Monitor the job's log files and execute a callback on new lines. 332 333 Parameters 334 ---------- 335 callback_function: Callable[[str], None], default partial(print, end='') 336 The callback to execute as new data comes in. 337 Defaults to printing the output directly to `stdout`. 338 339 input_callback_function: Optional[Callable[[], str]], default None 340 If provided, execute this callback when the daemon is blocking on stdin. 341 Defaults to `sys.stdin.readline()`. 342 343 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 344 If provided, execute this callback when the daemon stops. 345 The job's SuccessTuple will be passed to the callback. 346 347 stop_event: Optional[asyncio.Event], default None 348 If provided, stop monitoring when this event is set. 349 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 350 from within `callback_function` to stop monitoring. 351 352 stop_on_exit: bool, default False 353 If `True`, stop monitoring when the job stops. 354 355 strip_timestamps: bool, default False 356 If `True`, remove leading timestamps from lines. 357 358 accept_input: bool, default True 359 If `True`, accept input when the daemon blocks on stdin. 360 """ 361 return self._monitor( 362 callback_function, 363 input_callback_function, 364 stop_callback_function, 365 stop_event, 366 stop_on_exit, 367 strip_timestamps, 368 accept_input, 369 _wait_if_stopped, 370 loop=None, 371 stop_flag=None, 372 )
Monitor the job's log files and execute a callback on new lines.
Parameters
- callback_function (Callable[[str], None], default partial(print, end='')):
The callback to execute as new data comes in.
Defaults to printing the output directly to
stdout. - input_callback_function (Optional[Callable[[], str]], default None):
If provided, execute this callback when the daemon is blocking on stdin.
Defaults to
sys.stdin.readline(). - stop_callback_function (Optional[Callable[[SuccessTuple]], str], default None): If provided, execute this callback when the daemon stops. The job's SuccessTuple will be passed to the callback.
- stop_event (Optional[asyncio.Event], default None):
If provided, stop monitoring when this event is set.
You may instead raise
meerschaum.jobs.StopMonitoringLogsfrom withincallback_functionto stop monitoring. - stop_on_exit (bool, default False):
If
True, stop monitoring when the job stops. - strip_timestamps (bool, default False):
If
True, remove leading timestamps from lines. - accept_input (bool, default True):
If
True, accept input when the daemon blocks on stdin.
374 async def monitor_logs_async( 375 self, 376 callback_function: Callable[[str], None] = _default_stdout_callback, 377 input_callback_function: Optional[Callable[[], str]] = None, 378 stop_callback_function: Optional[Callable[[SuccessTuple], None]] = None, 379 stop_event: Optional[asyncio.Event] = None, 380 stop_on_exit: bool = False, 381 strip_timestamps: bool = False, 382 accept_input: bool = True, 383 debug: bool = False, 384 _wait_if_stopped: bool = True, 385 ): 386 """ 387 Monitor the job's log files and await a callback on new lines. 388 389 Parameters 390 ---------- 391 callback_function: Callable[[str], None], default _default_stdout_callback 392 The callback to execute as new data comes in. 393 Defaults to printing the output directly to `stdout`. 394 395 input_callback_function: Optional[Callable[[], str]], default None 396 If provided, execute this callback when the daemon is blocking on stdin. 397 Defaults to `sys.stdin.readline()`. 398 399 stop_callback_function: Optional[Callable[[SuccessTuple]], str], default None 400 If provided, execute this callback when the daemon stops. 401 The job's SuccessTuple will be passed to the callback. 402 403 stop_event: Optional[asyncio.Event], default None 404 If provided, stop monitoring when this event is set. 405 You may instead raise `meerschaum.jobs.StopMonitoringLogs` 406 from within `callback_function` to stop monitoring. 407 408 stop_on_exit: bool, default False 409 If `True`, stop monitoring when the job stops. 410 411 strip_timestamps: bool, default False 412 If `True`, remove leading timestamps from lines. 413 414 accept_input: bool, default True 415 If `True`, accept input when the daemon blocks on stdin. 416 """ 417 loop = asyncio.get_running_loop() 418 stop_flag = _native.core().StopFlag() 419 run = functools.partial( 420 self._monitor, 421 callback_function, 422 input_callback_function, 423 stop_callback_function, 424 stop_event, 425 stop_on_exit, 426 strip_timestamps, 427 accept_input, 428 _wait_if_stopped, 429 loop=loop, 430 stop_flag=stop_flag, 431 ) 432 try: 433 await loop.run_in_executor(None, run) 434 except asyncio.CancelledError: 435 stop_flag.set() 436 raise 437 finally: 438 stop_flag.set()
Monitor the job's log files and await a callback on new lines.
Parameters
- callback_function (Callable[[str], None], default _default_stdout_callback):
The callback to execute as new data comes in.
Defaults to printing the output directly to
stdout. - input_callback_function (Optional[Callable[[], str]], default None):
If provided, execute this callback when the daemon is blocking on stdin.
Defaults to
sys.stdin.readline(). - stop_callback_function (Optional[Callable[[SuccessTuple]], str], default None): If provided, execute this callback when the daemon stops. The job's SuccessTuple will be passed to the callback.
- stop_event (Optional[asyncio.Event], default None):
If provided, stop monitoring when this event is set.
You may instead raise
meerschaum.jobs.StopMonitoringLogsfrom withincallback_functionto stop monitoring. - stop_on_exit (bool, default False):
If
True, stop monitoring when the job stops. - strip_timestamps (bool, default False):
If
True, remove leading timestamps from lines. - accept_input (bool, default True):
If
True, accept input when the daemon blocks on stdin.
493 def is_blocking_on_stdin(self, debug: bool = False) -> bool: 494 """ 495 Return whether a job's daemon is blocking on stdin. 496 """ 497 return self._core.is_blocking_on_stdin()
Return whether a job's daemon is blocking on stdin.
499 def get_prompt_kwargs(self, debug: bool = False) -> Dict[str, Any]: 500 """ 501 Return the kwargs to the blocking `prompt()`, if available. 502 """ 503 return self._core.get_prompt_kwargs()
Return the kwargs to the blocking prompt(), if available.
505 def write_stdin(self, data): 506 """ 507 Write to a job's daemon's `stdin`. 508 """ 509 self._core.write_stdin(data)
Write to a job's daemon's stdin.
511 @property 512 def executor(self) -> Union[Executor, None]: 513 """ 514 If the job is not local, return the connector to its executor 515 (the remote API instance, `systemd`, a plugin's executor). 516 """ 517 if self.executor_keys == 'local': 518 return None 519 from meerschaum.jobs._native import is_python_executor_type 520 if is_python_executor_type(self.executor_keys.split(':', maxsplit=1)[0]): 521 return mrsm.get_connector(self.executor_keys) 522 ### An executor type of a native (Rust) plugin: no Python connector exists. 523 from meerschaum.jobs._NativeExecutor import NativeExecutor 524 return NativeExecutor(self.executor_keys)
If the job is not local, return the connector to its executor
(the remote API instance, systemd, a plugin's executor).
526 @property 527 def status(self) -> str: 528 """ 529 Return the running status of the job's daemon. 530 """ 531 if '_status_hook' in self.__dict__: 532 return self._status_hook() 533 534 return self._core.status
Return the running status of the job's daemon.
536 @property 537 def pid(self) -> Union[int, None]: 538 """ 539 Return the PID of the job's dameon. 540 """ 541 return self._core.pid
Return the PID of the job's dameon.
543 @property 544 def restart(self) -> bool: 545 """ 546 Return whether to restart a stopped job. 547 """ 548 return self._core.restarts
Return whether to restart a stopped job.
550 @property 551 def result(self) -> SuccessTuple: 552 """ 553 Return the `SuccessTuple` when the job has terminated. 554 """ 555 if '_result_hook' in self.__dict__: 556 if self.is_running(): 557 return True, f"{self} is running." 558 return self._result_hook() 559 560 return self._core.result
Return the SuccessTuple when the job has terminated.
562 @property 563 def sysargs(self) -> List[str]: 564 """ 565 Return the sysargs to use for the Daemon. 566 """ 567 return self._core.sysargs
Return the sysargs to use for the Daemon.
569 @property 570 def health(self) -> Dict[str, Any]: 571 """ 572 Return how the job's laps go (a job that repeats its work: `--loop`, `-s`, `--listen`). 573 574 Returns 575 ------- 576 A dictionary with the keys: 577 578 - `state`: `'healthy'` (the last lap succeeded), `'failing'` (the last lap failed), 579 `'stalled'` (no lap for three times the expected interval, and at least a minute; 580 an hour without a sync for `--listen`; at least an hour for a first lap), 581 `'stopped'` (not running, or paused) or `'unknown'` (nothing recorded yet, e.g. 582 `'first sync in progress'`). 583 - `last_success`, `last_lap`: ISO-8601 UTC timestamps (or `None`). 584 - `consecutive_failures`: laps that failed in a row. 585 - `expected_interval_seconds`: how often laps are expected (`--min-seconds`, the 586 schedule's period, an hour for `--listen`), or `None`. 587 - `next_run`: while a running job waits for its next lap (`--min-seconds`, a cooldown 588 after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); 589 otherwise `None`. 590 - `message`: what to show next to the state, e.g. `'last good sync 2 min ago'`. 591 592 Examples 593 -------- 594 >>> job = Job('weather', 'sync pipes -c plugin:noaa --loop') 595 >>> success, msg = job.start() 596 >>> job.health['state'] # Before its first lap ends. 597 'unknown' 598 """ 599 return self._core.health()
Return how the job's laps go (a job that repeats its work: --loop, -s, --listen).
Returns
- A dictionary with the keys:
- -
state('healthy'(the last lap succeeded),'failing'(the last lap failed),):'stalled'(no lap for three times the expected interval, and at least a minute; an hour without a sync for--listen; at least an hour for a first lap),'stopped'(not running, or paused) or'unknown'(nothing recorded yet, e.g.'first sync in progress'). -
last_success,last_lap(ISO-8601 UTC timestamps (orNone).):-
consecutive_failures(laps that failed in a row.):-
expected_interval_seconds(how often laps are expected (--min-seconds, the): schedule's period, an hour for--listen), orNone.- -
next_run(while a running job waits for its next lap (--min-seconds, a cooldown): after a failure, a schedule's next fire), when that lap is due (ISO-8601 UTC); otherwiseNone. - -
message(what to show next to the state, e.g.'last good sync 2 min ago'.):
Examples
>>> job = Job('weather', 'sync pipes -c plugin:noaa --loop')
>>> success, msg = job.start()
>>> job.health['state'] # Before its first lap ends.
'unknown'
601 @property 602 def owner(self) -> Union[str, None]: 603 """ 604 Return the user who started this job through the API server or the web console 605 (`None` for a job created locally: the CLI, Compose, systemd or Python). A job started 606 by another job has that job's owner. For an `api:` job, the server's record. 607 608 Examples 609 -------- 610 >>> Job('nightly', executor_keys='api:main').owner 611 'alice' 612 """ 613 return self._core.access().get('owner', None)
Return the user who started this job through the API server or the web console
(None for a job created locally: the CLI, Compose, systemd or Python). A job started
by another job has that job's owner. For an api: job, the server's record.
Examples
>>> Job('nightly', executor_keys='api:main').owner
'alice'
615 @property 616 def team(self) -> Union[str, None]: 617 """ 618 Return the team this job belongs to: the primary team of the user who started it through 619 the API server or the web console (a job started by another job: that job's team). 620 `None` means Everyone (every local job). 621 622 Members of the team (and administrators) see the job and, with the job permissions, 623 control it. Renaming the team renames it here; deleting the team moves the job to 624 Everyone. 625 """ 626 return self._core.access().get('team', None)
Return the team this job belongs to: the primary team of the user who started it through
the API server or the web console (a job started by another job: that job's team).
None means Everyone (every local job).
Members of the team (and administrators) see the job and, with the job permissions, control it. Renaming the team renames it here; deleting the team moves the job to Everyone.
628 def get_daemon_properties(self) -> Dict[str, Any]: 629 """ 630 Return the `properties` dictionary for the job's daemon. 631 """ 632 return self._core.get_daemon_properties()
Return the properties dictionary for the job's daemon.
634 @property 635 def daemon(self) -> 'Daemon': 636 """ 637 Return the daemon which this job manages. 638 """ 639 from meerschaum.utils.daemon import Daemon 640 if self.executor_keys == 'local' and self.exists(): 641 return Daemon(daemon_id=self.name) 642 643 return Daemon( 644 daemon_id=self.name, 645 sysargs=self.sysargs, 646 label=shlex.join(self.sysargs), 647 properties=self.get_daemon_properties(), 648 )
Return the daemon which this job manages.
650 @property 651 def began(self) -> Union[datetime, None]: 652 """ 653 The datetime when the job began running. 654 """ 655 return _parse_dt(self._core.began)
The datetime when the job began running.
657 @property 658 def ended(self) -> Union[datetime, None]: 659 """ 660 The datetime when the job stopped running. 661 """ 662 return _parse_dt(self._core.ended)
The datetime when the job stopped running.
664 @property 665 def paused(self) -> Union[datetime, None]: 666 """ 667 The datetime when the job was suspended while running. 668 """ 669 return _parse_dt(self._core.paused)
The datetime when the job was suspended while running.
671 @property 672 def stop_time(self) -> Union[datetime, None]: 673 """ 674 Return the timestamp when the job was manually stopped. 675 """ 676 return _parse_dt(self._core.stop_time)
Return the timestamp when the job was manually stopped.
685 def check_restart(self) -> SuccessTuple: 686 """ 687 If `restart` is `True` and the daemon is not running, 688 restart the job. 689 Do not restart if the job was manually stopped. 690 """ 691 result = self._core.check_restart() 692 self._warn() 693 return result
If restart is True and the daemon is not running,
restart the job.
Do not restart if the job was manually stopped.
695 @property 696 def label(self) -> str: 697 """ 698 Return the job's Daemon label (joined sysargs). 699 """ 700 return self._core.label
Return the job's Daemon label (joined sysargs).
723 @property 724 def env(self) -> Dict[str, str]: 725 """ 726 Return the environment variables to set for the job's process. 727 """ 728 return self._core.env
Return the environment variables to set for the job's process.
730 @property 731 def delete_after_completion(self) -> bool: 732 """ 733 Return whether this job is configured to delete itself after completion. 734 """ 735 return self._core.delete_after_completion
Return whether this job is configured to delete itself after completion.
50class StopMonitoringLogs(Exception): 51 """ 52 Raise this exception to stop the logs monitoring. 53 """
Raise this exception to stop the logs monitoring.
125def get_jobs( 126 executor_keys: Optional[str] = None, 127 include_hidden: bool = False, 128 combine_local_and_systemd: bool = True, 129 debug: bool = False, 130) -> Dict[str, Job]: 131 """ 132 Return a dictionary of the existing jobs. 133 134 Parameters 135 ---------- 136 executor_keys: Optional[str], default None 137 If provided, return remote jobs on the given API instance. 138 Otherwise return local jobs. 139 140 include_hidden: bool, default False 141 If `True`, include jobs with the `hidden` attribute. 142 143 Returns 144 ------- 145 A dictionary mapping job names to jobs. 146 """ 147 from meerschaum.utils.warnings import warn 148 native = _native.core() 149 try: 150 rows, warnings = native.jobs_get_jobs( 151 executor_keys, 152 include_hidden, 153 combine_local_and_systemd, 154 api_attributes=_native.api_attributes(executor_keys), 155 ) 156 except RuntimeError as e: 157 ### An unreachable or unauthorized executor is not "no jobs". 158 warn(f"Could not get the jobs of executor '{executor_keys}': {e}", stack=False) 159 return {} 160 161 _native.emit_warnings(warnings) 162 return _rows_to_jobs(rows)
Return a dictionary of the existing jobs.
Parameters
- executor_keys (Optional[str], default None): If provided, return remote jobs on the given API instance. Otherwise return local jobs.
- include_hidden (bool, default False):
If
True, include jobs with thehiddenattribute.
Returns
- A dictionary mapping job names to jobs.
165def get_filtered_jobs( 166 executor_keys: Optional[str] = None, 167 filter_list: Optional[List[str]] = None, 168 include_hidden: bool = False, 169 combine_local_and_systemd: bool = True, 170 warn: bool = False, 171 debug: bool = False, 172) -> Dict[str, Job]: 173 """ 174 Return a list of jobs filtered by the user. 175 """ 176 from meerschaum.utils.warnings import warn as _warn 177 native = _native.core() 178 try: 179 rows, listing_warnings, missing = native.jobs_get_filtered_jobs( 180 executor_keys, 181 filter_list or [], 182 include_hidden, 183 combine_local_and_systemd, 184 api_attributes=_native.api_attributes(executor_keys), 185 ) 186 except RuntimeError as e: 187 _warn(f"Could not get the jobs of executor '{executor_keys}': {e}", stack=False) 188 return {} 189 190 _native.emit_warnings(listing_warnings) 191 if warn: 192 _native.emit_warnings(missing) 193 return _rows_to_jobs(rows)
Return a list of jobs filtered by the user.
196def get_restart_jobs( 197 executor_keys: Optional[str] = None, 198 jobs: Optional[Dict[str, Job]] = None, 199 include_hidden: bool = False, 200 combine_local_and_systemd: bool = True, 201 debug: bool = False, 202) -> Dict[str, Job]: 203 """ 204 Return jobs which were created with `--restart` or `--loop`. 205 """ 206 if jobs is None: 207 jobs = get_jobs( 208 executor_keys, 209 include_hidden=include_hidden, 210 combine_local_and_systemd=combine_local_and_systemd, 211 debug=debug, 212 ) 213 214 return { 215 name: job 216 for name, job in jobs.items() 217 if job.restart 218 }
Return jobs which were created with --restart or --loop.
221def get_running_jobs( 222 executor_keys: Optional[str] = None, 223 jobs: Optional[Dict[str, Job]] = None, 224 include_hidden: bool = False, 225 combine_local_and_systemd: bool = True, 226 debug: bool = False, 227) -> Dict[str, Job]: 228 """ 229 Return a dictionary of running jobs. 230 """ 231 if jobs is None: 232 jobs = get_jobs( 233 executor_keys, 234 include_hidden=include_hidden, 235 combine_local_and_systemd=combine_local_and_systemd, 236 debug=debug, 237 ) 238 239 return { 240 name: job 241 for name, job in jobs.items() 242 if job.status == 'running' 243 }
Return a dictionary of running jobs.
271def get_stopped_jobs( 272 executor_keys: Optional[str] = None, 273 jobs: Optional[Dict[str, Job]] = None, 274 include_hidden: bool = False, 275 combine_local_and_systemd: bool = True, 276 debug: bool = False, 277) -> Dict[str, Job]: 278 """ 279 Return a dictionary of stopped jobs. 280 """ 281 if jobs is None: 282 jobs = get_jobs( 283 executor_keys, 284 include_hidden=include_hidden, 285 combine_local_and_systemd=combine_local_and_systemd, 286 debug=debug, 287 ) 288 289 return { 290 name: job 291 for name, job in jobs.items() 292 if job.status == 'stopped' 293 }
Return a dictionary of stopped jobs.
246def get_paused_jobs( 247 executor_keys: Optional[str] = None, 248 jobs: Optional[Dict[str, Job]] = None, 249 include_hidden: bool = False, 250 combine_local_and_systemd: bool = True, 251 debug: bool = False, 252) -> Dict[str, Job]: 253 """ 254 Return a dictionary of paused jobs. 255 """ 256 if jobs is None: 257 jobs = get_jobs( 258 executor_keys, 259 include_hidden=include_hidden, 260 combine_local_and_systemd=combine_local_and_systemd, 261 debug=debug, 262 ) 263 264 return { 265 name: job 266 for name, job in jobs.items() 267 if job.status == 'paused' 268 }
Return a dictionary of paused jobs.
427def get_executor_keys_from_context() -> str: 428 """ 429 Return the configured executor (`meerschaum:executor`). 430 Otherwise, on a host with `systemd`, return `'systemd'` (for any root directory) unless 431 Meerschaum runs from a source checkout or a development build. 432 Otherwise return `'local'`. 433 """ 434 return _native.core().jobs_executor_keys_from_context()
Return the configured executor (meerschaum:executor).
Otherwise, on a host with systemd, return 'systemd' (for any root directory) unless
Meerschaum runs from a source checkout or a development build.
Otherwise return 'local'.
296def make_executor(cls): 297 """ 298 Register a class as an `Executor`. 299 """ 300 import re 301 from meerschaum.connectors import make_connector 302 suffix_regex = r'executor$' 303 typ = re.sub(suffix_regex, '', cls.__name__.lower()) 304 if typ not in executor_types: 305 executor_types.append(typ) 306 return make_connector(cls, _is_executor=True)
Register a class as an Executor.
22class Executor(Connector): 23 """ 24 Define the methods for managing jobs. 25 """ 26 27 @abstractmethod 28 def get_job_names(self, debug: bool = False) -> List[str]: 29 """ 30 Return a list of existing jobs, including hidden ones. 31 """ 32 33 @abstractmethod 34 def get_job_exists(self, name: str, debug: bool = False) -> bool: 35 """ 36 Return whether a job exists. 37 """ 38 39 @abstractmethod 40 def get_jobs(self, debug: bool = False) -> Dict[str, Job]: 41 """ 42 Return a dictionary of existing jobs. 43 """ 44 45 @abstractmethod 46 def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]: 47 """ 48 Return a job's metadata. 49 """ 50 51 @abstractmethod 52 def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]: 53 """ 54 Return the underlying daemon's properties. 55 """ 56 @abstractmethod 57 def get_job_status(self, name: str, debug: bool = False) -> str: 58 """ 59 Return the job's status. 60 """ 61 62 @abstractmethod 63 def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]: 64 """ 65 Return when a job began running. 66 """ 67 68 @abstractmethod 69 def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]: 70 """ 71 Return when a job stopped running. 72 """ 73 74 @abstractmethod 75 def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]: 76 """ 77 Return a job's `paused` timestamp, if it exists. 78 """ 79 80 @abstractmethod 81 def create_job( 82 self, 83 name: str, 84 sysargs: List[str], 85 properties: Optional[Dict[str, Any]] = None, 86 debug: bool = False, 87 ) -> SuccessTuple: 88 """ 89 Create a new job. 90 """ 91 92 @abstractmethod 93 def start_job(self, name: str, debug: bool = False) -> SuccessTuple: 94 """ 95 Start a job. 96 """ 97 98 @abstractmethod 99 def stop_job(self, name: str, debug: bool = False) -> SuccessTuple: 100 """ 101 Stop a job. 102 """ 103 104 @abstractmethod 105 def pause_job(self, name: str, debug: bool = False) -> SuccessTuple: 106 """ 107 Pause a job. 108 """ 109 110 @abstractmethod 111 def delete_job(self, name: str, debug: bool = False) -> SuccessTuple: 112 """ 113 Delete a job. 114 """ 115 116 @abstractmethod 117 def get_logs(self, name: str, debug: bool = False) -> str: 118 """ 119 Return a job's log output. 120 """ 121 122 @abstractmethod 123 def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]: 124 """ 125 Return the job's manual stop time. 126 """ 127 128 @abstractmethod 129 async def monitor_logs_async( 130 self, 131 name: str, 132 callback_function: Callable[[Any], Any], 133 input_callback_function: Callable[[], str], 134 stop_callback_function: Callable[[SuccessTuple], str], 135 stop_on_exit: bool = False, 136 strip_timestamps: bool = False, 137 accept_input: bool = True, 138 debug: bool = False, 139 ): 140 """ 141 Monitor a job's log files and await a callback with the changes. 142 """ 143 144 @abstractmethod 145 def monitor_logs(self, *args, **kwargs): 146 """ 147 Monitor a job's log files. 148 """ 149 150 @abstractmethod 151 def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool: 152 """ 153 Return whether a job is blocking on stdin. 154 """ 155 156 @abstractmethod 157 def get_job_prompt_kwargs(self, name: str, debug: bool = False) -> Dict[str, Any]: 158 """ 159 Return the kwargs to the blocking prompt. 160 """
Define the methods for managing jobs.
27 @abstractmethod 28 def get_job_names(self, debug: bool = False) -> List[str]: 29 """ 30 Return a list of existing jobs, including hidden ones. 31 """
Return a list of existing jobs, including hidden ones.
33 @abstractmethod 34 def get_job_exists(self, name: str, debug: bool = False) -> bool: 35 """ 36 Return whether a job exists. 37 """
Return whether a job exists.
39 @abstractmethod 40 def get_jobs(self, debug: bool = False) -> Dict[str, Job]: 41 """ 42 Return a dictionary of existing jobs. 43 """
Return a dictionary of existing jobs.
45 @abstractmethod 46 def get_job_metadata(self, name: str, debug: bool = False) -> Dict[str, Any]: 47 """ 48 Return a job's metadata. 49 """
Return a job's metadata.
51 @abstractmethod 52 def get_job_properties(self, name: str, debug: bool = False) -> Dict[str, Any]: 53 """ 54 Return the underlying daemon's properties. 55 """
Return the underlying daemon's properties.
56 @abstractmethod 57 def get_job_status(self, name: str, debug: bool = False) -> str: 58 """ 59 Return the job's status. 60 """
Return the job's status.
62 @abstractmethod 63 def get_job_began(self, name: str, debug: bool = False) -> Union[str, None]: 64 """ 65 Return when a job began running. 66 """
Return when a job began running.
68 @abstractmethod 69 def get_job_ended(self, name: str, debug: bool = False) -> Union[str, None]: 70 """ 71 Return when a job stopped running. 72 """
Return when a job stopped running.
74 @abstractmethod 75 def get_job_paused(self, name: str, debug: bool = False) -> Union[str, None]: 76 """ 77 Return a job's `paused` timestamp, if it exists. 78 """
Return a job's paused timestamp, if it exists.
80 @abstractmethod 81 def create_job( 82 self, 83 name: str, 84 sysargs: List[str], 85 properties: Optional[Dict[str, Any]] = None, 86 debug: bool = False, 87 ) -> SuccessTuple: 88 """ 89 Create a new job. 90 """
Create a new job.
92 @abstractmethod 93 def start_job(self, name: str, debug: bool = False) -> SuccessTuple: 94 """ 95 Start a job. 96 """
Start a job.
98 @abstractmethod 99 def stop_job(self, name: str, debug: bool = False) -> SuccessTuple: 100 """ 101 Stop a job. 102 """
Stop a job.
104 @abstractmethod 105 def pause_job(self, name: str, debug: bool = False) -> SuccessTuple: 106 """ 107 Pause a job. 108 """
Pause a job.
110 @abstractmethod 111 def delete_job(self, name: str, debug: bool = False) -> SuccessTuple: 112 """ 113 Delete a job. 114 """
Delete a job.
116 @abstractmethod 117 def get_logs(self, name: str, debug: bool = False) -> str: 118 """ 119 Return a job's log output. 120 """
Return a job's log output.
122 @abstractmethod 123 def get_job_stop_time(self, name: str, debug: bool = False) -> Union[datetime, None]: 124 """ 125 Return the job's manual stop time. 126 """
Return the job's manual stop time.
128 @abstractmethod 129 async def monitor_logs_async( 130 self, 131 name: str, 132 callback_function: Callable[[Any], Any], 133 input_callback_function: Callable[[], str], 134 stop_callback_function: Callable[[SuccessTuple], str], 135 stop_on_exit: bool = False, 136 strip_timestamps: bool = False, 137 accept_input: bool = True, 138 debug: bool = False, 139 ): 140 """ 141 Monitor a job's log files and await a callback with the changes. 142 """
Monitor a job's log files and await a callback with the changes.
144 @abstractmethod 145 def monitor_logs(self, *args, **kwargs): 146 """ 147 Monitor a job's log files. 148 """
Monitor a job's log files.
150 @abstractmethod 151 def get_job_is_blocking_on_stdin(self, name: str, debug: bool = False) -> bool: 152 """ 153 Return whether a job is blocking on stdin. 154 """
Return whether a job is blocking on stdin.
156 @abstractmethod 157 def get_job_prompt_kwargs(self, name: str, debug: bool = False) -> Dict[str, Any]: 158 """ 159 Return the kwargs to the blocking prompt. 160 """
Return the kwargs to the blocking prompt.
309def check_restart_jobs( 310 executor_keys: Optional[str] = 'local', 311 jobs: Optional[Dict[str, Job]] = None, 312 include_hidden: bool = True, 313 silent: bool = False, 314 debug: bool = False, 315) -> SuccessTuple: 316 """ 317 Restart any stopped jobs which were created with `--restart`. 318 319 Parameters 320 ---------- 321 executor_keys: Optional[str], default None 322 If provided, check jobs on the given remote API instance. 323 Otherwise check local jobs. 324 325 include_hidden: bool, default True 326 If `True`, include hidden jobs in the check. 327 328 silent: bool, default False 329 If `True`, do not print the restart success message. 330 """ 331 from meerschaum.utils.misc import items_str 332 333 if jobs is None: 334 jobs = get_jobs( 335 executor_keys, 336 include_hidden=include_hidden, 337 combine_local_and_systemd=False, 338 debug=debug, 339 ) 340 341 if not jobs: 342 return True, "No jobs to restart." 343 344 results = {} 345 for name, job in jobs.items(): 346 check_success, check_msg = job.check_restart() 347 results[job.name] = (check_success, check_msg) 348 if not silent: 349 mrsm.pprint((check_success, check_msg)) 350 351 success_names = [name for name, (check_success, check_msg) in results.items() if check_success] 352 fail_names = [name for name, (check_success, check_msg) in results.items() if not check_success] 353 success = len(success_names) == len(jobs) 354 msg = ( 355 ( 356 "Successfully restarted job" 357 + ('s' if len(success_names) != 1 else '') 358 + ' ' + items_str(success_names) + '.' 359 ) 360 if success 361 else ( 362 "Failed to restart job" 363 + ('s' if len(success_names) != 1 else '') 364 + ' ' + items_str(fail_names) + '.' 365 ) 366 ) 367 return success, msg
Restart any stopped jobs which were created with --restart.
Parameters
- executor_keys (Optional[str], default None): If provided, check jobs on the given remote API instance. Otherwise check local jobs.
- include_hidden (bool, default True):
If
True, include hidden jobs in the check. - silent (bool, default False):
If
True, do not print the restart success message.
383def start_check_jobs_thread(): 384 """ 385 Start a thread to regularly monitor jobs. 386 """ 387 import atexit 388 from functools import partial 389 from meerschaum.utils.threading import RepeatTimer 390 from meerschaum._internal.static import STATIC_CONFIG 391 392 global _check_loop_stop_thread 393 sleep_seconds = STATIC_CONFIG['jobs']['check_restart_seconds'] 394 395 _check_loop_stop_thread = RepeatTimer( 396 sleep_seconds, 397 partial( 398 _check_restart_jobs_against_lock, 399 silent=True, 400 ) 401 ) 402 _check_loop_stop_thread.daemon = True 403 atexit.register(stop_check_jobs_thread) 404 405 _check_loop_stop_thread.start() 406 return _check_loop_stop_thread
Start a thread to regularly monitor jobs.
409def stop_check_jobs_thread(): 410 """ 411 Stop the job monitoring thread. 412 """ 413 import meerschaum.config.paths as paths 414 from meerschaum.utils.warnings import warn 415 if _check_loop_stop_thread is None: 416 return 417 418 _check_loop_stop_thread.cancel() 419 420 try: 421 if paths.CHECK_JOBS_LOCK_PATH.exists(): 422 paths.CHECK_JOBS_LOCK_PATH.unlink() 423 except Exception as e: 424 warn(f"Failed to remove check jobs lock file:\n{e}")
Stop the job monitoring thread.
175def filter_visible_jobs( 176 principal_or_user: Any, 177 jobs: Union[Dict[str, Any], Iterable[Any]], 178 instance: Any = None, 179) -> Union[Dict[str, Any], List[Any]]: 180 """ 181 Return the jobs a person may see. 182 183 Administrators see every job and owners their own. A job started through the API server or 184 the console belongs to its owner's primary team, and its team's members see it; a local job 185 (the CLI, Compose, systemd) is Everyone's, and every user sees it. A console run is private: 186 its owner, the managers of its team and administrators. 187 188 Parameters 189 ---------- 190 principal_or_user: Any 191 Who is asking: a `meerschaum.core.access.Principal`, the console's viewer, a dictionary 192 (`username`, `is_admin`, `teams`), a `meerschaum.core.User` or a username (read from 193 `instance`), or `None` (anonymous: only Everyone's jobs). 194 195 jobs: Union[Dict[str, Job], Iterable[Job]] 196 The jobs to filter, e.g. `meerschaum.jobs.get_jobs()`. 197 198 instance: Any, default None 199 The instance a `User` or a username is read from (default: the user's own, else the 200 configured instance). A user who cannot be read is no administrator and in no team. 201 202 Returns 203 ------- 204 The visible jobs, in the same shape as `jobs` (a dictionary or a list). 205 206 Examples 207 -------- 208 >>> from meerschaum.jobs import get_jobs, filter_visible_jobs 209 >>> visible = filter_visible_jobs(mrsm.User('bob'), get_jobs()) 210 """ 211 parts = _viewer_parts(principal_or_user, instance=instance) 212 if isinstance(jobs, dict): 213 return {name: job for name, job in jobs.items() if _can_see(parts, job)} 214 return [job for job in jobs if _can_see(parts, job)]
Return the jobs a person may see.
Administrators see every job and owners their own. A job started through the API server or the console belongs to its owner's primary team, and its team's members see it; a local job (the CLI, Compose, systemd) is Everyone's, and every user sees it. A console run is private: its owner, the managers of its team and administrators.
Parameters
- principal_or_user (Any):
Who is asking: a
meerschaum.core.access.Principal, the console's viewer, a dictionary (username,is_admin,teams), ameerschaum.core.Useror a username (read frominstance), orNone(anonymous: only Everyone's jobs). - jobs (Union[Dict[str, Job], Iterable[Job]]):
The jobs to filter, e.g.
meerschaum.jobs.get_jobs(). - instance (Any, default None):
The instance a
Useror a username is read from (default: the user's own, else the configured instance). A user who cannot be read is no administrator and in no team.
Returns
- The visible jobs, in the same shape as
jobs(a dictionary or a list).
Examples
>>> from meerschaum.jobs import get_jobs, filter_visible_jobs
>>> visible = filter_visible_jobs(mrsm.User('bob'), get_jobs())
168def can_see_job(principal_or_user: Any, job: Any, instance: Any = None) -> bool: 169 """ 170 Return whether `principal_or_user` may see `job` (see `filter_visible_jobs`). 171 """ 172 return _can_see(_viewer_parts(principal_or_user, instance=instance), job)
Return whether principal_or_user may see job (see filter_visible_jobs).
140def job_access(job: Any) -> Dict[str, Any]: 141 """ 142 Return who a job belongs to: `{'owner', 'team', 'private'}`. 143 144 `owner` is the user who started it through the API server or the console (`None` for a local 145 job), `team` its team (`None`: Everyone) and `private` whether it is a console run (visible to 146 its owner, its team's managers and administrators only). A `systemd` or `api:` job reports 147 what its executor recorded. 148 """ 149 core = getattr(job, '_core', None) 150 if core is not None and hasattr(core, 'access'): 151 return core.access() 152 return _native.core().jobs_access_of(str(getattr(job, 'name', '')), _job_properties(job))
Return who a job belongs to: {'owner', 'team', 'private'}.
owner is the user who started it through the API server or the console (None for a local
job), team its team (None: Everyone) and private whether it is a console run (visible to
its owner, its team's managers and administrators only). A systemd or api: job reports
what its executor recorded.
217def ownership_properties(owner: Optional[str] = None, team: Optional[str] = None) -> Dict[str, Any]: 218 """ 219 Return the properties that record who started a job, to pass as `Job(..., _properties=...)`. 220 221 The API server and the console record the caller (`owner`) and their primary team (`team`; 222 `None` or `'Everyone'`: Everyone) on the jobs they start. 223 224 Examples 225 -------- 226 >>> ownership_properties('alice', 'Data') 227 {'owner': 'alice', 'team': 'Data', 'mrsm_owner': 'alice'} 228 """ 229 return _native.core().jobs_ownership_properties(owner, team)
Return the properties that record who started a job, to pass as Job(..., _properties=...).
The API server and the console record the caller (owner) and their primary team (team;
None or 'Everyone': Everyone) on the jobs they start.
Examples
>>> ownership_properties('alice', 'Data')
{'owner': 'alice', 'team': 'Data', 'mrsm_owner': 'alice'}
232def get_team_jobs(team: str) -> List[str]: 233 """ 234 Return the names of this root's jobs (local and `systemd`) whose team is `team` (names compare 235 without case; `'Everyone'`: the jobs without a team), e.g. for a deletion's confirmation. 236 """ 237 return list(_native.core().jobs_team_jobs(str(team)))
Return the names of this root's jobs (local and systemd) whose team is team (names compare
without case; 'Everyone': the jobs without a team), e.g. for a deletion's confirmation.
240def move_team_jobs(team: str, to: Optional[str] = None) -> SuccessTuple: 241 """ 242 Move this root's jobs of `team` to the team `to` (a rename), or to Everyone (`to=None`, as 243 deleting the team does). 244 245 Renaming or deleting a team through `mrsm.Team`, the CLI, the API server or the console calls 246 this for the root where it runs. 247 248 Returns 249 ------- 250 A `SuccessTuple` naming how many jobs moved (and which could not be updated). 251 """ 252 moved, failed = _native.core().jobs_retag_team(str(team), to) 253 target = f"team '{to}'" if to and str(to).strip().lower() != EVERYONE.lower() else EVERYONE 254 msg = f"Moved {len(moved)} job{'' if len(moved) == 1 else 's'} to {target}." 255 if failed: 256 msg += " Could not update " + ', '.join(f"'{name}' ({why})" for name, why in failed) + '.' 257 return not failed, msg
Move this root's jobs of team to the team to (a rename), or to Everyone (to=None, as
deleting the team does).
Renaming or deleting a team through mrsm.Team, the CLI, the API server or the console calls
this for the root where it runs.
Returns
- A
SuccessTuplenaming how many jobs moved (and which could not be updated).
58def report_lap( 59 success: bool = True, 60 message: str = '', 61 next_in: Optional[float] = None, 62) -> SuccessTuple: 63 """ 64 Record a lap of a plugin action that repeats its work by itself (its function takes `loop`, 65 so `--loop` is left to it): the job's health then shows how its laps go, and the Overview's 66 Next runs when the next one is due. 67 68 Call it once at the end of each iteration. Outside a job (a foreground run, a script) it does 69 nothing. An action without a `loop` parameter does not need it: with `--loop` or `-s`, the 70 host runs it in laps and records them itself. 71 72 Parameters 73 ---------- 74 success: bool, default True 75 Whether this lap succeeded (a failed lap counts toward `consecutive_failures`). 76 77 message: str, default '' 78 What the lap did, shown with the job's health. 79 80 next_in: Optional[float], default None 81 Seconds until the next lap begins (the action's own `min_seconds`). It sets the cadence 82 the job is expected at (stalled after three times it) and `job.health['next_run']`. 83 84 Returns 85 ------- 86 A `SuccessTuple`: whether the lap was recorded. 87 88 Examples 89 -------- 90 >>> import time 91 >>> import meerschaum as mrsm 92 >>> def heartbeat_ack(loop: bool = False, min_seconds: float = 20, **kwargs): 93 ... while True: 94 ... success, msg = ack_once() 95 ... mrsm.jobs.report_lap(success, msg, next_in=min_seconds) 96 ... if not loop: 97 ... return success, msg 98 ... time.sleep(min_seconds) 99 """ 100 reporter = _lap_reporter 101 if reporter is None: 102 return True, "Not running in a job's plugin action: the lap was not recorded." 103 lap = {'success': bool(success), 'message': str(message or '')} 104 if next_in is not None: 105 lap['next_in'] = float(next_in) 106 value = reporter(lap) 107 if isinstance(value, (list, tuple)) and len(value) == 2: 108 return bool(value[0]), str(value[1]) 109 return False, f"Unexpected answer from the host: {value!r}"
Record a lap of a plugin action that repeats its work by itself (its function takes loop,
so --loop is left to it): the job's health then shows how its laps go, and the Overview's
Next runs when the next one is due.
Call it once at the end of each iteration. Outside a job (a foreground run, a script) it does
nothing. An action without a loop parameter does not need it: with --loop or -s, the
host runs it in laps and records them itself.
Parameters
- success (bool, default True):
Whether this lap succeeded (a failed lap counts toward
consecutive_failures). - message (str, default ''): What the lap did, shown with the job's health.
- next_in (Optional[float], default None):
Seconds until the next lap begins (the action's own
min_seconds). It sets the cadence the job is expected at (stalled after three times it) andjob.health['next_run'].
Returns
- A
SuccessTuple(whether the lap was recorded.):
Examples
>>> import time
>>> import meerschaum as mrsm
>>> def heartbeat_ack(loop: bool = False, min_seconds: float = 20, **kwargs):
... while True:
... success, msg = ack_once()
... mrsm.jobs.report_lap(success, msg, next_in=min_seconds)
... if not loop:
... return success, msg
... time.sleep(min_seconds)