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 +0 -0
- daemonizer/cli/__init__.py +1 -0
- daemonizer/cli/commands.py +287 -0
- daemonizer/cli/daemon_loader.py +148 -0
- daemonizer/cli/processor.py +212 -0
- daemonizer/constants.py +34 -0
- daemonizer/core/__init__.py +1 -0
- daemonizer/core/daemons/__init__.py +1 -0
- daemonizer/core/daemons/base.py +96 -0
- daemonizer/core/daemons/flags.py +14 -0
- daemonizer/core/daemons/logic.py +68 -0
- daemonizer/core/daemons/unix.py +386 -0
- daemonizer/core/handlers/__init__.py +1 -0
- daemonizer/core/handlers/base_handler.py +45 -0
- daemonizer/core/handlers/ctx_manager.py +253 -0
- daemonizer/core/handlers/func_handler.py +11 -0
- daemonizer/core/pid/__init__.py +1 -0
- daemonizer/core/pid/pidfile.py +206 -0
- daemonizer/core/pid/pidfile_destinations.py +78 -0
- daemonizer/exceptions.py +37 -0
- daemonizer/files.py +15 -0
- daemonizer/samples/__init__.py +1 -0
- daemonizer/samples/writers.py +50 -0
- daemonizer/utils/__init__.py +1 -0
- daemonizer/utils/func.py +30 -0
- daemonizer/utils/logs.py +179 -0
- daemonizer/utils/oscheck.py +86 -0
- daemonizer/utils/process.py +25 -0
- daemonizer/utils/streams.py +64 -0
- daemonizer_py-1.5.1.dist-info/METADATA +135 -0
- daemonizer_py-1.5.1.dist-info/RECORD +34 -0
- daemonizer_py-1.5.1.dist-info/WHEEL +4 -0
- daemonizer_py-1.5.1.dist-info/entry_points.txt +2 -0
- daemonizer_py-1.5.1.dist-info/licenses/LICENSE +21 -0
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
|
daemonizer/constants.py
ADDED
|
@@ -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"""
|