/
process.py
206 lines (167 loc) · 6.76 KB
/
process.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
import contextlib
import os
import os.path
import re
import shlex
import signal
import subprocess
import sys
from .. import __version__
from ..platformflags import is_win32, is_linux, is_freebsd, is_darwin
from ..logger import create_logger
logger = create_logger()
def daemonize():
"""Detach process from controlling terminal and run in background
Returns: old and new get_process_id tuples
"""
from ..platform import get_process_id
old_id = get_process_id()
pid = os.fork()
if pid:
os._exit(0)
os.setsid()
pid = os.fork()
if pid:
os._exit(0)
os.chdir('/')
os.close(0)
os.close(1)
os.close(2)
fd = os.open(os.devnull, os.O_RDWR)
os.dup2(fd, 0)
os.dup2(fd, 1)
os.dup2(fd, 2)
new_id = get_process_id()
return old_id, new_id
class SignalException(BaseException):
"""base class for all signal-based exceptions"""
class SigHup(SignalException):
"""raised on SIGHUP signal"""
class SigTerm(SignalException):
"""raised on SIGTERM signal"""
@contextlib.contextmanager
def signal_handler(sig, handler):
"""
when entering context, set up signal handler <handler> for signal <sig>.
when leaving context, restore original signal handler.
<sig> can bei either a str when giving a signal.SIGXXX attribute name (it
won't crash if the attribute name does not exist as some names are platform
specific) or a int, when giving a signal number.
<handler> is any handler value as accepted by the signal.signal(sig, handler).
"""
if isinstance(sig, str):
sig = getattr(signal, sig, None)
if sig is not None:
orig_handler = signal.signal(sig, handler)
try:
yield
finally:
if sig is not None:
signal.signal(sig, orig_handler)
def raising_signal_handler(exc_cls):
def handler(sig_no, frame):
# setting SIG_IGN avoids that an incoming second signal of this
# kind would raise a 2nd exception while we still process the
# exception handler for exc_cls for the 1st signal.
signal.signal(sig_no, signal.SIG_IGN)
raise exc_cls
return handler
class SigIntManager:
def __init__(self):
self._sig_int_triggered = False
self._action_triggered = False
self._action_done = False
self.ctx = signal_handler('SIGINT', self.handler)
def __bool__(self):
# this will be True (and stay True) after the first Ctrl-C/SIGINT
return self._sig_int_triggered
def action_triggered(self):
# this is True to indicate that the action shall be done
return self._action_triggered
def action_done(self):
# this will be True after the action has completed
return self._action_done
def action_completed(self):
# this must be called when the action triggered is completed,
# to avoid that the action is repeatedly triggered.
self._action_triggered = False
self._action_done = True
def handler(self, sig_no, stack):
# handle the first ctrl-c / SIGINT.
self.__exit__(None, None, None)
self._sig_int_triggered = True
self._action_triggered = True
def __enter__(self):
self.ctx.__enter__()
def __exit__(self, exception_type, exception_value, traceback):
# restore the original ctrl-c handler, so the next ctrl-c / SIGINT does the normal thing:
if self.ctx:
self.ctx.__exit__(exception_type, exception_value, traceback)
self.ctx = None
# global flag which might trigger some special behaviour on first ctrl-c / SIGINT,
# e.g. if this is interrupting "borg create", it shall try to create a checkpoint.
sig_int = SigIntManager()
def popen_with_error_handling(cmd_line: str, log_prefix='', **kwargs):
"""
Handle typical errors raised by subprocess.Popen. Return None if an error occurred,
otherwise return the Popen object.
*cmd_line* is split using shlex (e.g. 'gzip -9' => ['gzip', '-9']).
Log messages will be prefixed with *log_prefix*; if set, it should end with a space
(e.g. log_prefix='--some-option: ').
Does not change the exit code.
"""
assert not kwargs.get('shell'), 'Sorry pal, shell mode is a no-no'
try:
command = shlex.split(cmd_line)
if not command:
raise ValueError('an empty command line is not permitted')
except ValueError as ve:
logger.error('%s%s', log_prefix, ve)
return
logger.debug('%scommand line: %s', log_prefix, command)
try:
return subprocess.Popen(command, **kwargs)
except FileNotFoundError:
logger.error('%sexecutable not found: %s', log_prefix, command[0])
return
except PermissionError:
logger.error('%spermission denied: %s', log_prefix, command[0])
return
def is_terminal(fd=sys.stdout):
return hasattr(fd, 'isatty') and fd.isatty() and (not is_win32 or 'ANSICON' in os.environ)
def prepare_subprocess_env(system, env=None):
"""
Prepare the environment for a subprocess we are going to create.
:param system: True for preparing to invoke system-installed binaries,
False for stuff inside the pyinstaller environment (like borg, python).
:param env: optionally give a environment dict here. if not given, default to os.environ.
:return: a modified copy of the environment
"""
env = dict(env if env is not None else os.environ)
if system:
# a pyinstaller binary's bootloader modifies LD_LIBRARY_PATH=/tmp/_MEIXXXXXX,
# but we do not want that system binaries (like ssh or other) pick up
# (non-matching) libraries from there.
# thus we install the original LDLP, before pyinstaller has modified it:
lp_key = 'LD_LIBRARY_PATH'
lp_orig = env.get(lp_key + '_ORIG') # pyinstaller >= 20160820 / v3.2.1 has this
if lp_orig is not None:
env[lp_key] = lp_orig
else:
# We get here in 2 cases:
# 1. when not running a pyinstaller-made binary.
# in this case, we must not kill LDLP.
# 2. when running a pyinstaller-made binary and there was no LDLP
# in the original env (in this case, the pyinstaller bootloader
# does *not* put ..._ORIG into the env either).
# in this case, we must kill LDLP.
# The directory used by pyinstaller is created by mkdtemp("_MEIXXXXXX"),
# we can use that to differentiate between the cases.
lp = env.get(lp_key)
if lp is not None and re.search(r'/_MEI......', lp):
env.pop(lp_key)
# security: do not give secrets to subprocess
env.pop('BORG_PASSPHRASE', None)
# for information, give borg version to the subprocess
env['BORG_VERSION'] = __version__
return env