jenkins-bot has submitted this change. (
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1191813?usp=email )
Change subject: IMPT: daemonize improvements
......................................................................
IMPT: daemonize improvements
- enforce keyword-only parameters and add deprecation warning
- raise a NotImplementedError if called under non-posix os
- suppress OSError when closing standard streams
- Update and expand docstring
- add daemonize to pre-commit MyPy tests
Change-Id: I50ae24b2567c092add345a842ae0c2035a2be0e4
---
M .pre-commit-config.yaml
M ROADMAP.rst
M conftest.py
M pywikibot/daemonize.py
4 files changed, 89 insertions(+), 8 deletions(-)
Approvals:
Xqt: Looks good to me, approved
jenkins-bot: Verified
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 10a958f..872128b 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -65,7 +65,7 @@
language: python
require_serial: true
- repo: https://github.com/astral-sh/ruff-pre-commit
- rev: v0.13.1
+ rev: v0.13.2
hooks:
- id: ruff-check
alias: ruff
@@ -126,7 +126,7 @@
# They should be also used in conftest.py to exclude them from
non-voting mypy test.
files: |
(?x)^pywikibot/(
-
(__metadata__|backports|config|diff|echo|exceptions|fixes|logging|plural|time)|
+
(__metadata__|backports|config|daemonize|diff|echo|exceptions|fixes|logging|plural|time)|
(comms|data|families|specialbots)/__init__|
comms/eventstreams|
data/(api/(__init__|_optionset)|memento|wikistats)|
diff --git a/ROADMAP.rst b/ROADMAP.rst
index 69367fd..3e5439f 100644
--- a/ROADMAP.rst
+++ b/ROADMAP.rst
@@ -1,6 +1,8 @@
Current Release Changes
=======================
+* Positional arguments of :func:`daemonize()<daemonize.daemonize>` are
deprecated and must
+ be given as keyword arguments.
* Return :meth:`bot.BaseBot.userPut` result with
:meth:`AutomaticTWSummaryBot.put_current()
<bot.AutomaticTWSummaryBot.put_current>` method
@@ -11,6 +13,8 @@
Pending removal in Pywikibot 13
-------------------------------
+* 10.6.0: Positional arguments of :func:`daemonize()<daemonize.daemonize>` are
deprecated and must
+ be given as keyword arguments.
* 10.5.0: Accessing the fallback '*' keys in 'languages', 'namespaces',
'namespacealiases', and
'skins' properties of
:attr:`APISite.siteinfo<pywikibot.site._apisite.APISite.siteinfo>` are
deprecated and will be removed.
diff --git a/conftest.py b/conftest.py
index e33738a..d34593e 100644
--- a/conftest.py
+++ b/conftest.py
@@ -16,8 +16,8 @@
EXCLUDE_PATTERN = re.compile(
r'(?:'
- r'(__metadata__|backports|config|diff|echo|exceptions|fixes|logging|'
- r'plural|time)|'
+ r'(__metadata__|backports|config|daemonize|diff|echo|exceptions|fixes|'
+ r'logging|plural|time)|'
r'(comms|data|families|specialbots)/__init__|'
r'comms/eventstreams|'
r'data/(api/(__init__|_optionset)|memento|wikistats)|'
diff --git a/pywikibot/daemonize.py b/pywikibot/daemonize.py
index ea90c16..9bdc9e5 100644
--- a/pywikibot/daemonize.py
+++ b/pywikibot/daemonize.py
@@ -1,4 +1,54 @@
-"""Module to daemonize the current process on Unix."""
+"""Module to daemonize the current process on POSIX systems.
+
+This module provides a function :func:`daemonize` to turn the current
+Python process into a background daemon process on POSIX-compatible
+operating systems (Linux, macOS, FreeBSD) but not on not WASI Android or
+iOS. It uses the standard double-fork technique to detach the process
+from the controlling terminal and optionally closes or redirects
+standard streams.
+
+Double-fork diagram::
+
+ Original process (parent)
+ ├── fork() → creates first child
+ │ └─ Parent exits via os._exit() → returns control to terminal
+ │
+ └── First child
+ ├── os.setsid() → becomes session leader (detaches from terminal)
+ ├── fork() → creates second child (grandchild)
+ │ └─ First child exits → ensures grandchild is NOT a session leader
+ │
+ └── Second child (Daemon)
+ ├── is_daemon = True
+ ├── Optionally close/redirect standard streams
+ ├── Optionally change working directory
+ └── # Daemon continues here
+ while True:
+ do_background_work()
+
+The "while True" loop represents the main work of the daemon:
+
+- It runs indefinitely in the background
+- Performs tasks such as monitoring files, processing data, or logging
+- Everything after :func:`daemonize` runs only in the daemon process
+
+Example usage:
+
+ .. code-block:: Python
+
+ import time
+ from pywikibot.daemonize import daemonize
+
+ def background_task():
+ while True:
+ print("Daemon is working...")
+ time.sleep(5)
+
+ daemonize()
+
+ # This code only runs in the daemon process
+ background_task()
+"""
#
# (C) Pywikibot team, 2007-2025
#
@@ -7,11 +57,15 @@
from __future__ import annotations
import os
+import platform
import stat
import sys
+from contextlib import suppress
from enum import IntEnum
from pathlib import Path
+from pywikibot.tools import deprecate_positionals
+
class StandardFD(IntEnum):
@@ -25,7 +79,9 @@
is_daemon = False
-def daemonize(close_fd: bool = True,
+@deprecate_positionals(since='10.6.0')
+def daemonize(*,
+ close_fd: bool = True,
chdir: bool = True,
redirect_std: str | None = None) -> None:
"""Daemonize the current process.
@@ -33,11 +89,27 @@
Only works on POSIX compatible operating systems. The process will
fork to the background and return control to terminal.
+ .. versionchanged:: 10.6
+ raises NotImplementedError instead of AttributeError if daemonize
+ is not available for the given platform. Parameters must be given
+ as keyword-only arguments.
+
+ .. caution::
+ Do not use it in multithreaded scripts or in a subinterpreter.
+
:param close_fd: Close the standard streams and replace them by
/dev/null
:param chdir: Change the current working directory to /
:param redirect_std: Filename to redirect stdout and stdin to
+ :raises RuntimeError: Must not be run in a subinterpreter
+ :raises NotImplementedError: Daemon mode not supported on given
+ platform
"""
+ # platform check for MyPy
+ if not hasattr(os, 'fork') or sys.platform == 'win32':
+ msg = f'Daemon mode not supported on {platform.system()}'
+ raise NotImplementedError(msg)
+
# Fork away
if not os.fork():
# Become session leader
@@ -50,9 +122,12 @@
global is_daemon
is_daemon = True
+ # Optionally close and redirect standard streams
if close_fd:
for fd in StandardFD:
- os.close(fd)
+ with suppress(OSError):
+ os.close(fd)
+
os.open('/dev/null', os.O_RDWR)
if redirect_std:
@@ -67,9 +142,11 @@
os.dup2(StandardFD.STDIN, StandardFD.STDOUT)
os.dup2(StandardFD.STDOUT, StandardFD.STDERR)
+ # Optionally change working directory
if chdir:
os.chdir('/')
- return
+
+ return # Daemon continues here
# Write out the pid
path = Path(Path(sys.argv[0]).name).with_suffix('.pid')
--
To view, visit
https://gerrit.wikimedia.org/r/c/pywikibot/core/+/1191813?usp=email
To unsubscribe, or for help writing mail filters, visit
https://gerrit.wikimedia.org/r/settings?usp=email
Gerrit-MessageType: merged
Gerrit-Project: pywikibot/core
Gerrit-Branch: master
Gerrit-Change-Id: I50ae24b2567c092add345a842ae0c2035a2be0e4
Gerrit-Change-Number: 1191813
Gerrit-PatchSet: 2
Gerrit-Owner: Xqt <[email protected]>
Gerrit-Reviewer: Xqt <[email protected]>
Gerrit-Reviewer: jenkins-bot
_______________________________________________
Pywikibot-commits mailing list -- [email protected]
To unsubscribe send an email to [email protected]