Replace tpo git repository URL by gitlab
[stem.git] / stem / process.py
blob417a0570b847953e3c0bb72926b36c7e31555a50
1 # Copyright 2011-2020, Damian Johnson and The Tor Project
2 # See LICENSE for licensing information
4 """
5 Helper functions for working with tor as a process.
7 :NO_TORRC:
8 when provided as a torrc_path tor is ran with a blank configuration
10 :DEFAULT_INIT_TIMEOUT:
11 number of seconds before we time out our attempt to start a tor instance
13 **Module Overview:**
17 launch_tor - starts up a tor process
18 launch_tor_with_config - starts a tor process with a custom torrc
19 """
21 import os
22 import re
23 import signal
24 import subprocess
25 import tempfile
26 import threading
28 import stem.util.str_tools
29 import stem.util.system
30 import stem.version
32 from typing import Any, Callable, Dict, Optional, Sequence, Union
34 NO_TORRC = '<no torrc>'
35 DEFAULT_INIT_TIMEOUT = 90
38 def launch_tor(tor_cmd: str = 'tor', args: Optional[Sequence[str]] = None, torrc_path: Optional[str] = None, completion_percent: int = 100, init_msg_handler: Optional[Callable[[str], None]] = None, timeout: int = DEFAULT_INIT_TIMEOUT, take_ownership: bool = False, close_output: bool = True, stdin: Optional[str] = None) -> subprocess.Popen:
39 """
40 Initializes a tor process. This blocks until initialization completes or we
41 error out.
43 If tor's data directory is missing or stale then bootstrapping will include
44 making several requests to the directory authorities which can take a little
45 while. Usually this is done in 50 seconds or so, but occasionally calls seem
46 to get stuck, taking well over the default timeout.
48 **To work to must log at NOTICE runlevel to stdout.** It does this by
49 default, but if you have a 'Log' entry in your torrc then you'll also need
50 'Log NOTICE stdout'.
52 Note: The timeout argument does not work on Windows or when outside the
53 main thread, and relies on the global state of the signal module.
55 .. versionchanged:: 1.6.0
56 Allowing the timeout argument to be a float.
58 .. versionchanged:: 1.7.0
59 Added the **close_output** argument.
61 :param tor_cmd: command for starting tor
62 :param args: additional arguments for tor
63 :param torrc_path: location of the torrc for us to use
64 :param completion_percent: percent of bootstrap completion at which
65 this'll return
66 :param init_msg_handler: optional functor that will be provided with
67 tor's initialization stdout as we get it
68 :param timeout: time after which the attempt to start tor is aborted, no
69 timeouts are applied if **None**
70 :param take_ownership: asserts ownership over the tor process so it
71 aborts if this python process terminates or a :class:`~stem.control.Controller`
72 we establish to it disconnects
73 :param close_output: closes tor's stdout and stderr streams when
74 bootstrapping is complete if true
75 :param stdin: content to provide on stdin
77 :returns: **subprocess.Popen** instance for the tor subprocess
79 :raises: **OSError** if we either fail to create the tor process or reached a
80 timeout without success
81 """
83 if stem.util.system.is_windows():
84 if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
85 raise OSError('You cannot launch tor with a timeout on Windows')
87 timeout = None
88 elif threading.current_thread().__class__.__name__ != '_MainThread':
89 if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
90 raise OSError('Launching tor with a timeout can only be done in the main thread')
92 timeout = None
94 # sanity check that we got a tor binary
96 if os.path.sep in tor_cmd:
97 # got a path (either relative or absolute), check what it leads to
99 if os.path.isdir(tor_cmd):
100 raise OSError("'%s' is a directory, not the tor executable" % tor_cmd)
101 elif not os.path.isfile(tor_cmd):
102 raise OSError("'%s' doesn't exist" % tor_cmd)
103 elif not stem.util.system.is_available(tor_cmd):
104 raise OSError("'%s' isn't available on your system. Maybe it's not in your PATH?" % tor_cmd)
106 # double check that we have a torrc to work with
107 if torrc_path not in (None, NO_TORRC) and not os.path.exists(torrc_path):
108 raise OSError("torrc doesn't exist (%s)" % torrc_path)
110 # starts a tor subprocess, raising an OSError if it fails
111 runtime_args, temp_file = [tor_cmd], None
113 if args:
114 runtime_args += args
116 if torrc_path:
117 if torrc_path == NO_TORRC:
118 temp_file = tempfile.mkstemp(prefix = 'empty-torrc-', text = True)[1]
119 runtime_args += ['-f', temp_file]
120 else:
121 runtime_args += ['-f', torrc_path]
123 if take_ownership:
124 runtime_args += ['__OwningControllerProcess', str(os.getpid())]
126 tor_process = None
128 try:
129 tor_process = subprocess.Popen(runtime_args, stdout = subprocess.PIPE, stdin = subprocess.PIPE, stderr = subprocess.PIPE)
131 if stdin:
132 tor_process.stdin.write(stem.util.str_tools._to_bytes(stdin))
133 tor_process.stdin.close()
135 if timeout:
136 def timeout_handler(signum: int, frame: Any) -> None:
137 raise OSError('reached a %i second timeout without success' % timeout)
139 signal.signal(signal.SIGALRM, timeout_handler)
140 signal.setitimer(signal.ITIMER_REAL, timeout)
142 bootstrap_line = re.compile('Bootstrapped ([0-9]+)%')
143 problem_line = re.compile('\\[(warn|err)\\] (.*)$')
144 last_problem = 'Timed out'
146 while True:
147 # Tor's stdout will be read as ASCII bytes. That means it'll mismatch
148 # with other operations (for instance the bootstrap_line.search() call
149 # later will fail), so normalizing to unicode.
151 init_line = tor_process.stdout.readline().decode('utf-8', 'replace').strip()
153 # this will provide empty results if the process is terminated
155 if not init_line:
156 raise OSError('Process terminated: %s' % last_problem)
158 # provide the caller with the initialization message if they want it
160 if init_msg_handler:
161 init_msg_handler(init_line)
163 # return the process if we're done with bootstrapping
165 bootstrap_match = bootstrap_line.search(init_line)
166 problem_match = problem_line.search(init_line)
168 if bootstrap_match and int(bootstrap_match.group(1)) >= completion_percent:
169 return tor_process
170 elif problem_match:
171 runlevel, msg = problem_match.groups()
173 if 'see warnings above' not in msg:
174 if ': ' in msg:
175 msg = msg.split(': ')[-1].strip()
177 last_problem = msg
178 except:
179 if tor_process:
180 tor_process.kill() # don't leave a lingering process
181 tor_process.wait()
183 raise
184 finally:
185 if timeout:
186 signal.alarm(0) # stop alarm
188 if tor_process and close_output:
189 if tor_process.stdout:
190 tor_process.stdout.close()
192 if tor_process.stderr:
193 tor_process.stderr.close()
195 if temp_file:
196 try:
197 os.remove(temp_file)
198 except:
199 pass
202 def launch_tor_with_config(config: Dict[str, Union[str, Sequence[str]]], tor_cmd: str = 'tor', completion_percent: int = 100, init_msg_handler: Optional[Callable[[str], None]] = None, timeout: int = DEFAULT_INIT_TIMEOUT, take_ownership: bool = False, close_output: bool = True) -> subprocess.Popen:
204 Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a
205 customized configuration. This writes a temporary torrc to disk, launches
206 tor, then deletes the torrc.
208 For example...
212 tor_process = stem.process.launch_tor_with_config(
213 config = {
214 'ControlPort': '2778',
215 'Log': [
216 'NOTICE stdout',
217 'ERR file /tmp/tor_error_log',
222 .. versionchanged:: 1.7.0
223 Added the **close_output** argument.
225 :param config: configuration options, such as "{'ControlPort': '9051'}",
226 values can either be a **str** or **list of str** if for multiple values
227 :param tor_cmd: command for starting tor
228 :param completion_percent: percent of bootstrap completion at which
229 this'll return
230 :param init_msg_handler: optional functor that will be provided with
231 tor's initialization stdout as we get it
232 :param timeout: time after which the attempt to start tor is aborted, no
233 timeouts are applied if **None**
234 :param take_ownership: asserts ownership over the tor process so it
235 aborts if this python process terminates or a :class:`~stem.control.Controller`
236 we establish to it disconnects
237 :param close_output: closes tor's stdout and stderr streams when
238 bootstrapping is complete if true
240 :returns: **subprocess.Popen** instance for the tor subprocess
242 :raises: **OSError** if we either fail to create the tor process or reached a
243 timeout without success
246 # we need to be sure that we're logging to stdout to figure out when we're
247 # done bootstrapping
249 if 'Log' in config:
250 stdout_options = ['DEBUG stdout', 'INFO stdout', 'NOTICE stdout']
252 if isinstance(config['Log'], str):
253 config['Log'] = [config['Log']]
255 has_stdout = False
257 for log_config in config['Log']:
258 if log_config in stdout_options:
259 has_stdout = True
260 break
262 if not has_stdout:
263 config['Log'] = list(config['Log']) + ['NOTICE stdout']
265 config_str = ''
267 for key, values in list(config.items()):
268 if isinstance(values, str):
269 config_str += '%s %s\n' % (key, values)
270 else:
271 for value in values:
272 config_str += '%s %s\n' % (key, value)
274 return launch_tor(tor_cmd, ['-f', '-'], None, completion_percent, init_msg_handler, timeout, take_ownership, close_output, stdin = config_str)