File: process.py

package info (click to toggle)
python-stem 1.7.1-1.1
  • links: PTS, VCS
  • area: main
  • in suites: bullseye, sid
  • size: 5,768 kB
  • sloc: python: 29,441; java: 312; makefile: 125; sh: 17
file content (302 lines) | stat: -rw-r--r-- 10,070 bytes parent folder | download | duplicates (2)
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
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
# Copyright 2011-2018, Damian Johnson and The Tor Project
# See LICENSE for licensing information

"""
Helper functions for working with tor as a process.

:NO_TORRC:
  when provided as a torrc_path tor is ran with a blank configuration

:DEFAULT_INIT_TIMEOUT:
  number of seconds before we time out our attempt to start a tor instance

**Module Overview:**

::

  launch_tor             - starts up a tor process
  launch_tor_with_config - starts a tor process with a custom torrc
"""

import os
import re
import signal
import subprocess
import tempfile
import threading

import stem.prereq
import stem.util.str_tools
import stem.util.system
import stem.version

NO_TORRC = '<no torrc>'
DEFAULT_INIT_TIMEOUT = 90


def launch_tor(tor_cmd = 'tor', args = None, torrc_path = None, completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False, close_output = True, stdin = None):
  """
  Initializes a tor process. This blocks until initialization completes or we
  error out.

  If tor's data directory is missing or stale then bootstrapping will include
  making several requests to the directory authorities which can take a little
  while. Usually this is done in 50 seconds or so, but occasionally calls seem
  to get stuck, taking well over the default timeout.

  **To work to must log at NOTICE runlevel to stdout.** It does this by
  default, but if you have a 'Log' entry in your torrc then you'll also need
  'Log NOTICE stdout'.

  Note: The timeout argument does not work on Windows or when outside the
  main thread, and relies on the global state of the signal module.

  .. versionchanged:: 1.6.0
     Allowing the timeout argument to be a float.

  .. versionchanged:: 1.7.0
     Added the **close_output** argument.

  :param str tor_cmd: command for starting tor
  :param list args: additional arguments for tor
  :param str torrc_path: location of the torrc for us to use
  :param int completion_percent: percent of bootstrap completion at which
    this'll return
  :param functor init_msg_handler: optional functor that will be provided with
    tor's initialization stdout as we get it
  :param int timeout: time after which the attempt to start tor is aborted, no
    timeouts are applied if **None**
  :param bool take_ownership: asserts ownership over the tor process so it
    aborts if this python process terminates or a :class:`~stem.control.Controller`
    we establish to it disconnects
  :param bool close_output: closes tor's stdout and stderr streams when
    bootstrapping is complete if true
  :param str stdin: content to provide on stdin

  :returns: **subprocess.Popen** instance for the tor subprocess

  :raises: **OSError** if we either fail to create the tor process or reached a
    timeout without success
  """

  if stem.util.system.is_windows():
    if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
      raise OSError('You cannot launch tor with a timeout on Windows')

    timeout = None
  elif threading.current_thread().__class__.__name__ != '_MainThread':
    if timeout is not None and timeout != DEFAULT_INIT_TIMEOUT:
      raise OSError('Launching tor with a timeout can only be done in the main thread')

    timeout = None

  # sanity check that we got a tor binary

  if os.path.sep in tor_cmd:
    # got a path (either relative or absolute), check what it leads to

    if os.path.isdir(tor_cmd):
      raise OSError("'%s' is a directory, not the tor executable" % tor_cmd)
    elif not os.path.isfile(tor_cmd):
      raise OSError("'%s' doesn't exist" % tor_cmd)
  elif not stem.util.system.is_available(tor_cmd):
    raise OSError("'%s' isn't available on your system. Maybe it's not in your PATH?" % tor_cmd)

  # double check that we have a torrc to work with
  if torrc_path not in (None, NO_TORRC) and not os.path.exists(torrc_path):
    raise OSError("torrc doesn't exist (%s)" % torrc_path)

  # starts a tor subprocess, raising an OSError if it fails
  runtime_args, temp_file = [tor_cmd], None

  if args:
    runtime_args += args

  if torrc_path:
    if torrc_path == NO_TORRC:
      temp_file = tempfile.mkstemp(prefix = 'empty-torrc-', text = True)[1]
      runtime_args += ['-f', temp_file]
    else:
      runtime_args += ['-f', torrc_path]

  if take_ownership:
    runtime_args += ['__OwningControllerProcess', str(os.getpid())]

  tor_process = None

  try:
    tor_process = subprocess.Popen(runtime_args, stdout = subprocess.PIPE, stdin = subprocess.PIPE, stderr = subprocess.PIPE)

    if stdin:
      tor_process.stdin.write(stem.util.str_tools._to_bytes(stdin))
      tor_process.stdin.close()

    if timeout:
      def timeout_handler(signum, frame):
        raise OSError('reached a %i second timeout without success' % timeout)

      signal.signal(signal.SIGALRM, timeout_handler)
      signal.setitimer(signal.ITIMER_REAL, timeout)

    bootstrap_line = re.compile('Bootstrapped ([0-9]+)%')
    problem_line = re.compile('\[(warn|err)\] (.*)$')
    last_problem = 'Timed out'

    while True:
      # Tor's stdout will be read as ASCII bytes. This is fine for python 2, but
      # in python 3 that means it'll mismatch with other operations (for instance
      # the bootstrap_line.search() call later will fail).
      #
      # It seems like python 2.x is perfectly happy for this to be unicode, so
      # normalizing to that.

      init_line = tor_process.stdout.readline().decode('utf-8', 'replace').strip()

      # this will provide empty results if the process is terminated

      if not init_line:
        raise OSError('Process terminated: %s' % last_problem)

      # provide the caller with the initialization message if they want it

      if init_msg_handler:
        init_msg_handler(init_line)

      # return the process if we're done with bootstrapping

      bootstrap_match = bootstrap_line.search(init_line)
      problem_match = problem_line.search(init_line)

      if bootstrap_match and int(bootstrap_match.group(1)) >= completion_percent:
        return tor_process
      elif problem_match:
        runlevel, msg = problem_match.groups()

        if 'see warnings above' not in msg:
          if ': ' in msg:
            msg = msg.split(': ')[-1].strip()

          last_problem = msg
  except:
    if tor_process:
      tor_process.kill()  # don't leave a lingering process
      tor_process.wait()

    raise
  finally:
    if timeout:
      signal.alarm(0)  # stop alarm

    if tor_process and close_output:
      if tor_process.stdout:
        tor_process.stdout.close()

      if tor_process.stderr:
        tor_process.stderr.close()

    if temp_file:
      try:
        os.remove(temp_file)
      except:
        pass


