Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 916cf2d

Browse files
authored
re-implement daemonize (#8011)
This has long been something I've wanted to do. Basically the `Daemonize` code is both too flexible and not flexible enough, in that it offers a bunch of features that we don't use (changing UID, closing FDs in the child, logging to syslog) and doesn't offer a bunch that we could do with (redirecting stdout/err to a file instead of /dev/null; having the parent not exit until the child is running). As a first step, I've lifted the Daemonize code and removed the bits we don't use. This should be a non-functional change. Fixing everything else will come later.
1 parent 481f76c commit 916cf2d

File tree

4 files changed

+135
-14
lines changed

4 files changed

+135
-14
lines changed

changelog.d/8011.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Replace daemonize library with a local implementation.

synapse/app/_base.py

Lines changed: 3 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,6 @@
1212
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1313
# See the License for the specific language governing permissions and
1414
# limitations under the License.
15-
1615
import gc
1716
import logging
1817
import os
@@ -22,7 +21,6 @@
2221
import traceback
2322
from typing import Iterable
2423

25-
from daemonize import Daemonize
2624
from typing_extensions import NoReturn
2725

2826
from twisted.internet import defer, error, reactor
@@ -34,6 +32,7 @@
3432
from synapse.crypto import context_factory
3533
from synapse.logging.context import PreserveLoggingContext
3634
from synapse.util.async_helpers import Linearizer
35+
from synapse.util.daemonize import daemonize_process
3736
from synapse.util.rlimit import change_resource_limit
3837
from synapse.util.versionstring import get_version_string
3938

@@ -129,17 +128,8 @@ def run():
129128
if print_pidfile:
130129
print(pid_file)
131130

132-
daemon = Daemonize(
133-
app=appname,
134-
pid=pid_file,
135-
action=run,
136-
auto_close_fds=False,
137-
verbose=True,
138-
logger=logger,
139-
)
140-
daemon.start()
141-
else:
142-
run()
131+
daemonize_process(pid_file, logger)
132+
run()
143133

144134

145135
def quit_with_error(error_string: str) -> NoReturn:

synapse/python_dependencies.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@
5959
"pyyaml>=3.11",
6060
"pyasn1>=0.1.9",
6161
"pyasn1-modules>=0.0.7",
62-
"daemonize>=2.3.1",
6362
"bcrypt>=3.1.0",
6463
"pillow>=4.3.0",
6564
"sortedcontainers>=1.4.4",

synapse/util/daemonize.py

Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
1+
# -*- coding: utf-8 -*-
2+
# Copyright (c) 2012, 2013, 2014 Ilya Otyutskiy <ilya.otyutskiy@icloud.com>
3+
# Copyright 2020 The Matrix.org Foundation C.I.C.
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
17+
import atexit
18+
import fcntl
19+
import logging
20+
import os
21+
import signal
22+
import sys
23+
24+
25+
def daemonize_process(pid_file: str, logger: logging.Logger, chdir: str = "/") -> None:
26+
"""daemonize the current process
27+
28+
This calls fork(), and has the main process exit. When it returns we will be
29+
running in the child process.
30+
"""
31+
32+
# If pidfile already exists, we should read pid from there; to overwrite it, if
33+
# locking will fail, because locking attempt somehow purges the file contents.
34+
if os.path.isfile(pid_file):
35+
with open(pid_file, "r") as pid_fh:
36+
old_pid = pid_fh.read()
37+
38+
# Create a lockfile so that only one instance of this daemon is running at any time.
39+
try:
40+
lock_fh = open(pid_file, "w")
41+
except IOError:
42+
print("Unable to create the pidfile.")
43+
sys.exit(1)
44+
45+
try:
46+
# Try to get an exclusive lock on the file. This will fail if another process
47+
# has the file locked.
48+
fcntl.flock(lock_fh, fcntl.LOCK_EX | fcntl.LOCK_NB)
49+
except IOError:
50+
print("Unable to lock on the pidfile.")
51+
# We need to overwrite the pidfile if we got here.
52+
#
53+
# XXX better to avoid overwriting it, surely. this looks racey as the pid file
54+
# could be created between us trying to read it and us trying to lock it.
55+
with open(pid_file, "w") as pid_fh:
56+
pid_fh.write(old_pid)
57+
sys.exit(1)
58+
59+
# Fork, creating a new process for the child.
60+
process_id = os.fork()
61+
62+
if process_id != 0:
63+
# parent process
64+
sys.exit(0)
65+
66+
# This is the child process. Continue.
67+
68+
# Stop listening for signals that the parent process receives.
69+
# This is done by getting a new process id.
70+
# setpgrp() is an alternative to setsid().
71+
# setsid puts the process in a new parent group and detaches its controlling
72+
# terminal.
73+
74+
os.setsid()
75+
76+
# point stdin, stdout, stderr at /dev/null
77+
devnull = "/dev/null"
78+
if hasattr(os, "devnull"):
79+
# Python has set os.devnull on this system, use it instead as it might be
80+
# different than /dev/null.
81+
devnull = os.devnull
82+
83+
devnull_fd = os.open(devnull, os.O_RDWR)
84+
os.dup2(devnull_fd, 0)
85+
os.dup2(devnull_fd, 1)
86+
os.dup2(devnull_fd, 2)
87+
os.close(devnull_fd)
88+
89+
# now that we have redirected stderr to /dev/null, any uncaught exceptions will
90+
# get sent to /dev/null, so make sure we log them.
91+
#
92+
# (we don't normally expect reactor.run to raise any exceptions, but this will
93+
# also catch any other uncaught exceptions before we get that far.)
94+
95+
def excepthook(type_, value, traceback):
96+
logger.critical("Unhanded exception", exc_info=(type_, value, traceback))
97+
98+
sys.excepthook = excepthook
99+
100+
# Set umask to default to safe file permissions when running as a root daemon. 027
101+
# is an octal number which we are typing as 0o27 for Python3 compatibility.
102+
os.umask(0o27)
103+
104+
# Change to a known directory. If this isn't done, starting a daemon in a
105+
# subdirectory that needs to be deleted results in "directory busy" errors.
106+
os.chdir(chdir)
107+
108+
try:
109+
lock_fh.write("%s" % (os.getpid()))
110+
lock_fh.flush()
111+
except IOError:
112+
logger.error("Unable to write pid to the pidfile.")
113+
print("Unable to write pid to the pidfile.")
114+
sys.exit(1)
115+
116+
# write a log line on SIGTERM.
117+
def sigterm(signum, frame):
118+
logger.warning("Caught signal %s. Stopping daemon." % signum)
119+
sys.exit(0)
120+
121+
signal.signal(signal.SIGTERM, sigterm)
122+
123+
# Cleanup pid file at exit.
124+
def exit():
125+
logger.warning("Stopping daemon.")
126+
os.remove(pid_file)
127+
sys.exit(0)
128+
129+
atexit.register(exit)
130+
131+
logger.warning("Starting daemon.")

0 commit comments

Comments
 (0)