daemonizer-py 1.5.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
daemonizer/__init__.py ADDED
File without changes
@@ -0,0 +1 @@
1
+ """CLI sub-module"""
@@ -0,0 +1,287 @@
1
+ """CLI commands"""
2
+
3
+ import glob
4
+ import signal
5
+ from pathlib import Path
6
+ from typing import List, Tuple
7
+
8
+ import click
9
+
10
+ from daemonizer.cli.daemon_loader import find_daemon_classes, load_module_from_script
11
+ from daemonizer.cli.processor import _cli_parse_daemons, _stop_pid
12
+ from daemonizer.constants import APP_NAME, APP_VERSION
13
+ from daemonizer.core.daemons.flags import RESTART, START, STATUS, STOP
14
+ from daemonizer.files import PID_FILES_DIR
15
+ from daemonizer.utils.logs import get_logger
16
+ from daemonizer.utils.process import is_active_process
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ # Entry point
22
+ @click.group()
23
+ def cli() -> None:
24
+ """
25
+ CLI entry point
26
+ """
27
+ pass
28
+
29
+
30
+ # Command: $ daemonizer version
31
+ @cli.command()
32
+ def version() -> None:
33
+ """
34
+ Version info
35
+ """
36
+ click.echo(f"{APP_NAME} v{APP_VERSION}")
37
+
38
+
39
+ # Command: $ daemonizer start
40
+ @cli.command()
41
+ @click.argument(
42
+ "script",
43
+ type=click.Path(
44
+ exists=True,
45
+ file_okay=True,
46
+ dir_okay=False,
47
+ readable=True,
48
+ ),
49
+ required=True,
50
+ )
51
+ @click.argument("daemons", type=click.STRING, required=False, nargs=-1)
52
+ @click.option(
53
+ "--strict/--no-strict",
54
+ "-s",
55
+ type=click.BOOL,
56
+ is_flag=True,
57
+ required=False,
58
+ default=True,
59
+ )
60
+ def start(script: str, daemons: Tuple[str, ...], strict: bool) -> None:
61
+ """
62
+ Start daemons (CLI target)
63
+ """
64
+ _cli_parse_daemons(script, list(daemons), START, strict)
65
+
66
+
67
+ # Command: $ daemonizer stop
68
+ @cli.command()
69
+ @click.argument(
70
+ "script",
71
+ type=click.Path(
72
+ exists=True,
73
+ file_okay=True,
74
+ dir_okay=False,
75
+ readable=True,
76
+ ),
77
+ required=True,
78
+ )
79
+ @click.argument("daemons", type=click.STRING, required=False, nargs=-1)
80
+ @click.option(
81
+ "--strict/--no-strict",
82
+ "-s",
83
+ type=click.BOOL,
84
+ is_flag=True,
85
+ required=False,
86
+ default=True,
87
+ )
88
+ def stop(script: str, daemons: Tuple[str, ...], strict: bool) -> None:
89
+ """
90
+ Stop daemons (CLI target)
91
+ """
92
+ _cli_parse_daemons(script, list(daemons), STOP, strict)
93
+
94
+
95
+ # Command: $ daemonizer restart
96
+ @cli.command()
97
+ @click.argument(
98
+ "script",
99
+ type=click.Path(
100
+ exists=True,
101
+ file_okay=True,
102
+ dir_okay=False,
103
+ readable=True,
104
+ ),
105
+ required=True,
106
+ )
107
+ @click.argument("daemons", type=click.STRING, required=False, nargs=-1)
108
+ @click.option(
109
+ "--strict/--no-strict",
110
+ "-s",
111
+ type=click.BOOL,
112
+ is_flag=True,
113
+ required=False,
114
+ default=True,
115
+ )
116
+ def restart(script: str, daemons: Tuple[str, ...], strict: bool) -> None:
117
+ """
118
+ Restart daemons (CLI target)
119
+ """
120
+ _cli_parse_daemons(script, list(daemons), RESTART, strict)
121
+
122
+
123
+ # Command: $ daemonizer status
124
+ @cli.command()
125
+ @click.argument(
126
+ "script",
127
+ type=click.Path(
128
+ exists=True,
129
+ file_okay=True,
130
+ dir_okay=False,
131
+ readable=True,
132
+ ),
133
+ required=True,
134
+ )
135
+ @click.argument("daemons", type=click.STRING, required=False, nargs=-1)
136
+ @click.option(
137
+ "--strict/--no-strict",
138
+ "-s",
139
+ type=click.BOOL,
140
+ is_flag=True,
141
+ required=False,
142
+ default=True,
143
+ )
144
+ def status(script: str, daemons: Tuple[str, ...], strict: bool) -> None:
145
+ """
146
+ Restart daemons (CLI target)
147
+ """
148
+ _cli_parse_daemons(script, list(daemons), STATUS, strict)
149
+
150
+
151
+ # Command: $ daemonizer scan
152
+ @cli.command()
153
+ @click.argument(
154
+ "script",
155
+ type=click.Path(
156
+ exists=True,
157
+ file_okay=True,
158
+ dir_okay=False,
159
+ readable=True,
160
+ ),
161
+ required=True,
162
+ )
163
+ @click.option(
164
+ "--strict/--no-strict",
165
+ "-s",
166
+ type=click.BOOL,
167
+ is_flag=True,
168
+ required=False,
169
+ default=True,
170
+ )
171
+ def scan(script: str, strict: bool) -> None:
172
+ """
173
+ Scan daemons (CLI target)
174
+ """
175
+ click.echo(f"Scan | Script: {script} - Strict: {strict}")
176
+
177
+ module = load_module_from_script(script_path=script)
178
+
179
+ daemon_classes = find_daemon_classes(module=module, strict=strict)
180
+ click.echo(f"Found {len(daemon_classes)} daemon classes")
181
+ for i, daemon_class in enumerate(daemon_classes):
182
+ click.echo(
183
+ f"{i + 1} \t {daemon_class.__name__} - (module: {daemon_class.__module__})"
184
+ )
185
+
186
+
187
+ # Command: $ daemonizer stop-pid
188
+ @cli.command()
189
+ @click.argument("pids", type=click.INT, required=False, nargs=-1)
190
+ @click.option(
191
+ "--signal",
192
+ "-g",
193
+ type=click.INT,
194
+ is_flag=False,
195
+ required=False,
196
+ default=signal.SIGTERM,
197
+ )
198
+ def stop_pid(pids: Tuple[int, ...], signal: int) -> None:
199
+ """
200
+ Stop daemons via PID input.
201
+ This command is recommended if you already have the daemon's PID.
202
+ On signal reception, daemon will kill stop itself, clean the on-disk PID file and stop the process.
203
+ :param pids: PIDs
204
+ :type pids: Tuple[int, ...]
205
+ :param signal: Signal to be used
206
+ :type signal: int
207
+ """
208
+ click.echo("Stop pids: " + str(pids))
209
+
210
+ for pid in pids:
211
+ if pid <= 0:
212
+ click.echo("PID must be strictly greater than 0")
213
+ return None
214
+
215
+ # TODO: check on signal input
216
+
217
+ for pid in pids:
218
+ _stop_pid(pid=pid, sig=signal)
219
+ return None
220
+
221
+
222
+ # Command: $ daemonizer stop-name
223
+ @cli.command()
224
+ @click.argument("names", type=click.STRING, required=False, nargs=-1)
225
+ @click.option(
226
+ "--signal",
227
+ "-g",
228
+ type=click.INT,
229
+ is_flag=False,
230
+ required=False,
231
+ default=signal.SIGTERM,
232
+ )
233
+ def stop_name(names: Tuple[str, ...], signal: int) -> None:
234
+ """
235
+ Stop daemons via daemon's name input.
236
+ This command is recommended if you already have the daemon names.
237
+ """
238
+ click.echo("Stop daemon names: " + str(names))
239
+
240
+ for daemon_name in names:
241
+ pid: int = -1
242
+
243
+ # Cleaning daemon name
244
+ if daemon_name.endswith(".pid"):
245
+ daemon_name = daemon_name.replace(".pid", "")
246
+
247
+ # Getting daemon name
248
+ p = PID_FILES_DIR / Path(daemon_name + ".pid")
249
+
250
+ if p.exists():
251
+ with open(p.absolute(), "r") as f:
252
+ pid = int(f.readline().strip())
253
+ _stop_pid(pid=pid, sig=signal)
254
+ else:
255
+ click.echo(f"PID file for this daemon's name: {daemon_name} does not exist")
256
+
257
+
258
+ # Command: $ daemonizer ls
259
+ @cli.command()
260
+ def ls() -> None:
261
+ """
262
+ Listing all daemons currently found
263
+ """
264
+ pattern = PID_FILES_DIR / "*.pid"
265
+ found_daemons: List[str] = sorted(glob.glob(pattern.__str__()))
266
+
267
+ for i, daemon in enumerate(found_daemons):
268
+ pidfile: Path = Path(daemon).absolute()
269
+
270
+ daemon_name: str = pidfile.stem
271
+
272
+ with open(pidfile.absolute(), "r") as f:
273
+ pid = int(f.readline().strip())
274
+
275
+ is_active_daemon: bool = is_active_process(pid_=pid)
276
+ click.echo(
277
+ f"({i + 1}) {daemon_name} | PID := {pid} | Active?: {is_active_daemon}"
278
+ )
279
+
280
+
281
+ # Command: $ daemonizer pidfiles
282
+ @cli.command()
283
+ def pidfiles() -> None:
284
+ """
285
+ Get pid files folder. This can be used to `cd $(daemonizer pidfiles)`
286
+ """
287
+ click.echo(PID_FILES_DIR.__str__())
@@ -0,0 +1,148 @@
1
+ """Core logic to:
2
+ (1) scan external Python scripts requested by CLI users when interacting with daemons,
3
+ (2) collect all classes inheriting from Daemon or UNIXDaemon base classes
4
+ (3) Instantiate one object per class found to start these daemons when CLI users request it
5
+ """
6
+
7
+ import importlib.util
8
+ import inspect
9
+ from _frozen_importlib import ModuleSpec
10
+ from pathlib import Path
11
+ from types import ModuleType
12
+ from typing import Any, Dict, List, Type
13
+
14
+ from daemonizer.core.daemons.base import Daemon
15
+ from daemonizer.core.daemons.unix import UNIXDaemon
16
+ from daemonizer.utils.logs import get_logger
17
+
18
+ logger = get_logger(__name__)
19
+
20
+
21
+ def load_module_from_script(script_path: Path | str | None = None) -> ModuleType | None:
22
+ """
23
+ Function to load a specific module from a given input Python script.
24
+ This function is using **importlib** (docs: https://docs.python.org/3/library/importlib.html)
25
+ and **pathlib** (docs: https://docs.python.org/3/library/pathlib.html).
26
+ :param script_path: Input script path
27
+ :type script_path: Path | str | None
28
+ :return: Module object imported via importlib.util if successful, None otherwise
29
+ :rtype: ModuleType | None
30
+ """
31
+
32
+ if script_path is None:
33
+ return None
34
+
35
+ if isinstance(script_path, str):
36
+ script_path = Path(script_path)
37
+
38
+ # Making the path absolute, resolving all symlinks
39
+ path: Path = script_path.resolve()
40
+
41
+ # Checking extension (.py)
42
+ extension: str = "".join(path.suffixes)
43
+ if extension not in [".py", ".pyi"]:
44
+ logger.error(f"Script extension {extension} not supported")
45
+ return None
46
+
47
+ # Module name (final path component, without its suffix)
48
+ module_name = path.stem
49
+
50
+ logger.info(f"Loading module {module_name} from script {path.absolute()}")
51
+
52
+ # A factory function for creating a ModuleSpec instance based on the path to a file
53
+ # ModuleSpec := A specification for a module’s import-system-related state
54
+ spec: ModuleSpec | None = importlib.util.spec_from_file_location(module_name, path)
55
+ if spec is None:
56
+ logger.error("Module spec import failed (importlib)")
57
+ return None
58
+
59
+ # Create a new module based on spec
60
+ module: ModuleType | None = importlib.util.module_from_spec(spec)
61
+
62
+ if module is None:
63
+ logger.error("Module creation from spec failed (importlib)")
64
+ return None
65
+
66
+ # spec.loader (:= an object that loads a module)
67
+ if spec.loader is None:
68
+ logger.error("Module creation from spec failed (importlib)")
69
+ return None
70
+
71
+ # Loading module in current context
72
+ # (executes the module in its own namespace when a module is imported or reloaded)
73
+ spec.loader.exec_module(module=module)
74
+ return module
75
+
76
+
77
+ def find_daemon_classes(
78
+ module: ModuleType | None = None, strict: bool = True
79
+ ) -> List[Type]:
80
+ """
81
+ Function to scan input module and collect + return a list of specific daemon classes.
82
+ Module scan is performed using the **inspect** module (docs: https://docs.python.org/3/library/inspect.html)
83
+ :param module: Input module
84
+ :type module: ModuleType | None
85
+ :param strict: Return only (True) the classes from the script itself (not those imported to the script from other sub-modules/dependencies)
86
+ :type strict: bool
87
+ :return: List of daemon classes
88
+ :rtype: List[Type]
89
+ """
90
+
91
+ daemons: List[Type] = []
92
+ if module is None or not isinstance(module, ModuleType):
93
+ logger.error("Input module is invalid")
94
+ return daemons
95
+
96
+ # Checking all module members (docs: https://docs.python.org/3/library/inspect.html#inspect.getmembers)
97
+ for e, cls in inspect.getmembers(module, inspect.isclass):
98
+ # Skipping UNIXDaemon itself (not relevant)
99
+ if cls is UNIXDaemon or cls is Daemon:
100
+ continue
101
+
102
+ # TODO: Handle c-tor arguments (inspect.signature)
103
+ if issubclass(cls, UNIXDaemon): # mro check otherwise
104
+ if strict:
105
+ # Handling case where we only want daemons from the script itself (not from its dependencies)
106
+ if cls.__module__ != module.__name__:
107
+ continue
108
+ # print(f"Sig: {inspect.signature(cls).parameters["name"].annotation}")
109
+ daemons.append(cls)
110
+
111
+ return daemons
112
+
113
+
114
+ def get_daemon_instances(
115
+ daemons: List[Type] | None = None,
116
+ only_includes: Dict[str, str] | None = None,
117
+ script_path: Path | None = None,
118
+ ) -> List[Any]:
119
+ """
120
+ Function to get daemon instances from daemon classes
121
+ :param daemons: Input daemon classes
122
+ :type daemons: List[Type] | None
123
+ :param only_includes: Dict of daemon classes (and daemon names) to be included only if found in the module (by func fun: `find_daemon_classes`)
124
+ :type only_includes: List[str] | None
125
+ :param script_path: Input script path (used to get a default daemon name in case a given daemon class (from CLI input) does not have a respective daemon name
126
+ :type script_path: Path | None
127
+ :return: List of daemon objects (1 y input daemon class)
128
+ :rtype: List[Any]
129
+ """
130
+
131
+ daemon_instances: List[Any] = []
132
+ if daemons is None:
133
+ logger.error("Input daemon classes are invalid")
134
+ return daemon_instances
135
+
136
+ if script_path is None:
137
+ logger.error("Input script path is invalid")
138
+ return daemon_instances
139
+
140
+ for daemon in daemons:
141
+ daemon_name: str = script_path.stem + daemon.__name__ + "_daemon"
142
+ if only_includes:
143
+ if daemon.__name__ not in only_includes.keys():
144
+ continue
145
+ # TODO: Handle constructor with daemon name
146
+ daemon_name = only_includes.get(daemon.__name__, "")
147
+ daemon_instances.append(daemon(name=daemon_name))
148
+ return daemon_instances
@@ -0,0 +1,212 @@
1
+ """Submodule to initialize and manage the bridge between CLI inputs and operations on daemons"""
2
+
3
+ import os
4
+ import signal
5
+ from pathlib import Path
6
+ from typing import Callable, Dict, List
7
+
8
+ import click
9
+
10
+ from daemonizer.cli.daemon_loader import (
11
+ find_daemon_classes,
12
+ get_daemon_instances,
13
+ load_module_from_script,
14
+ )
15
+ from daemonizer.core.daemons.flags import (
16
+ DEFAULT_FLAG,
17
+ FLAGS,
18
+ RESTART,
19
+ START,
20
+ STATUS,
21
+ STOP,
22
+ )
23
+ from daemonizer.core.handlers.ctx_manager import DaemonHandler
24
+ from daemonizer.utils.logs import get_logger
25
+
26
+ logger = get_logger(__name__)
27
+
28
+
29
+ def _cli_parse_daemons(
30
+ script: str | Path | None = None,
31
+ exclusive_daemon_classes_names: List[str] | None = None,
32
+ flag_operation: int = DEFAULT_FLAG,
33
+ strict: bool = True,
34
+ ) -> None:
35
+ """
36
+ This function will parse the CLI request including:
37
+ - input script path
38
+ - specific daemons to run or everything found
39
+ - operation to apply on these daemons
40
+ :param script: Input script path
41
+ :type script: str | Path | None
42
+ :param exclusive_daemon_classes_names: Dict of specific daemon classes (and names for daemons to be named) to be run (against the logic scanned from the module). If nothing is specified, all daemons from the module must be considered.
43
+ :type exclusive_daemon_classes_names: List[str] | None
44
+ :param flag_operation: Flag of the operation to perform on considered daemons
45
+ :type flag_operation: int
46
+ :param strict: True if only daemons from current modules should be considered, False otherwise (all daemons including from dependencies)
47
+ :type strict: bool
48
+ :return: Nothing
49
+ :rtype: None
50
+ """
51
+
52
+ if script is None:
53
+ return None
54
+
55
+ if isinstance(script, str):
56
+ script = Path(script)
57
+
58
+ # Checking if we have clean daemons CLI input before proceeding
59
+ if not _checking_cli_input_daemons(exclusive_daemon_classes_names):
60
+ return None
61
+
62
+ if isinstance(exclusive_daemon_classes_names, tuple):
63
+ exclusive_daemon_classes_names = list(exclusive_daemon_classes_names)
64
+
65
+ assert isinstance(exclusive_daemon_classes_names, list), "Daemons must be a list"
66
+
67
+ exclusive_daemon_classes: List[str] = [
68
+ exclusive_daemon_classes_names[k]
69
+ for k in range(len(exclusive_daemon_classes_names))
70
+ if k % 2 == 0
71
+ ]
72
+ exclusive_daemon_names: List[str] = [
73
+ exclusive_daemon_classes_names[k]
74
+ for k in range(len(exclusive_daemon_classes_names))
75
+ if k % 2 == 1
76
+ ]
77
+ d_exclusive_daemon_classes_names: Dict[str, str] = dict(
78
+ zip(exclusive_daemon_classes, exclusive_daemon_names)
79
+ )
80
+
81
+ # Loading module
82
+ module = load_module_from_script(script_path=script)
83
+
84
+ # Finding daemon classes in module
85
+ found_daemon_classes = find_daemon_classes(module=module, strict=strict)
86
+ click.echo(f"Classes: {found_daemon_classes}")
87
+
88
+ # Getting daemon instances
89
+ daemon_instances = get_daemon_instances(
90
+ daemons=found_daemon_classes,
91
+ only_includes=d_exclusive_daemon_classes_names,
92
+ script_path=script,
93
+ )
94
+ click.echo(f"Instances: {daemon_instances}")
95
+
96
+ # Getting correct operation from input flag
97
+ func_name: str = _get_op_func_from_flag2(flag_operation=flag_operation)
98
+
99
+ # TODO: Adding context handler here instead of _get_op_func_from_flag
100
+ with DaemonHandler() as h:
101
+ for daemon_instance in daemon_instances:
102
+ getattr(h, func_name)(daemon_instance)
103
+ # func(h)()
104
+ # For each daemon instances, execute the given function
105
+ # for daemon_instance in daemon_instances:
106
+ # getattr(daemon_instance, func)()
107
+ # daemon_instance.func()
108
+ # func(
109
+ # daemon_instance
110
+ # ) # clean form as we have a lambda func whose unique parameter is the daemon instance itself
111
+ return None
112
+
113
+
114
+ def _get_op_func_from_flag(flag_operation: int) -> Callable:
115
+ """
116
+ (Not used as we are using the `DaemonHandler` context manager)
117
+ This function will return a function that will run the specified operation,
118
+ translated from the input flag operation. This is somehow a *mapping* function
119
+ :param flag_operation: Valid input flag operation
120
+ :type flag_operation: int
121
+ :return: Lambda function that will run the specified operation once called with a valid `Daemon` instance
122
+ :rtype: Callable
123
+ """
124
+ if flag_operation not in FLAGS:
125
+ return lambda _: None
126
+
127
+ if flag_operation == START:
128
+ return lambda x: x.start()
129
+ elif flag_operation == STOP:
130
+ return lambda x: x.stop()
131
+ elif flag_operation == STATUS:
132
+ return lambda x: x.status()
133
+ elif flag_operation == RESTART:
134
+ return lambda x: x.restart()
135
+ else:
136
+ return lambda x: None
137
+
138
+
139
+ def _get_op_func_from_flag2(flag_operation: int) -> str:
140
+ """
141
+ (Not used as we are using the `DaemonHandler` context manager)
142
+ This function will return a function that will run the specified operation,
143
+ translated from the input flag operation. This is somehow a *mapping* function
144
+ :param flag_operation: Valid input flag operation
145
+ :type flag_operation: int
146
+ :return: Function name that will run the specified operation once called with a valid `Daemon` instance
147
+ :rtype: str
148
+ """
149
+ if flag_operation not in FLAGS:
150
+ return ""
151
+
152
+ if flag_operation == START:
153
+ return "start"
154
+ elif flag_operation == STOP:
155
+ return "stop"
156
+ elif flag_operation == STATUS:
157
+ return "status"
158
+ elif flag_operation == RESTART:
159
+ return "restart"
160
+ else:
161
+ return ""
162
+
163
+
164
+ def _checking_cli_input_daemons(cli_input_daemons: List[str] | None = None) -> bool:
165
+ """
166
+ Function to check daemons from CLI input
167
+ Daemons to be considered for given
168
+ :param cli_input_daemons: Daemons classes + names from input CLI entry
169
+ :type cli_input_daemons: List[str] | None
170
+ :return: True if CLI entry is valid, False otherwise
171
+ :rtype: bool
172
+ """
173
+
174
+ if cli_input_daemons is None:
175
+ return False
176
+ if len(cli_input_daemons) != 0:
177
+ if len(cli_input_daemons) % 2 != 0:
178
+ click.echo(
179
+ "You must follow pattern: DaemonClass1, DaemonName1, DaemonClass2, DaemonName2, ..., DaemonClassN, DaemonNameN"
180
+ )
181
+ return False
182
+ else:
183
+ click.echo("You must add the list of daemon classes and names")
184
+ return False
185
+ return True
186
+
187
+
188
+ def _stop_pid(pid: int | None = None, sig: int = signal.SIGTERM) -> bool:
189
+ """
190
+ Function to stop a given process for the specified PID, by sending the input sig (POSIX signal)
191
+ :param pid: PID of the process to stop
192
+ :type pid: int | None
193
+ :param sig: Signal to send to daemon
194
+ :type sig: int
195
+ :return: True if process was stopped, False otherwise
196
+ :rtype: bool
197
+ """
198
+
199
+ if pid is None:
200
+ logger.error("PID must be provided")
201
+ return False
202
+ if not isinstance(pid, int):
203
+ logger.error("PID must be a valid integer")
204
+ return False
205
+
206
+ try:
207
+ # Sending custom signal to process whose PID is matching
208
+ os.kill(pid, sig)
209
+ return True
210
+ except Exception as exc_:
211
+ logger.error(f"Failed to kill daemon for PID {pid}. Error: {exc_}")
212
+ return False
@@ -0,0 +1,34 @@
1
+ """List of defined constants to be used in the project"""
2
+
3
+ from importlib.metadata import PackageNotFoundError, version
4
+ from typing import List, Tuple
5
+
6
+ APP_NAME: str = "daemonizer"
7
+
8
+ PKG_NAME: str = f"{APP_NAME}-py"
9
+
10
+ # Version dynamically updated via Makefile targets
11
+ APP_VERSION: str = version(PKG_NAME)
12
+ APP_VERSION_TUPLE: Tuple[int, ...] = tuple(map(int, APP_VERSION.split(".")))
13
+
14
+ try:
15
+ __version__: str = version(PKG_NAME)
16
+ except PackageNotFoundError:
17
+ __version__ = "unknown" # "0.0.0"
18
+
19
+
20
+ # UNIX system names
21
+ UNIX_SYSTEM_NAMES: List[str] = [
22
+ "Linux",
23
+ "Darwin",
24
+ "FreeBSD",
25
+ "NetBSD",
26
+ "OpenBSD",
27
+ "SunOS",
28
+ "AIX",
29
+ ]
30
+
31
+ DEFAULT_PID_FILENAME_LENGTH: int = 5
32
+
33
+ # Start method to be used when creating child processes from `multiprocessing` module
34
+ MULTIPROC_START_METHOD: str = "fork"
@@ -0,0 +1 @@
1
+ """Core logic"""
@@ -0,0 +1 @@
1
+ """Module to define and implement core logic for daemons"""