def launch_tor_with_config(config, tor_cmd = 'tor', completion_percent = 100, init_msg_handler = None, timeout = DEFAULT_INIT_TIMEOUT, take_ownership = False, close_output = True):
  """
  Initializes a tor process, like :func:`~stem.process.launch_tor`, but with a
  customized configuration. This writes a temporary torrc to disk, launches
  tor, then deletes the torrc.

  For example...

  ::

    tor_process = stem.process.launch_tor_with_config(
      config = {
        'ControlPort': '2778',
        'Log': [
          'NOTICE stdout',
          'ERR file /tmp/tor_error_log',
        ],
      },
    )

  .. versionchanged:: 1.7.0
     Added the **close_output** argument.

  :param dict config: configuration options, such as "{'ControlPort': '9051'}",
    values can either be a **str** or **list of str** if for multiple values
  :param str tor_cmd: command for starting tor
  :param int completion_percent: percent of bootstrap completion at which
    this'll return
  :param functor init_msg_handler: optional functor that will be provided with
    tor's initialization stdout as we get it
  :param int timeout: time after which the attempt to start tor is aborted, no
    timeouts are applied if **None**
  :param bool take_ownership: asserts ownership over the tor process so it
    aborts if this python process terminates or a :class:`~stem.control.Controller`
    we establish to it disconnects
  :param bool close_output: closes tor's stdout and stderr streams when
    bootstrapping is complete if true

  :returns: **subprocess.Popen** instance for the tor subprocess

  :raises: **OSError** if we either fail to create the tor process or reached a
    timeout without success
  """

  # TODO: Drop this version check when tor 0.2.6.3 or higher is the only game
  # in town.

  try:
    use_stdin = stem.version.get_system_tor_version(tor_cmd) >= stem.version.Requirement.TORRC_VIA_STDIN
  except IOError:
    use_stdin = False

  # we need to be sure that we're logging to stdout to figure out when we're
  # done bootstrapping

  if 'Log' in config:
    stdout_options = ['DEBUG stdout', 'INFO stdout', 'NOTICE stdout']

    if isinstance(config['Log'], str):
      config['Log'] = [config['Log']]

    has_stdout = False

    for log_config in config['Log']:
      if log_config in stdout_options:
        has_stdout = True
        break

    if not has_stdout:
      config['Log'].append('NOTICE stdout')

  config_str = ''

  for key, values in list(config.items()):
    if isinstance(values, str):
      config_str += '%s %s\n' % (key, values)
    else:
      for value in values:
        config_str += '%s %s\n' % (key, value)

  if use_stdin:
    return launch_tor(tor_cmd, ['-f', '-'], None, completion_percent, init_msg_handler, timeout, take_ownership, close_output, stdin = config_str)
  else:
    torrc_descriptor, torrc_path = tempfile.mkstemp(prefix = 'torrc-', text = True)

    try:
      with open(torrc_path, 'w') as torrc_file:
        torrc_file.write(config_str)

      # prevents tor from erroring out due to a missing torrc if it gets a sighup
      args = ['__ReloadTorrcOnSIGHUP', '0']

      return launch_tor(tor_cmd, args, torrc_path, completion_percent, init_msg_handler, timeout, take_ownership)
    finally:
      try:
        os.close(torrc_descriptor)
        os.remove(torrc_path)
      except:
        pass