meerschaum.utils.daemon.Daemon
Manage a job's daemon: its directory, its process and its logs.
Daemon is a read and control façade over the native daemon (crate meerschaum-jobs, bound as
meerschaum._core.Daemon). A daemon runs a Meerschaum command (Daemon(sysargs=[...]), what
Job manages); running an arbitrary Python callable as a daemon (Daemon(target=...)) is not
supported in Meerschaum v5.
1#! /usr/bin/env python3 2# -*- coding: utf-8 -*- 3# vim:fenc=utf-8 4 5""" 6Manage a job's daemon: its directory, its process and its logs. 7 8`Daemon` is a read and control façade over the native daemon (crate `meerschaum-jobs`, bound as 9`meerschaum._core.Daemon`). A daemon runs a Meerschaum command (`Daemon(sysargs=[...])`, what 10`Job` manages); running an arbitrary Python callable as a daemon (`Daemon(target=...)`) is not 11supported in Meerschaum v5. 12""" 13 14from __future__ import annotations 15 16import pathlib 17import shlex 18 19from meerschaum.utils.typing import Any, Callable, Dict, List, Optional, SuccessTuple, Union 20 21_REMOVED = ( 22 "Running a Python callable as a daemon is not supported in Meerschaum v5. " 23 "Use `meerschaum.jobs.Job(name, sysargs)` to run a Meerschaum command in the background " 24 "(see the migration guide, 'Removed in 5.0')." 25) 26 27#: `Daemon.run()` results of this process, by daemon ID (kept for v4's `_results`). 28_results: Dict[str, SuccessTuple] = {} 29 30 31class Daemon: 32 """ 33 Daemonize Python functions into background processes (v5: Meerschaum commands). 34 """ 35 36 def __init__( 37 self, 38 target: Optional[Callable[[Any], Any]] = None, 39 target_args: Optional[List[Any]] = None, 40 target_kw: Optional[Dict[str, Any]] = None, 41 env: Optional[Dict[str, str]] = None, 42 daemon_id: Optional[str] = None, 43 label: Optional[str] = None, 44 properties: Optional[Dict[str, Any]] = None, 45 pickle: bool = True, 46 *, 47 sysargs: Optional[List[str]] = None, 48 ): 49 """ 50 Open an existing daemon (`Daemon(daemon_id='name')`) or describe a new one 51 (`Daemon(daemon_id='name', sysargs=[...])`). 52 53 Parameters 54 ---------- 55 target: Optional[Callable[[Any], Any]], default None 56 Only `meerschaum._internal.entry.entry` (a Meerschaum command, with the sysargs as 57 its first argument) is supported. 58 59 target_args: Optional[List[Any]], default None 60 The arguments of `target`: `[sysargs]`. 61 62 env: Optional[Dict[str, str]], default None 63 Environment variables to set in the daemon's process. 64 65 daemon_id: Optional[str], default None 66 The name of the daemon (and of the job). 67 68 label: Optional[str], default None 69 The label to show (the joined sysargs). 70 71 properties: Optional[Dict[str, Any]], default None 72 Patch the daemon's properties. 73 """ 74 from meerschaum.jobs import _native 75 core = _native.core() 76 if target is not None: 77 if getattr(target, '__name__', None) != 'entry': 78 raise NotImplementedError(_REMOVED) 79 sysargs = list((target_args or [[]])[0] or []) 80 81 if target_kw: 82 raise NotImplementedError(_REMOVED) 83 84 if daemon_id is None: 85 if sysargs is None: 86 raise ValueError("Pass a `daemon_id` to open an existing daemon.") 87 daemon_id = core.jobs_new_daemon_name() 88 89 if env: 90 properties = {**(properties or {}), 'env': dict(env)} 91 92 if sysargs is not None and label is None: 93 label = shlex.join(sysargs) 94 95 self.daemon_id = daemon_id 96 self._core = core.Daemon(daemon_id, sysargs, label, properties) 97 98 @classmethod 99 def from_properties_file(cls, daemon_id: str) -> 'Daemon': 100 """Open the daemon `daemon_id` from its `properties.json`.""" 101 return cls(daemon_id=daemon_id) 102 103 @property 104 def label(self) -> str: 105 return self._core.label 106 107 @property 108 def target(self) -> None: 109 return None 110 111 @property 112 def target_args(self) -> List[List[str]]: 113 """The arguments of the daemon's target: `[sysargs]`.""" 114 return [self._core.sysargs] 115 116 @property 117 def target_kw(self) -> Dict[str, Any]: 118 return {} 119 120 @property 121 def hidden(self) -> bool: 122 return self._core.hidden 123 124 @property 125 def status(self) -> str: 126 """Return `'running'`, `'paused'` or `'stopped'`.""" 127 return self._core.status 128 129 @property 130 def pid(self) -> Union[int, None]: 131 """Return the PID of the daemon's process, if it has one.""" 132 return self._core.pid 133 134 @property 135 def process(self) -> Union[int, None]: 136 """Return the PID if the process exists (v4 returned a `psutil.Process`).""" 137 return self._core.process 138 139 @property 140 def properties(self) -> Dict[str, Any]: 141 """Return the daemon's properties (`properties.json` patched with this object's).""" 142 return self._core.properties() 143 144 @properties.setter 145 def properties(self, patch: Dict[str, Any]) -> None: 146 self._core.update_properties(patch) 147 148 def read_properties(self) -> Optional[Dict[str, Any]]: 149 """Read `properties.json`.""" 150 return self._core.read_properties() 151 152 def write_properties(self) -> SuccessTuple: 153 """Write `properties.json`.""" 154 return self._core.write_properties() 155 156 def _paths(self) -> Dict[str, str]: 157 return self._core.paths() 158 159 @property 160 def path(self) -> pathlib.Path: 161 """The daemon's directory (`jobs/<daemon_id>`).""" 162 return pathlib.Path(self._paths()['dir']) 163 164 @property 165 def properties_path(self) -> pathlib.Path: 166 return pathlib.Path(self._paths()['properties']) 167 168 @property 169 def pid_path(self) -> pathlib.Path: 170 return pathlib.Path(self._paths()['pid']) 171 172 @property 173 def pickle_path(self) -> pathlib.Path: 174 return pathlib.Path(self._paths()['pickle']) 175 176 @property 177 def stdin_file_path(self) -> pathlib.Path: 178 return pathlib.Path(self._paths()['stdin']) 179 180 @property 181 def blocking_stdin_file_path(self) -> pathlib.Path: 182 return pathlib.Path(self._paths()['stdin_block']) 183 184 @property 185 def prompt_kwargs_file_path(self) -> pathlib.Path: 186 return pathlib.Path(self._paths()['prompt_kwargs']) 187 188 @property 189 def stop_path(self) -> pathlib.Path: 190 return pathlib.Path(self._paths()['stop']) 191 192 @property 193 def log_path(self) -> pathlib.Path: 194 return pathlib.Path(self._paths()['log']) 195 196 @property 197 def log_offset_path(self) -> pathlib.Path: 198 return pathlib.Path(self._paths()['log_offset']) 199 200 @property 201 def stdin_file(self): 202 """A `StdinFile` for the daemon's stdin FIFO.""" 203 from meerschaum.utils.daemon.StdinFile import StdinFile 204 return StdinFile(self.stdin_file_path, self.blocking_stdin_file_path) 205 206 @property 207 def rotating_log(self): 208 """A `RotatingFile` over the daemon's logs.""" 209 from meerschaum.utils.daemon.RotatingFile import RotatingFile 210 logs = self.properties.get('logs', {}) or {} 211 return RotatingFile( 212 self.log_path, 213 num_files_to_keep=logs.get('num_files_to_keep'), 214 max_file_size=logs.get('max_file_size'), 215 write_timestamps=bool(logs.get('write_timestamps', False)), 216 timestamp_format=logs.get('timestamp_format'), 217 ) 218 219 @property 220 def log_text(self) -> Optional[str]: 221 """The text of the daemon's logs.""" 222 return self._core.log_text() 223 224 def readlines(self) -> List[str]: 225 """The log lines since the last call (the cursor is persisted).""" 226 return self._core.readlines() 227 228 def _read_stop_file(self) -> Optional[Dict[str, Any]]: 229 return self._core.read_stop_file() 230 231 def mkdir_if_not_exists(self, allow_dirty_run: bool = False) -> SuccessTuple: 232 """Create the daemon's directory.""" 233 return self._core.mkdir_if_not_exists(allow_dirty_run) 234 235 def run( 236 self, 237 keep_daemon_output: bool = True, 238 allow_dirty_run: bool = False, 239 wait: bool = False, 240 timeout: Union[int, float] = 4, 241 debug: bool = False, 242 ) -> SuccessTuple: 243 """ 244 Run the daemon's command in the background. 245 246 Parameters 247 ---------- 248 keep_daemon_output: bool, default True 249 If `False`, delete the daemon's directory and logs when it finishes. 250 251 allow_dirty_run: bool, default False 252 If `True`, run although the daemon's directory exists. 253 254 wait: bool, default False 255 If `True`, wait up to `timeout` seconds until the daemon's process is running. 256 257 timeout: Union[int, float], default 4 258 How long to wait when `wait` is `True`. 259 """ 260 result = self._core.run(keep_daemon_output, allow_dirty_run) 261 if wait and result[0]: 262 import time 263 deadline = time.monotonic() + timeout 264 while time.monotonic() < deadline and self.status == 'stopped': 265 time.sleep(0.05) 266 if self.status == 'stopped' and 'result' not in (self.read_properties() or {}): 267 result = False, f"Daemon '{self.daemon_id}' did not start within {timeout} seconds." 268 _results[self.daemon_id] = result 269 return result 270 271 def kill( 272 self, 273 timeout: Union[int, float, None] = 8, 274 debug: bool = False, 275 ) -> SuccessTuple: 276 """Forcibly terminate the daemon.""" 277 return self._core.kill(timeout) 278 279 def quit( 280 self, 281 timeout: Union[int, float, None] = None, 282 debug: bool = False, 283 ) -> SuccessTuple: 284 """Gracefully quit the daemon (`SIGINT`).""" 285 return self._core.quit(timeout) 286 287 def pause( 288 self, 289 timeout: Union[int, float, None] = None, 290 check_timeout_interval: Union[float, int, None] = None, 291 debug: bool = False, 292 ) -> SuccessTuple: 293 """Suspend the daemon's process.""" 294 return self._core.pause(timeout, check_timeout_interval) 295 296 def resume( 297 self, 298 timeout: Union[int, float, None] = None, 299 check_timeout_interval: Union[float, int, None] = None, 300 debug: bool = False, 301 ) -> SuccessTuple: 302 """Resume the daemon's process.""" 303 return self._core.resume(timeout, check_timeout_interval) 304 305 def cleanup(self, keep_logs: bool = False, debug: bool = False) -> SuccessTuple: 306 """Remove the daemon's directory (and its logs unless `keep_logs`).""" 307 return self._core.cleanup(keep_logs) 308 309 def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]: 310 """Return `timeout`, or the configured `jobs:timeout_seconds`.""" 311 return self._core.get_timeout_seconds(timeout) 312 313 def get_check_timeout_interval_seconds( 314 self, 315 check_timeout_interval: Union[int, float, None] = None, 316 ) -> Union[int, float]: 317 """Return the interval, or the configured `jobs:check_timeout_interval_seconds`.""" 318 return self._core.get_check_timeout_interval_seconds(check_timeout_interval) 319 320 def __eq__(self, other) -> bool: 321 return isinstance(other, Daemon) and other.daemon_id == self.daemon_id 322 323 def __hash__(self) -> int: 324 return hash(self.daemon_id) 325 326 def __str__(self) -> str: 327 return self.daemon_id 328 329 def __repr__(self) -> str: 330 return str(self)
32class Daemon: 33 """ 34 Daemonize Python functions into background processes (v5: Meerschaum commands). 35 """ 36 37 def __init__( 38 self, 39 target: Optional[Callable[[Any], Any]] = None, 40 target_args: Optional[List[Any]] = None, 41 target_kw: Optional[Dict[str, Any]] = None, 42 env: Optional[Dict[str, str]] = None, 43 daemon_id: Optional[str] = None, 44 label: Optional[str] = None, 45 properties: Optional[Dict[str, Any]] = None, 46 pickle: bool = True, 47 *, 48 sysargs: Optional[List[str]] = None, 49 ): 50 """ 51 Open an existing daemon (`Daemon(daemon_id='name')`) or describe a new one 52 (`Daemon(daemon_id='name', sysargs=[...])`). 53 54 Parameters 55 ---------- 56 target: Optional[Callable[[Any], Any]], default None 57 Only `meerschaum._internal.entry.entry` (a Meerschaum command, with the sysargs as 58 its first argument) is supported. 59 60 target_args: Optional[List[Any]], default None 61 The arguments of `target`: `[sysargs]`. 62 63 env: Optional[Dict[str, str]], default None 64 Environment variables to set in the daemon's process. 65 66 daemon_id: Optional[str], default None 67 The name of the daemon (and of the job). 68 69 label: Optional[str], default None 70 The label to show (the joined sysargs). 71 72 properties: Optional[Dict[str, Any]], default None 73 Patch the daemon's properties. 74 """ 75 from meerschaum.jobs import _native 76 core = _native.core() 77 if target is not None: 78 if getattr(target, '__name__', None) != 'entry': 79 raise NotImplementedError(_REMOVED) 80 sysargs = list((target_args or [[]])[0] or []) 81 82 if target_kw: 83 raise NotImplementedError(_REMOVED) 84 85 if daemon_id is None: 86 if sysargs is None: 87 raise ValueError("Pass a `daemon_id` to open an existing daemon.") 88 daemon_id = core.jobs_new_daemon_name() 89 90 if env: 91 properties = {**(properties or {}), 'env': dict(env)} 92 93 if sysargs is not None and label is None: 94 label = shlex.join(sysargs) 95 96 self.daemon_id = daemon_id 97 self._core = core.Daemon(daemon_id, sysargs, label, properties) 98 99 @classmethod 100 def from_properties_file(cls, daemon_id: str) -> 'Daemon': 101 """Open the daemon `daemon_id` from its `properties.json`.""" 102 return cls(daemon_id=daemon_id) 103 104 @property 105 def label(self) -> str: 106 return self._core.label 107 108 @property 109 def target(self) -> None: 110 return None 111 112 @property 113 def target_args(self) -> List[List[str]]: 114 """The arguments of the daemon's target: `[sysargs]`.""" 115 return [self._core.sysargs] 116 117 @property 118 def target_kw(self) -> Dict[str, Any]: 119 return {} 120 121 @property 122 def hidden(self) -> bool: 123 return self._core.hidden 124 125 @property 126 def status(self) -> str: 127 """Return `'running'`, `'paused'` or `'stopped'`.""" 128 return self._core.status 129 130 @property 131 def pid(self) -> Union[int, None]: 132 """Return the PID of the daemon's process, if it has one.""" 133 return self._core.pid 134 135 @property 136 def process(self) -> Union[int, None]: 137 """Return the PID if the process exists (v4 returned a `psutil.Process`).""" 138 return self._core.process 139 140 @property 141 def properties(self) -> Dict[str, Any]: 142 """Return the daemon's properties (`properties.json` patched with this object's).""" 143 return self._core.properties() 144 145 @properties.setter 146 def properties(self, patch: Dict[str, Any]) -> None: 147 self._core.update_properties(patch) 148 149 def read_properties(self) -> Optional[Dict[str, Any]]: 150 """Read `properties.json`.""" 151 return self._core.read_properties() 152 153 def write_properties(self) -> SuccessTuple: 154 """Write `properties.json`.""" 155 return self._core.write_properties() 156 157 def _paths(self) -> Dict[str, str]: 158 return self._core.paths() 159 160 @property 161 def path(self) -> pathlib.Path: 162 """The daemon's directory (`jobs/<daemon_id>`).""" 163 return pathlib.Path(self._paths()['dir']) 164 165 @property 166 def properties_path(self) -> pathlib.Path: 167 return pathlib.Path(self._paths()['properties']) 168 169 @property 170 def pid_path(self) -> pathlib.Path: 171 return pathlib.Path(self._paths()['pid']) 172 173 @property 174 def pickle_path(self) -> pathlib.Path: 175 return pathlib.Path(self._paths()['pickle']) 176 177 @property 178 def stdin_file_path(self) -> pathlib.Path: 179 return pathlib.Path(self._paths()['stdin']) 180 181 @property 182 def blocking_stdin_file_path(self) -> pathlib.Path: 183 return pathlib.Path(self._paths()['stdin_block']) 184 185 @property 186 def prompt_kwargs_file_path(self) -> pathlib.Path: 187 return pathlib.Path(self._paths()['prompt_kwargs']) 188 189 @property 190 def stop_path(self) -> pathlib.Path: 191 return pathlib.Path(self._paths()['stop']) 192 193 @property 194 def log_path(self) -> pathlib.Path: 195 return pathlib.Path(self._paths()['log']) 196 197 @property 198 def log_offset_path(self) -> pathlib.Path: 199 return pathlib.Path(self._paths()['log_offset']) 200 201 @property 202 def stdin_file(self): 203 """A `StdinFile` for the daemon's stdin FIFO.""" 204 from meerschaum.utils.daemon.StdinFile import StdinFile 205 return StdinFile(self.stdin_file_path, self.blocking_stdin_file_path) 206 207 @property 208 def rotating_log(self): 209 """A `RotatingFile` over the daemon's logs.""" 210 from meerschaum.utils.daemon.RotatingFile import RotatingFile 211 logs = self.properties.get('logs', {}) or {} 212 return RotatingFile( 213 self.log_path, 214 num_files_to_keep=logs.get('num_files_to_keep'), 215 max_file_size=logs.get('max_file_size'), 216 write_timestamps=bool(logs.get('write_timestamps', False)), 217 timestamp_format=logs.get('timestamp_format'), 218 ) 219 220 @property 221 def log_text(self) -> Optional[str]: 222 """The text of the daemon's logs.""" 223 return self._core.log_text() 224 225 def readlines(self) -> List[str]: 226 """The log lines since the last call (the cursor is persisted).""" 227 return self._core.readlines() 228 229 def _read_stop_file(self) -> Optional[Dict[str, Any]]: 230 return self._core.read_stop_file() 231 232 def mkdir_if_not_exists(self, allow_dirty_run: bool = False) -> SuccessTuple: 233 """Create the daemon's directory.""" 234 return self._core.mkdir_if_not_exists(allow_dirty_run) 235 236 def run( 237 self, 238 keep_daemon_output: bool = True, 239 allow_dirty_run: bool = False, 240 wait: bool = False, 241 timeout: Union[int, float] = 4, 242 debug: bool = False, 243 ) -> SuccessTuple: 244 """ 245 Run the daemon's command in the background. 246 247 Parameters 248 ---------- 249 keep_daemon_output: bool, default True 250 If `False`, delete the daemon's directory and logs when it finishes. 251 252 allow_dirty_run: bool, default False 253 If `True`, run although the daemon's directory exists. 254 255 wait: bool, default False 256 If `True`, wait up to `timeout` seconds until the daemon's process is running. 257 258 timeout: Union[int, float], default 4 259 How long to wait when `wait` is `True`. 260 """ 261 result = self._core.run(keep_daemon_output, allow_dirty_run) 262 if wait and result[0]: 263 import time 264 deadline = time.monotonic() + timeout 265 while time.monotonic() < deadline and self.status == 'stopped': 266 time.sleep(0.05) 267 if self.status == 'stopped' and 'result' not in (self.read_properties() or {}): 268 result = False, f"Daemon '{self.daemon_id}' did not start within {timeout} seconds." 269 _results[self.daemon_id] = result 270 return result 271 272 def kill( 273 self, 274 timeout: Union[int, float, None] = 8, 275 debug: bool = False, 276 ) -> SuccessTuple: 277 """Forcibly terminate the daemon.""" 278 return self._core.kill(timeout) 279 280 def quit( 281 self, 282 timeout: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """Gracefully quit the daemon (`SIGINT`).""" 286 return self._core.quit(timeout) 287 288 def pause( 289 self, 290 timeout: Union[int, float, None] = None, 291 check_timeout_interval: Union[float, int, None] = None, 292 debug: bool = False, 293 ) -> SuccessTuple: 294 """Suspend the daemon's process.""" 295 return self._core.pause(timeout, check_timeout_interval) 296 297 def resume( 298 self, 299 timeout: Union[int, float, None] = None, 300 check_timeout_interval: Union[float, int, None] = None, 301 debug: bool = False, 302 ) -> SuccessTuple: 303 """Resume the daemon's process.""" 304 return self._core.resume(timeout, check_timeout_interval) 305 306 def cleanup(self, keep_logs: bool = False, debug: bool = False) -> SuccessTuple: 307 """Remove the daemon's directory (and its logs unless `keep_logs`).""" 308 return self._core.cleanup(keep_logs) 309 310 def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]: 311 """Return `timeout`, or the configured `jobs:timeout_seconds`.""" 312 return self._core.get_timeout_seconds(timeout) 313 314 def get_check_timeout_interval_seconds( 315 self, 316 check_timeout_interval: Union[int, float, None] = None, 317 ) -> Union[int, float]: 318 """Return the interval, or the configured `jobs:check_timeout_interval_seconds`.""" 319 return self._core.get_check_timeout_interval_seconds(check_timeout_interval) 320 321 def __eq__(self, other) -> bool: 322 return isinstance(other, Daemon) and other.daemon_id == self.daemon_id 323 324 def __hash__(self) -> int: 325 return hash(self.daemon_id) 326 327 def __str__(self) -> str: 328 return self.daemon_id 329 330 def __repr__(self) -> str: 331 return str(self)
Daemonize Python functions into background processes (v5: Meerschaum commands).
37 def __init__( 38 self, 39 target: Optional[Callable[[Any], Any]] = None, 40 target_args: Optional[List[Any]] = None, 41 target_kw: Optional[Dict[str, Any]] = None, 42 env: Optional[Dict[str, str]] = None, 43 daemon_id: Optional[str] = None, 44 label: Optional[str] = None, 45 properties: Optional[Dict[str, Any]] = None, 46 pickle: bool = True, 47 *, 48 sysargs: Optional[List[str]] = None, 49 ): 50 """ 51 Open an existing daemon (`Daemon(daemon_id='name')`) or describe a new one 52 (`Daemon(daemon_id='name', sysargs=[...])`). 53 54 Parameters 55 ---------- 56 target: Optional[Callable[[Any], Any]], default None 57 Only `meerschaum._internal.entry.entry` (a Meerschaum command, with the sysargs as 58 its first argument) is supported. 59 60 target_args: Optional[List[Any]], default None 61 The arguments of `target`: `[sysargs]`. 62 63 env: Optional[Dict[str, str]], default None 64 Environment variables to set in the daemon's process. 65 66 daemon_id: Optional[str], default None 67 The name of the daemon (and of the job). 68 69 label: Optional[str], default None 70 The label to show (the joined sysargs). 71 72 properties: Optional[Dict[str, Any]], default None 73 Patch the daemon's properties. 74 """ 75 from meerschaum.jobs import _native 76 core = _native.core() 77 if target is not None: 78 if getattr(target, '__name__', None) != 'entry': 79 raise NotImplementedError(_REMOVED) 80 sysargs = list((target_args or [[]])[0] or []) 81 82 if target_kw: 83 raise NotImplementedError(_REMOVED) 84 85 if daemon_id is None: 86 if sysargs is None: 87 raise ValueError("Pass a `daemon_id` to open an existing daemon.") 88 daemon_id = core.jobs_new_daemon_name() 89 90 if env: 91 properties = {**(properties or {}), 'env': dict(env)} 92 93 if sysargs is not None and label is None: 94 label = shlex.join(sysargs) 95 96 self.daemon_id = daemon_id 97 self._core = core.Daemon(daemon_id, sysargs, label, properties)
Open an existing daemon (Daemon(daemon_id='name')) or describe a new one
(Daemon(daemon_id='name', sysargs=[...])).
Parameters
- target (Optional[Callable[[Any], Any]], default None):
Only
meerschaum._internal.entry.entry(a Meerschaum command, with the sysargs as its first argument) is supported. - target_args (Optional[List[Any]], default None):
The arguments of
target:[sysargs]. - env (Optional[Dict[str, str]], default None): Environment variables to set in the daemon's process.
- daemon_id (Optional[str], default None): The name of the daemon (and of the job).
- label (Optional[str], default None): The label to show (the joined sysargs).
- properties (Optional[Dict[str, Any]], default None): Patch the daemon's properties.
99 @classmethod 100 def from_properties_file(cls, daemon_id: str) -> 'Daemon': 101 """Open the daemon `daemon_id` from its `properties.json`.""" 102 return cls(daemon_id=daemon_id)
Open the daemon daemon_id from its properties.json.
112 @property 113 def target_args(self) -> List[List[str]]: 114 """The arguments of the daemon's target: `[sysargs]`.""" 115 return [self._core.sysargs]
The arguments of the daemon's target: [sysargs].
125 @property 126 def status(self) -> str: 127 """Return `'running'`, `'paused'` or `'stopped'`.""" 128 return self._core.status
Return 'running', 'paused' or 'stopped'.
130 @property 131 def pid(self) -> Union[int, None]: 132 """Return the PID of the daemon's process, if it has one.""" 133 return self._core.pid
Return the PID of the daemon's process, if it has one.
135 @property 136 def process(self) -> Union[int, None]: 137 """Return the PID if the process exists (v4 returned a `psutil.Process`).""" 138 return self._core.process
Return the PID if the process exists (v4 returned a psutil.Process).
140 @property 141 def properties(self) -> Dict[str, Any]: 142 """Return the daemon's properties (`properties.json` patched with this object's).""" 143 return self._core.properties()
Return the daemon's properties (properties.json patched with this object's).
149 def read_properties(self) -> Optional[Dict[str, Any]]: 150 """Read `properties.json`.""" 151 return self._core.read_properties()
Read properties.json.
153 def write_properties(self) -> SuccessTuple: 154 """Write `properties.json`.""" 155 return self._core.write_properties()
Write properties.json.
160 @property 161 def path(self) -> pathlib.Path: 162 """The daemon's directory (`jobs/<daemon_id>`).""" 163 return pathlib.Path(self._paths()['dir'])
The daemon's directory (jobs/<daemon_id>).
201 @property 202 def stdin_file(self): 203 """A `StdinFile` for the daemon's stdin FIFO.""" 204 from meerschaum.utils.daemon.StdinFile import StdinFile 205 return StdinFile(self.stdin_file_path, self.blocking_stdin_file_path)
A StdinFile for the daemon's stdin FIFO.
207 @property 208 def rotating_log(self): 209 """A `RotatingFile` over the daemon's logs.""" 210 from meerschaum.utils.daemon.RotatingFile import RotatingFile 211 logs = self.properties.get('logs', {}) or {} 212 return RotatingFile( 213 self.log_path, 214 num_files_to_keep=logs.get('num_files_to_keep'), 215 max_file_size=logs.get('max_file_size'), 216 write_timestamps=bool(logs.get('write_timestamps', False)), 217 timestamp_format=logs.get('timestamp_format'), 218 )
A RotatingFile over the daemon's logs.
220 @property 221 def log_text(self) -> Optional[str]: 222 """The text of the daemon's logs.""" 223 return self._core.log_text()
The text of the daemon's logs.
225 def readlines(self) -> List[str]: 226 """The log lines since the last call (the cursor is persisted).""" 227 return self._core.readlines()
The log lines since the last call (the cursor is persisted).
232 def mkdir_if_not_exists(self, allow_dirty_run: bool = False) -> SuccessTuple: 233 """Create the daemon's directory.""" 234 return self._core.mkdir_if_not_exists(allow_dirty_run)
Create the daemon's directory.
236 def run( 237 self, 238 keep_daemon_output: bool = True, 239 allow_dirty_run: bool = False, 240 wait: bool = False, 241 timeout: Union[int, float] = 4, 242 debug: bool = False, 243 ) -> SuccessTuple: 244 """ 245 Run the daemon's command in the background. 246 247 Parameters 248 ---------- 249 keep_daemon_output: bool, default True 250 If `False`, delete the daemon's directory and logs when it finishes. 251 252 allow_dirty_run: bool, default False 253 If `True`, run although the daemon's directory exists. 254 255 wait: bool, default False 256 If `True`, wait up to `timeout` seconds until the daemon's process is running. 257 258 timeout: Union[int, float], default 4 259 How long to wait when `wait` is `True`. 260 """ 261 result = self._core.run(keep_daemon_output, allow_dirty_run) 262 if wait and result[0]: 263 import time 264 deadline = time.monotonic() + timeout 265 while time.monotonic() < deadline and self.status == 'stopped': 266 time.sleep(0.05) 267 if self.status == 'stopped' and 'result' not in (self.read_properties() or {}): 268 result = False, f"Daemon '{self.daemon_id}' did not start within {timeout} seconds." 269 _results[self.daemon_id] = result 270 return result
Run the daemon's command in the background.
Parameters
- keep_daemon_output (bool, default True):
If
False, delete the daemon's directory and logs when it finishes. - allow_dirty_run (bool, default False):
If
True, run although the daemon's directory exists. - wait (bool, default False):
If
True, wait up totimeoutseconds until the daemon's process is running. - timeout (Union[int, float], default 4):
How long to wait when
waitisTrue.
272 def kill( 273 self, 274 timeout: Union[int, float, None] = 8, 275 debug: bool = False, 276 ) -> SuccessTuple: 277 """Forcibly terminate the daemon.""" 278 return self._core.kill(timeout)
Forcibly terminate the daemon.
280 def quit( 281 self, 282 timeout: Union[int, float, None] = None, 283 debug: bool = False, 284 ) -> SuccessTuple: 285 """Gracefully quit the daemon (`SIGINT`).""" 286 return self._core.quit(timeout)
Gracefully quit the daemon (SIGINT).
288 def pause( 289 self, 290 timeout: Union[int, float, None] = None, 291 check_timeout_interval: Union[float, int, None] = None, 292 debug: bool = False, 293 ) -> SuccessTuple: 294 """Suspend the daemon's process.""" 295 return self._core.pause(timeout, check_timeout_interval)
Suspend the daemon's process.
297 def resume( 298 self, 299 timeout: Union[int, float, None] = None, 300 check_timeout_interval: Union[float, int, None] = None, 301 debug: bool = False, 302 ) -> SuccessTuple: 303 """Resume the daemon's process.""" 304 return self._core.resume(timeout, check_timeout_interval)
Resume the daemon's process.
306 def cleanup(self, keep_logs: bool = False, debug: bool = False) -> SuccessTuple: 307 """Remove the daemon's directory (and its logs unless `keep_logs`).""" 308 return self._core.cleanup(keep_logs)
Remove the daemon's directory (and its logs unless keep_logs).
310 def get_timeout_seconds(self, timeout: Union[int, float, None] = None) -> Union[int, float]: 311 """Return `timeout`, or the configured `jobs:timeout_seconds`.""" 312 return self._core.get_timeout_seconds(timeout)
Return timeout, or the configured jobs:timeout_seconds.
314 def get_check_timeout_interval_seconds( 315 self, 316 check_timeout_interval: Union[int, float, None] = None, 317 ) -> Union[int, float]: 318 """Return the interval, or the configured `jobs:check_timeout_interval_seconds`.""" 319 return self._core.get_check_timeout_interval_seconds(check_timeout_interval)
Return the interval, or the configured jobs:check_timeout_interval_seconds.