django-nativemojo 0.1.15__py3-none-any.whl → 0.1.17__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.
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
- django_nativemojo-0.1.17.dist-info/RECORD +302 -0
- mojo/__init__.py +1 -1
- mojo/apps/account/management/commands/serializer_admin.py +121 -1
- mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
- mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
- mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
- mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
- mojo/apps/account/migrations/0010_group_avatar.py +20 -0
- mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
- mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
- mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
- mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
- mojo/apps/account/models/__init__.py +2 -0
- mojo/apps/account/models/device.py +279 -0
- mojo/apps/account/models/group.py +294 -8
- mojo/apps/account/models/member.py +14 -1
- mojo/apps/account/models/push/__init__.py +4 -0
- mojo/apps/account/models/push/config.py +112 -0
- mojo/apps/account/models/push/delivery.py +93 -0
- mojo/apps/account/models/push/device.py +66 -0
- mojo/apps/account/models/push/template.py +99 -0
- mojo/apps/account/models/user.py +190 -17
- mojo/apps/account/rest/__init__.py +2 -0
- mojo/apps/account/rest/device.py +39 -0
- mojo/apps/account/rest/group.py +8 -0
- mojo/apps/account/rest/push.py +187 -0
- mojo/apps/account/rest/user.py +95 -5
- mojo/apps/account/services/__init__.py +1 -0
- mojo/apps/account/services/push.py +363 -0
- mojo/apps/aws/migrations/0001_initial.py +206 -0
- mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
- mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
- mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
- mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
- mojo/apps/aws/models/__init__.py +19 -0
- mojo/apps/aws/models/email_attachment.py +99 -0
- mojo/apps/aws/models/email_domain.py +218 -0
- mojo/apps/aws/models/email_template.py +132 -0
- mojo/apps/aws/models/incoming_email.py +197 -0
- mojo/apps/aws/models/mailbox.py +288 -0
- mojo/apps/aws/models/sent_message.py +175 -0
- mojo/apps/aws/rest/__init__.py +6 -0
- mojo/apps/aws/rest/email.py +33 -0
- mojo/apps/aws/rest/email_ops.py +183 -0
- mojo/apps/aws/rest/messages.py +32 -0
- mojo/apps/aws/rest/send.py +101 -0
- mojo/apps/aws/rest/sns.py +403 -0
- mojo/apps/aws/rest/templates.py +19 -0
- mojo/apps/aws/services/__init__.py +32 -0
- mojo/apps/aws/services/email.py +390 -0
- mojo/apps/aws/services/email_ops.py +548 -0
- mojo/apps/docit/__init__.py +6 -0
- mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
- mojo/apps/docit/markdown_plugins/toc.py +12 -0
- mojo/apps/docit/migrations/0001_initial.py +113 -0
- mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
- mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
- mojo/apps/docit/models/__init__.py +17 -0
- mojo/apps/docit/models/asset.py +231 -0
- mojo/apps/docit/models/book.py +227 -0
- mojo/apps/docit/models/page.py +319 -0
- mojo/apps/docit/models/page_revision.py +203 -0
- mojo/apps/docit/rest/__init__.py +10 -0
- mojo/apps/docit/rest/asset.py +17 -0
- mojo/apps/docit/rest/book.py +22 -0
- mojo/apps/docit/rest/page.py +22 -0
- mojo/apps/docit/rest/page_revision.py +17 -0
- mojo/apps/docit/services/__init__.py +11 -0
- mojo/apps/docit/services/docit.py +315 -0
- mojo/apps/docit/services/markdown.py +44 -0
- mojo/apps/fileman/backends/s3.py +209 -0
- mojo/apps/fileman/models/file.py +45 -9
- mojo/apps/fileman/models/manager.py +269 -3
- mojo/apps/incident/migrations/0007_event_uid.py +18 -0
- mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
- mojo/apps/incident/migrations/0009_incident_status.py +18 -0
- mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
- mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
- mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
- mojo/apps/incident/models/__init__.py +1 -0
- mojo/apps/incident/models/event.py +35 -0
- mojo/apps/incident/models/incident.py +2 -0
- mojo/apps/incident/models/ticket.py +62 -0
- mojo/apps/incident/reporter.py +21 -3
- mojo/apps/incident/rest/__init__.py +1 -0
- mojo/apps/incident/rest/ticket.py +43 -0
- mojo/apps/jobs/__init__.py +489 -0
- mojo/apps/jobs/adapters.py +24 -0
- mojo/apps/jobs/cli.py +616 -0
- mojo/apps/jobs/daemon.py +370 -0
- mojo/apps/jobs/examples/sample_jobs.py +376 -0
- mojo/apps/jobs/examples/webhook_examples.py +203 -0
- mojo/apps/jobs/handlers/__init__.py +5 -0
- mojo/apps/jobs/handlers/webhook.py +317 -0
- mojo/apps/jobs/job_engine.py +734 -0
- mojo/apps/jobs/keys.py +203 -0
- mojo/apps/jobs/local_queue.py +363 -0
- mojo/apps/jobs/management/__init__.py +3 -0
- mojo/apps/jobs/management/commands/__init__.py +3 -0
- mojo/apps/jobs/manager.py +1327 -0
- mojo/apps/jobs/migrations/0001_initial.py +97 -0
- mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
- mojo/apps/jobs/models/__init__.py +6 -0
- mojo/apps/jobs/models/job.py +441 -0
- mojo/apps/jobs/rest/__init__.py +2 -0
- mojo/apps/jobs/rest/control.py +466 -0
- mojo/apps/jobs/rest/jobs.py +421 -0
- mojo/apps/jobs/scheduler.py +571 -0
- mojo/apps/jobs/services/__init__.py +6 -0
- mojo/apps/jobs/services/job_actions.py +465 -0
- mojo/apps/jobs/settings.py +209 -0
- mojo/apps/logit/models/log.py +3 -0
- mojo/apps/metrics/__init__.py +8 -1
- mojo/apps/metrics/redis_metrics.py +198 -0
- mojo/apps/metrics/rest/__init__.py +3 -0
- mojo/apps/metrics/rest/categories.py +266 -0
- mojo/apps/metrics/rest/helpers.py +48 -0
- mojo/apps/metrics/rest/permissions.py +99 -0
- mojo/apps/metrics/rest/values.py +277 -0
- mojo/apps/metrics/utils.py +17 -0
- mojo/decorators/http.py +40 -1
- mojo/helpers/aws/__init__.py +11 -7
- mojo/helpers/aws/inbound_email.py +309 -0
- mojo/helpers/aws/kms.py +413 -0
- mojo/helpers/aws/ses_domain.py +959 -0
- mojo/helpers/crypto/__init__.py +1 -1
- mojo/helpers/crypto/utils.py +15 -0
- mojo/helpers/location/__init__.py +2 -0
- mojo/helpers/location/countries.py +262 -0
- mojo/helpers/location/geolocation.py +196 -0
- mojo/helpers/logit.py +37 -0
- mojo/helpers/redis/__init__.py +2 -0
- mojo/helpers/redis/adapter.py +606 -0
- mojo/helpers/redis/client.py +48 -0
- mojo/helpers/redis/pool.py +225 -0
- mojo/helpers/request.py +8 -0
- mojo/helpers/response.py +8 -0
- mojo/middleware/auth.py +1 -1
- mojo/middleware/cors.py +40 -0
- mojo/middleware/logging.py +131 -12
- mojo/middleware/mojo.py +5 -0
- mojo/models/rest.py +271 -57
- mojo/models/secrets.py +86 -0
- mojo/serializers/__init__.py +16 -10
- mojo/serializers/core/__init__.py +90 -0
- mojo/serializers/core/cache/__init__.py +121 -0
- mojo/serializers/core/cache/backends.py +518 -0
- mojo/serializers/core/cache/base.py +102 -0
- mojo/serializers/core/cache/disabled.py +181 -0
- mojo/serializers/core/cache/memory.py +287 -0
- mojo/serializers/core/cache/redis.py +533 -0
- mojo/serializers/core/cache/utils.py +454 -0
- mojo/serializers/{manager.py → core/manager.py} +53 -4
- mojo/serializers/core/serializer.py +475 -0
- mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
- mojo/serializers/suggested_improvements.md +388 -0
- testit/client.py +1 -1
- testit/helpers.py +14 -0
- testit/runner.py +23 -6
- django_nativemojo-0.1.15.dist-info/RECORD +0 -234
- mojo/apps/notify/README.md +0 -91
- mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
- mojo/apps/notify/admin.py +0 -52
- mojo/apps/notify/handlers/example_handlers.py +0 -516
- mojo/apps/notify/handlers/ses/__init__.py +0 -25
- mojo/apps/notify/handlers/ses/complaint.py +0 -25
- mojo/apps/notify/handlers/ses/message.py +0 -86
- mojo/apps/notify/management/commands/__init__.py +0 -1
- mojo/apps/notify/management/commands/process_notifications.py +0 -370
- mojo/apps/notify/mod +0 -0
- mojo/apps/notify/models/__init__.py +0 -12
- mojo/apps/notify/models/account.py +0 -128
- mojo/apps/notify/models/attachment.py +0 -24
- mojo/apps/notify/models/bounce.py +0 -68
- mojo/apps/notify/models/complaint.py +0 -40
- mojo/apps/notify/models/inbox.py +0 -113
- mojo/apps/notify/models/inbox_message.py +0 -173
- mojo/apps/notify/models/outbox.py +0 -129
- mojo/apps/notify/models/outbox_message.py +0 -288
- mojo/apps/notify/models/template.py +0 -30
- mojo/apps/notify/providers/aws.py +0 -73
- mojo/apps/notify/rest/ses.py +0 -0
- mojo/apps/notify/utils/__init__.py +0 -2
- mojo/apps/notify/utils/notifications.py +0 -404
- mojo/apps/notify/utils/parsing.py +0 -202
- mojo/apps/notify/utils/render.py +0 -144
- mojo/apps/tasks/README.md +0 -118
- mojo/apps/tasks/__init__.py +0 -44
- mojo/apps/tasks/manager.py +0 -644
- mojo/apps/tasks/rest/__init__.py +0 -2
- mojo/apps/tasks/rest/hooks.py +0 -0
- mojo/apps/tasks/rest/tasks.py +0 -76
- mojo/apps/tasks/runner.py +0 -439
- mojo/apps/tasks/task.py +0 -99
- mojo/apps/tasks/tq_handlers.py +0 -132
- mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
- mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
- mojo/helpers/redis.py +0 -10
- mojo/models/meta.py +0 -262
- mojo/serializers/advanced/README.md +0 -363
- mojo/serializers/advanced/__init__.py +0 -247
- mojo/serializers/advanced/formats/__init__.py +0 -28
- mojo/serializers/advanced/formats/excel.py +0 -516
- mojo/serializers/advanced/formats/json.py +0 -239
- mojo/serializers/advanced/formats/response.py +0 -485
- mojo/serializers/advanced/serializer.py +0 -568
- mojo/serializers/optimized.py +0 -618
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
- {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
- /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
- /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
- /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
- /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
- /mojo/{serializers → rest}/openapi.py +0 -0
- /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
- /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
- /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
mojo/apps/jobs/daemon.py
ADDED
@@ -0,0 +1,370 @@
|
|
1
|
+
"""
|
2
|
+
Daemon utility for running processes in foreground or background mode.
|
3
|
+
|
4
|
+
Provides daemonization support for job engine and scheduler processes,
|
5
|
+
with proper PID file management and signal handling.
|
6
|
+
"""
|
7
|
+
import os
|
8
|
+
import sys
|
9
|
+
import atexit
|
10
|
+
import signal
|
11
|
+
import time
|
12
|
+
import fcntl
|
13
|
+
from typing import Callable, Optional
|
14
|
+
from pathlib import Path
|
15
|
+
|
16
|
+
from mojo.helpers import logit
|
17
|
+
logger = logit.get_logger("jobs", "jobs.log")
|
18
|
+
|
19
|
+
class DaemonContext:
|
20
|
+
"""
|
21
|
+
Context manager for daemonizing a process.
|
22
|
+
|
23
|
+
Supports both foreground and background modes with proper
|
24
|
+
signal handling and PID file management.
|
25
|
+
"""
|
26
|
+
|
27
|
+
def __init__(
|
28
|
+
self,
|
29
|
+
pidfile: Optional[str] = None,
|
30
|
+
stdin: str = '/dev/null',
|
31
|
+
stdout: str = '/dev/null',
|
32
|
+
stderr: str = '/dev/null',
|
33
|
+
working_dir: str = '/',
|
34
|
+
umask: int = 0o22,
|
35
|
+
detach: bool = True
|
36
|
+
):
|
37
|
+
"""
|
38
|
+
Initialize daemon context.
|
39
|
+
|
40
|
+
Args:
|
41
|
+
pidfile: Path to PID file (optional)
|
42
|
+
stdin: Path for stdin redirection in daemon mode
|
43
|
+
stdout: Path for stdout redirection in daemon mode
|
44
|
+
stderr: Path for stderr redirection in daemon mode
|
45
|
+
working_dir: Working directory for daemon
|
46
|
+
umask: File creation mask
|
47
|
+
detach: If True, detach from terminal (background mode)
|
48
|
+
"""
|
49
|
+
self.pidfile = pidfile
|
50
|
+
self.stdin = stdin
|
51
|
+
self.stdout = stdout
|
52
|
+
self.stderr = stderr
|
53
|
+
self.working_dir = working_dir
|
54
|
+
self.umask = umask
|
55
|
+
self.detach = detach
|
56
|
+
self._pidfile_handle = None
|
57
|
+
|
58
|
+
def __enter__(self):
|
59
|
+
"""Enter daemon context."""
|
60
|
+
if self.detach:
|
61
|
+
self._daemonize()
|
62
|
+
|
63
|
+
# Write PID file
|
64
|
+
if self.pidfile:
|
65
|
+
self._write_pidfile()
|
66
|
+
|
67
|
+
return self
|
68
|
+
|
69
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
70
|
+
"""Exit daemon context and cleanup."""
|
71
|
+
if self.pidfile and self._pidfile_handle:
|
72
|
+
self._remove_pidfile()
|
73
|
+
|
74
|
+
def _daemonize(self):
|
75
|
+
"""
|
76
|
+
Detach from terminal and become a daemon.
|
77
|
+
|
78
|
+
Uses double-fork technique to properly detach from terminal.
|
79
|
+
"""
|
80
|
+
# First fork
|
81
|
+
try:
|
82
|
+
pid = os.fork()
|
83
|
+
if pid > 0:
|
84
|
+
# Parent process exits
|
85
|
+
sys.exit(0)
|
86
|
+
except OSError as e:
|
87
|
+
logger.error(f"First fork failed: {e}")
|
88
|
+
sys.exit(1)
|
89
|
+
|
90
|
+
# Decouple from parent environment
|
91
|
+
os.chdir(self.working_dir)
|
92
|
+
os.setsid()
|
93
|
+
os.umask(self.umask)
|
94
|
+
|
95
|
+
# Second fork
|
96
|
+
try:
|
97
|
+
pid = os.fork()
|
98
|
+
if pid > 0:
|
99
|
+
# Parent process exits
|
100
|
+
sys.exit(0)
|
101
|
+
except OSError as e:
|
102
|
+
logger.error(f"Second fork failed: {e}")
|
103
|
+
sys.exit(1)
|
104
|
+
|
105
|
+
# Redirect standard file descriptors
|
106
|
+
sys.stdout.flush()
|
107
|
+
sys.stderr.flush()
|
108
|
+
|
109
|
+
# Open file descriptors
|
110
|
+
si = open(self.stdin, 'r')
|
111
|
+
so = open(self.stdout, 'a+')
|
112
|
+
se = open(self.stderr, 'a+')
|
113
|
+
|
114
|
+
# Duplicate file descriptors
|
115
|
+
os.dup2(si.fileno(), sys.stdin.fileno())
|
116
|
+
os.dup2(so.fileno(), sys.stdout.fileno())
|
117
|
+
os.dup2(se.fileno(), sys.stderr.fileno())
|
118
|
+
|
119
|
+
logger.info(f"Process daemonized with PID: {os.getpid()}")
|
120
|
+
|
121
|
+
def _write_pidfile(self):
|
122
|
+
"""Write PID file with exclusive lock."""
|
123
|
+
pid = str(os.getpid())
|
124
|
+
|
125
|
+
try:
|
126
|
+
# Create parent directories if needed
|
127
|
+
Path(self.pidfile).parent.mkdir(parents=True, exist_ok=True)
|
128
|
+
|
129
|
+
# Open with exclusive create
|
130
|
+
self._pidfile_handle = open(self.pidfile, 'w')
|
131
|
+
|
132
|
+
# Try to acquire exclusive lock
|
133
|
+
fcntl.lockf(self._pidfile_handle, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
134
|
+
|
135
|
+
# Write PID
|
136
|
+
self._pidfile_handle.write(f"{pid}\n")
|
137
|
+
self._pidfile_handle.flush()
|
138
|
+
|
139
|
+
# Register cleanup
|
140
|
+
atexit.register(self._remove_pidfile)
|
141
|
+
|
142
|
+
logger.info(f"PID file created: {self.pidfile} (PID: {pid})")
|
143
|
+
|
144
|
+
except IOError as e:
|
145
|
+
if e.errno == 11: # Resource temporarily unavailable
|
146
|
+
logger.error(f"Another instance is already running (PID file: {self.pidfile})")
|
147
|
+
else:
|
148
|
+
logger.error(f"Failed to write PID file: {e}")
|
149
|
+
sys.exit(1)
|
150
|
+
|
151
|
+
def _remove_pidfile(self):
|
152
|
+
"""Remove PID file on exit."""
|
153
|
+
if self._pidfile_handle:
|
154
|
+
try:
|
155
|
+
# Release lock and close
|
156
|
+
fcntl.lockf(self._pidfile_handle, fcntl.LOCK_UN)
|
157
|
+
self._pidfile_handle.close()
|
158
|
+
self._pidfile_handle = None
|
159
|
+
except Exception:
|
160
|
+
pass
|
161
|
+
|
162
|
+
if self.pidfile and os.path.exists(self.pidfile):
|
163
|
+
try:
|
164
|
+
os.remove(self.pidfile)
|
165
|
+
logger.info(f"PID file removed: {self.pidfile}")
|
166
|
+
except Exception as e:
|
167
|
+
logger.warn(f"Failed to remove PID file: {e}")
|
168
|
+
|
169
|
+
|
170
|
+
class DaemonRunner:
|
171
|
+
"""
|
172
|
+
Runner for daemon processes with signal handling.
|
173
|
+
|
174
|
+
Provides a standard interface for running processes as daemons
|
175
|
+
with proper signal handling and lifecycle management.
|
176
|
+
"""
|
177
|
+
|
178
|
+
def __init__(
|
179
|
+
self,
|
180
|
+
name: str,
|
181
|
+
run_func: Callable,
|
182
|
+
stop_func: Optional[Callable] = None,
|
183
|
+
pidfile: Optional[str] = None,
|
184
|
+
logfile: Optional[str] = None,
|
185
|
+
daemon: bool = False
|
186
|
+
):
|
187
|
+
"""
|
188
|
+
Initialize daemon runner.
|
189
|
+
|
190
|
+
Args:
|
191
|
+
name: Daemon name for logging
|
192
|
+
run_func: Main function to run
|
193
|
+
stop_func: Function to call on shutdown (optional)
|
194
|
+
pidfile: Path to PID file
|
195
|
+
logfile: Path to log file (for background mode)
|
196
|
+
daemon: If True, run as background daemon
|
197
|
+
"""
|
198
|
+
self.name = name
|
199
|
+
self.run_func = run_func
|
200
|
+
self.stop_func = stop_func
|
201
|
+
self.pidfile = pidfile
|
202
|
+
self.logfile = logfile
|
203
|
+
self.daemon = daemon
|
204
|
+
self._stop_requested = False
|
205
|
+
|
206
|
+
def start(self):
|
207
|
+
"""Start the daemon."""
|
208
|
+
# Setup signal handlers
|
209
|
+
signal.signal(signal.SIGTERM, self._handle_signal)
|
210
|
+
signal.signal(signal.SIGINT, self._handle_signal)
|
211
|
+
|
212
|
+
# Determine output files for daemon mode
|
213
|
+
stdout = self.logfile if self.daemon and self.logfile else '/dev/null'
|
214
|
+
stderr = self.logfile if self.daemon and self.logfile else '/dev/null'
|
215
|
+
|
216
|
+
# Darwin safety: allow fork without exec for Objective-C initialized runtimes
|
217
|
+
if self.daemon and sys.platform == 'darwin' and 'OBJC_DISABLE_INITIALIZE_FORK_SAFETY' not in os.environ:
|
218
|
+
os.environ['OBJC_DISABLE_INITIALIZE_FORK_SAFETY'] = 'YES'
|
219
|
+
logger.info("Set OBJC_DISABLE_INITIALIZE_FORK_SAFETY=YES for Darwin fork-safety")
|
220
|
+
|
221
|
+
# Optional working directory from settings
|
222
|
+
working_dir = '/'
|
223
|
+
try:
|
224
|
+
from mojo.helpers.settings import settings as mojo_settings
|
225
|
+
working_dir = mojo_settings.get('JOBS_DAEMON_WORKDIR', '/')
|
226
|
+
except Exception:
|
227
|
+
working_dir = '/'
|
228
|
+
|
229
|
+
# Create daemon context
|
230
|
+
context = DaemonContext(
|
231
|
+
pidfile=self.pidfile,
|
232
|
+
stdout=stdout,
|
233
|
+
stderr=stderr,
|
234
|
+
working_dir=working_dir,
|
235
|
+
detach=self.daemon
|
236
|
+
)
|
237
|
+
|
238
|
+
with context:
|
239
|
+
if self.daemon:
|
240
|
+
logger.info(f"{self.name} started as background daemon (PID: {os.getpid()})")
|
241
|
+
else:
|
242
|
+
logger.info(f"{self.name} started in foreground mode (PID: {os.getpid()})")
|
243
|
+
|
244
|
+
try:
|
245
|
+
# Run the main function
|
246
|
+
self.run_func()
|
247
|
+
except Exception:
|
248
|
+
logger.exception(f"{self.name} crashed")
|
249
|
+
sys.exit(1)
|
250
|
+
finally:
|
251
|
+
if self.stop_func and not self._stop_requested:
|
252
|
+
self.stop_func()
|
253
|
+
|
254
|
+
def stop(self):
|
255
|
+
"""Stop a running daemon by PID file."""
|
256
|
+
if not self.pidfile or not os.path.exists(self.pidfile):
|
257
|
+
logger.error(f"PID file not found: {self.pidfile}")
|
258
|
+
return False
|
259
|
+
|
260
|
+
try:
|
261
|
+
with open(self.pidfile, 'r') as f:
|
262
|
+
pid = int(f.read().strip())
|
263
|
+
|
264
|
+
# Send SIGTERM
|
265
|
+
os.kill(pid, signal.SIGTERM)
|
266
|
+
logger.info(f"Sent SIGTERM to {self.name} (PID: {pid})")
|
267
|
+
|
268
|
+
# Wait for process to stop
|
269
|
+
for i in range(10):
|
270
|
+
try:
|
271
|
+
os.kill(pid, 0) # Check if process exists
|
272
|
+
time.sleep(1)
|
273
|
+
except ProcessLookupError:
|
274
|
+
logger.info(f"{self.name} stopped successfully")
|
275
|
+
return True
|
276
|
+
|
277
|
+
# Force kill if still running
|
278
|
+
try:
|
279
|
+
os.kill(pid, signal.SIGKILL)
|
280
|
+
logger.warn(f"Force killed {self.name} (PID: {pid})")
|
281
|
+
except ProcessLookupError:
|
282
|
+
pass
|
283
|
+
|
284
|
+
return True
|
285
|
+
|
286
|
+
except Exception as e:
|
287
|
+
logger.error(f"Failed to stop {self.name}: {e}")
|
288
|
+
return False
|
289
|
+
|
290
|
+
def status(self) -> bool:
|
291
|
+
"""
|
292
|
+
Check if daemon is running.
|
293
|
+
|
294
|
+
Returns:
|
295
|
+
True if daemon is running, False otherwise
|
296
|
+
"""
|
297
|
+
if not self.pidfile or not os.path.exists(self.pidfile):
|
298
|
+
return False
|
299
|
+
|
300
|
+
try:
|
301
|
+
with open(self.pidfile, 'r') as f:
|
302
|
+
pid = int(f.read().strip())
|
303
|
+
|
304
|
+
# Check if process exists
|
305
|
+
os.kill(pid, 0)
|
306
|
+
return True
|
307
|
+
|
308
|
+
except (ProcessLookupError, ValueError):
|
309
|
+
# Process doesn't exist or invalid PID
|
310
|
+
return False
|
311
|
+
except Exception:
|
312
|
+
return False
|
313
|
+
|
314
|
+
def restart(self):
|
315
|
+
"""Restart the daemon."""
|
316
|
+
if self.status():
|
317
|
+
logger.info(f"Stopping {self.name}...")
|
318
|
+
self.stop()
|
319
|
+
time.sleep(2)
|
320
|
+
|
321
|
+
logger.info(f"Starting {self.name}...")
|
322
|
+
self.start()
|
323
|
+
|
324
|
+
def _handle_signal(self, signum, frame):
|
325
|
+
"""Handle shutdown signals."""
|
326
|
+
logger.info(f"{self.name} received signal {signum}, shutting down gracefully")
|
327
|
+
self._stop_requested = True
|
328
|
+
|
329
|
+
if self.stop_func:
|
330
|
+
self.stop_func()
|
331
|
+
|
332
|
+
sys.exit(0)
|
333
|
+
|
334
|
+
|
335
|
+
def run_daemon(
|
336
|
+
name: str,
|
337
|
+
main_func: Callable,
|
338
|
+
args=None,
|
339
|
+
kwargs=None,
|
340
|
+
daemon: bool = False,
|
341
|
+
pidfile: Optional[str] = None,
|
342
|
+
logfile: Optional[str] = None
|
343
|
+
):
|
344
|
+
"""
|
345
|
+
Convenience function to run a process as a daemon.
|
346
|
+
|
347
|
+
Args:
|
348
|
+
name: Process name
|
349
|
+
main_func: Main function to run
|
350
|
+
args: Positional arguments for main_func
|
351
|
+
kwargs: Keyword arguments for main_func
|
352
|
+
daemon: If True, run as background daemon
|
353
|
+
pidfile: PID file path
|
354
|
+
logfile: Log file path (for background mode)
|
355
|
+
"""
|
356
|
+
args = args or ()
|
357
|
+
kwargs = kwargs or {}
|
358
|
+
|
359
|
+
def run():
|
360
|
+
main_func(*args, **kwargs)
|
361
|
+
|
362
|
+
runner = DaemonRunner(
|
363
|
+
name=name,
|
364
|
+
run_func=run,
|
365
|
+
pidfile=pidfile,
|
366
|
+
logfile=logfile,
|
367
|
+
daemon=daemon
|
368
|
+
)
|
369
|
+
|
370
|
+
runner.start()
|