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.
Files changed (221) hide show
  1. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/METADATA +3 -2
  2. django_nativemojo-0.1.17.dist-info/RECORD +302 -0
  3. mojo/__init__.py +1 -1
  4. mojo/apps/account/management/commands/serializer_admin.py +121 -1
  5. mojo/apps/account/migrations/0006_add_device_tracking_models.py +72 -0
  6. mojo/apps/account/migrations/0007_delete_userdevicelocation.py +16 -0
  7. mojo/apps/account/migrations/0008_userdevicelocation.py +33 -0
  8. mojo/apps/account/migrations/0009_geolocatedip_subnet.py +18 -0
  9. mojo/apps/account/migrations/0010_group_avatar.py +20 -0
  10. mojo/apps/account/migrations/0011_user_org_registereddevice_pushconfig_and_more.py +118 -0
  11. mojo/apps/account/migrations/0012_remove_pushconfig_apns_key_file_and_more.py +21 -0
  12. mojo/apps/account/migrations/0013_pushconfig_test_mode_alter_pushconfig_apns_enabled_and_more.py +28 -0
  13. mojo/apps/account/migrations/0014_notificationdelivery_data_payload_and_more.py +48 -0
  14. mojo/apps/account/models/__init__.py +2 -0
  15. mojo/apps/account/models/device.py +279 -0
  16. mojo/apps/account/models/group.py +294 -8
  17. mojo/apps/account/models/member.py +14 -1
  18. mojo/apps/account/models/push/__init__.py +4 -0
  19. mojo/apps/account/models/push/config.py +112 -0
  20. mojo/apps/account/models/push/delivery.py +93 -0
  21. mojo/apps/account/models/push/device.py +66 -0
  22. mojo/apps/account/models/push/template.py +99 -0
  23. mojo/apps/account/models/user.py +190 -17
  24. mojo/apps/account/rest/__init__.py +2 -0
  25. mojo/apps/account/rest/device.py +39 -0
  26. mojo/apps/account/rest/group.py +8 -0
  27. mojo/apps/account/rest/push.py +187 -0
  28. mojo/apps/account/rest/user.py +95 -5
  29. mojo/apps/account/services/__init__.py +1 -0
  30. mojo/apps/account/services/push.py +363 -0
  31. mojo/apps/aws/migrations/0001_initial.py +206 -0
  32. mojo/apps/aws/migrations/0002_emaildomain_can_recv_emaildomain_can_send_and_more.py +28 -0
  33. mojo/apps/aws/migrations/0003_mailbox_is_domain_default_mailbox_is_system_default_and_more.py +31 -0
  34. mojo/apps/aws/migrations/0004_s3bucket.py +39 -0
  35. mojo/apps/aws/migrations/0005_alter_emaildomain_region_delete_s3bucket.py +21 -0
  36. mojo/apps/aws/models/__init__.py +19 -0
  37. mojo/apps/aws/models/email_attachment.py +99 -0
  38. mojo/apps/aws/models/email_domain.py +218 -0
  39. mojo/apps/aws/models/email_template.py +132 -0
  40. mojo/apps/aws/models/incoming_email.py +197 -0
  41. mojo/apps/aws/models/mailbox.py +288 -0
  42. mojo/apps/aws/models/sent_message.py +175 -0
  43. mojo/apps/aws/rest/__init__.py +6 -0
  44. mojo/apps/aws/rest/email.py +33 -0
  45. mojo/apps/aws/rest/email_ops.py +183 -0
  46. mojo/apps/aws/rest/messages.py +32 -0
  47. mojo/apps/aws/rest/send.py +101 -0
  48. mojo/apps/aws/rest/sns.py +403 -0
  49. mojo/apps/aws/rest/templates.py +19 -0
  50. mojo/apps/aws/services/__init__.py +32 -0
  51. mojo/apps/aws/services/email.py +390 -0
  52. mojo/apps/aws/services/email_ops.py +548 -0
  53. mojo/apps/docit/__init__.py +6 -0
  54. mojo/apps/docit/markdown_plugins/syntax_highlight.py +25 -0
  55. mojo/apps/docit/markdown_plugins/toc.py +12 -0
  56. mojo/apps/docit/migrations/0001_initial.py +113 -0
  57. mojo/apps/docit/migrations/0002_alter_book_modified_by_alter_page_modified_by.py +26 -0
  58. mojo/apps/docit/migrations/0003_alter_book_group.py +20 -0
  59. mojo/apps/docit/models/__init__.py +17 -0
  60. mojo/apps/docit/models/asset.py +231 -0
  61. mojo/apps/docit/models/book.py +227 -0
  62. mojo/apps/docit/models/page.py +319 -0
  63. mojo/apps/docit/models/page_revision.py +203 -0
  64. mojo/apps/docit/rest/__init__.py +10 -0
  65. mojo/apps/docit/rest/asset.py +17 -0
  66. mojo/apps/docit/rest/book.py +22 -0
  67. mojo/apps/docit/rest/page.py +22 -0
  68. mojo/apps/docit/rest/page_revision.py +17 -0
  69. mojo/apps/docit/services/__init__.py +11 -0
  70. mojo/apps/docit/services/docit.py +315 -0
  71. mojo/apps/docit/services/markdown.py +44 -0
  72. mojo/apps/fileman/backends/s3.py +209 -0
  73. mojo/apps/fileman/models/file.py +45 -9
  74. mojo/apps/fileman/models/manager.py +269 -3
  75. mojo/apps/incident/migrations/0007_event_uid.py +18 -0
  76. mojo/apps/incident/migrations/0008_ticket_ticketnote.py +55 -0
  77. mojo/apps/incident/migrations/0009_incident_status.py +18 -0
  78. mojo/apps/incident/migrations/0010_event_country_code.py +18 -0
  79. mojo/apps/incident/migrations/0011_incident_country_code.py +18 -0
  80. mojo/apps/incident/migrations/0012_alter_incident_status.py +18 -0
  81. mojo/apps/incident/models/__init__.py +1 -0
  82. mojo/apps/incident/models/event.py +35 -0
  83. mojo/apps/incident/models/incident.py +2 -0
  84. mojo/apps/incident/models/ticket.py +62 -0
  85. mojo/apps/incident/reporter.py +21 -3
  86. mojo/apps/incident/rest/__init__.py +1 -0
  87. mojo/apps/incident/rest/ticket.py +43 -0
  88. mojo/apps/jobs/__init__.py +489 -0
  89. mojo/apps/jobs/adapters.py +24 -0
  90. mojo/apps/jobs/cli.py +616 -0
  91. mojo/apps/jobs/daemon.py +370 -0
  92. mojo/apps/jobs/examples/sample_jobs.py +376 -0
  93. mojo/apps/jobs/examples/webhook_examples.py +203 -0
  94. mojo/apps/jobs/handlers/__init__.py +5 -0
  95. mojo/apps/jobs/handlers/webhook.py +317 -0
  96. mojo/apps/jobs/job_engine.py +734 -0
  97. mojo/apps/jobs/keys.py +203 -0
  98. mojo/apps/jobs/local_queue.py +363 -0
  99. mojo/apps/jobs/management/__init__.py +3 -0
  100. mojo/apps/jobs/management/commands/__init__.py +3 -0
  101. mojo/apps/jobs/manager.py +1327 -0
  102. mojo/apps/jobs/migrations/0001_initial.py +97 -0
  103. mojo/apps/jobs/migrations/0002_alter_job_max_retries_joblog.py +39 -0
  104. mojo/apps/jobs/models/__init__.py +6 -0
  105. mojo/apps/jobs/models/job.py +441 -0
  106. mojo/apps/jobs/rest/__init__.py +2 -0
  107. mojo/apps/jobs/rest/control.py +466 -0
  108. mojo/apps/jobs/rest/jobs.py +421 -0
  109. mojo/apps/jobs/scheduler.py +571 -0
  110. mojo/apps/jobs/services/__init__.py +6 -0
  111. mojo/apps/jobs/services/job_actions.py +465 -0
  112. mojo/apps/jobs/settings.py +209 -0
  113. mojo/apps/logit/models/log.py +3 -0
  114. mojo/apps/metrics/__init__.py +8 -1
  115. mojo/apps/metrics/redis_metrics.py +198 -0
  116. mojo/apps/metrics/rest/__init__.py +3 -0
  117. mojo/apps/metrics/rest/categories.py +266 -0
  118. mojo/apps/metrics/rest/helpers.py +48 -0
  119. mojo/apps/metrics/rest/permissions.py +99 -0
  120. mojo/apps/metrics/rest/values.py +277 -0
  121. mojo/apps/metrics/utils.py +17 -0
  122. mojo/decorators/http.py +40 -1
  123. mojo/helpers/aws/__init__.py +11 -7
  124. mojo/helpers/aws/inbound_email.py +309 -0
  125. mojo/helpers/aws/kms.py +413 -0
  126. mojo/helpers/aws/ses_domain.py +959 -0
  127. mojo/helpers/crypto/__init__.py +1 -1
  128. mojo/helpers/crypto/utils.py +15 -0
  129. mojo/helpers/location/__init__.py +2 -0
  130. mojo/helpers/location/countries.py +262 -0
  131. mojo/helpers/location/geolocation.py +196 -0
  132. mojo/helpers/logit.py +37 -0
  133. mojo/helpers/redis/__init__.py +2 -0
  134. mojo/helpers/redis/adapter.py +606 -0
  135. mojo/helpers/redis/client.py +48 -0
  136. mojo/helpers/redis/pool.py +225 -0
  137. mojo/helpers/request.py +8 -0
  138. mojo/helpers/response.py +8 -0
  139. mojo/middleware/auth.py +1 -1
  140. mojo/middleware/cors.py +40 -0
  141. mojo/middleware/logging.py +131 -12
  142. mojo/middleware/mojo.py +5 -0
  143. mojo/models/rest.py +271 -57
  144. mojo/models/secrets.py +86 -0
  145. mojo/serializers/__init__.py +16 -10
  146. mojo/serializers/core/__init__.py +90 -0
  147. mojo/serializers/core/cache/__init__.py +121 -0
  148. mojo/serializers/core/cache/backends.py +518 -0
  149. mojo/serializers/core/cache/base.py +102 -0
  150. mojo/serializers/core/cache/disabled.py +181 -0
  151. mojo/serializers/core/cache/memory.py +287 -0
  152. mojo/serializers/core/cache/redis.py +533 -0
  153. mojo/serializers/core/cache/utils.py +454 -0
  154. mojo/serializers/{manager.py → core/manager.py} +53 -4
  155. mojo/serializers/core/serializer.py +475 -0
  156. mojo/serializers/{advanced/formats → formats}/csv.py +116 -139
  157. mojo/serializers/suggested_improvements.md +388 -0
  158. testit/client.py +1 -1
  159. testit/helpers.py +14 -0
  160. testit/runner.py +23 -6
  161. django_nativemojo-0.1.15.dist-info/RECORD +0 -234
  162. mojo/apps/notify/README.md +0 -91
  163. mojo/apps/notify/README_NOTIFICATIONS.md +0 -566
  164. mojo/apps/notify/admin.py +0 -52
  165. mojo/apps/notify/handlers/example_handlers.py +0 -516
  166. mojo/apps/notify/handlers/ses/__init__.py +0 -25
  167. mojo/apps/notify/handlers/ses/complaint.py +0 -25
  168. mojo/apps/notify/handlers/ses/message.py +0 -86
  169. mojo/apps/notify/management/commands/__init__.py +0 -1
  170. mojo/apps/notify/management/commands/process_notifications.py +0 -370
  171. mojo/apps/notify/mod +0 -0
  172. mojo/apps/notify/models/__init__.py +0 -12
  173. mojo/apps/notify/models/account.py +0 -128
  174. mojo/apps/notify/models/attachment.py +0 -24
  175. mojo/apps/notify/models/bounce.py +0 -68
  176. mojo/apps/notify/models/complaint.py +0 -40
  177. mojo/apps/notify/models/inbox.py +0 -113
  178. mojo/apps/notify/models/inbox_message.py +0 -173
  179. mojo/apps/notify/models/outbox.py +0 -129
  180. mojo/apps/notify/models/outbox_message.py +0 -288
  181. mojo/apps/notify/models/template.py +0 -30
  182. mojo/apps/notify/providers/aws.py +0 -73
  183. mojo/apps/notify/rest/ses.py +0 -0
  184. mojo/apps/notify/utils/__init__.py +0 -2
  185. mojo/apps/notify/utils/notifications.py +0 -404
  186. mojo/apps/notify/utils/parsing.py +0 -202
  187. mojo/apps/notify/utils/render.py +0 -144
  188. mojo/apps/tasks/README.md +0 -118
  189. mojo/apps/tasks/__init__.py +0 -44
  190. mojo/apps/tasks/manager.py +0 -644
  191. mojo/apps/tasks/rest/__init__.py +0 -2
  192. mojo/apps/tasks/rest/hooks.py +0 -0
  193. mojo/apps/tasks/rest/tasks.py +0 -76
  194. mojo/apps/tasks/runner.py +0 -439
  195. mojo/apps/tasks/task.py +0 -99
  196. mojo/apps/tasks/tq_handlers.py +0 -132
  197. mojo/helpers/crypto/__pycache__/hash.cpython-310.pyc +0 -0
  198. mojo/helpers/crypto/__pycache__/sign.cpython-310.pyc +0 -0
  199. mojo/helpers/crypto/__pycache__/utils.cpython-310.pyc +0 -0
  200. mojo/helpers/redis.py +0 -10
  201. mojo/models/meta.py +0 -262
  202. mojo/serializers/advanced/README.md +0 -363
  203. mojo/serializers/advanced/__init__.py +0 -247
  204. mojo/serializers/advanced/formats/__init__.py +0 -28
  205. mojo/serializers/advanced/formats/excel.py +0 -516
  206. mojo/serializers/advanced/formats/json.py +0 -239
  207. mojo/serializers/advanced/formats/response.py +0 -485
  208. mojo/serializers/advanced/serializer.py +0 -568
  209. mojo/serializers/optimized.py +0 -618
  210. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/LICENSE +0 -0
  211. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/NOTICE +0 -0
  212. {django_nativemojo-0.1.15.dist-info → django_nativemojo-0.1.17.dist-info}/WHEEL +0 -0
  213. /mojo/apps/{notify → aws/migrations}/__init__.py +0 -0
  214. /mojo/apps/{notify/handlers → docit/markdown_plugins}/__init__.py +0 -0
  215. /mojo/apps/{notify/management → docit/migrations}/__init__.py +0 -0
  216. /mojo/apps/{notify/providers → jobs/examples}/__init__.py +0 -0
  217. /mojo/apps/{notify/rest → jobs/migrations}/__init__.py +0 -0
  218. /mojo/{serializers → rest}/openapi.py +0 -0
  219. /mojo/serializers/{settings_example.py → examples/settings.py} +0 -0
  220. /mojo/{apps/notify/handlers/ses/bounce.py → serializers/formats/__init__.py} +0 -0
  221. /mojo/serializers/{advanced/formats → formats}/localizers.py +0 -0
@@ -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()