blob: c1f5f7655968ddfac8d8e7906a9148a95f9af165 [file] [log] [blame]
Takuto Ikuta38ebd0e2022-01-19 17:56:221#!/usr/bin/env vpython3
[email protected]0a88a652012-03-09 00:34:452# Copyright (c) 2012 The Chromium Authors. All rights reserved.
3# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5
Maksim Sisoveb30eff2020-03-11 09:25:586"""Runs tests with Xvfb and Openbox or Weston on Linux and normally on other
7 platforms."""
[email protected]0a88a652012-03-09 00:34:458
Luke Zielinskib57f7da2021-02-12 00:13:099from __future__ import print_function
10
Brian Sheedy78bd6f42021-03-18 00:49:5311import copy
[email protected]0a88a652012-03-09 00:34:4512import os
carloskcac17251a2017-03-15 02:21:0713import os.path
Ilia Samsonova00835302019-04-19 17:37:5914import random
Tom Andersonb9100162019-12-02 22:42:4315import re
[email protected]0a88a652012-03-09 00:34:4516import signal
17import subprocess
18import sys
msw76cf5fe12015-07-16 23:48:5719import threading
Ilia Samsonova00835302019-04-19 17:37:5920import time
Takuto Ikuta38ebd0e2022-01-19 17:56:2221
22import psutil
23
[email protected]0a88a652012-03-09 00:34:4524import test_env
25
Takuto Ikuta38ebd0e2022-01-19 17:56:2226
Ilia Samsonova00835302019-04-19 17:37:5927class _XvfbProcessError(Exception):
28 """Exception raised when Xvfb cannot start."""
29 pass
30
31
Maksim Sisoveb30eff2020-03-11 09:25:5832class _WestonProcessError(Exception):
33 """Exception raised when Weston cannot start."""
34 pass
35
36
Jochen Eisingerc322f4e92019-06-19 20:34:3237def kill(proc, name, timeout_in_seconds=10):
msw76cf5fe12015-07-16 23:48:5738 """Tries to kill |proc| gracefully with a timeout for each signal."""
Jochen Eisingerc322f4e92019-06-19 20:34:3239 if not proc:
msw76cf5fe12015-07-16 23:48:5740 return
41
Brian Sheedy1dfcb5e2021-03-30 00:43:2142 proc.terminate()
msw76cf5fe12015-07-16 23:48:5743 thread = threading.Thread(target=proc.wait)
44 thread.start()
45
46 thread.join(timeout_in_seconds)
47 if thread.is_alive():
Luke Zielinskib57f7da2021-02-12 00:13:0948 print('%s running after SIGTERM, trying SIGKILL.\n' % name, file=sys.stderr)
Jochen Eisingerc322f4e92019-06-19 20:34:3249 proc.kill()
msw76cf5fe12015-07-16 23:48:5750
51 thread.join(timeout_in_seconds)
52 if thread.is_alive():
Luke Zielinskib57f7da2021-02-12 00:13:0953 print('%s running after SIGTERM and SIGKILL; good luck!\n' % name,
54 file=sys.stderr)
msw76cf5fe12015-07-16 23:48:5755
56
Tom Andersonb9100162019-12-02 22:42:4357def launch_dbus(env):
58 """Starts a DBus session.
59
60 Works around a bug in GLib where it performs operations which aren't
61 async-signal-safe (in particular, memory allocations) between fork and exec
62 when it spawns subprocesses. This causes threads inside Chrome's browser and
63 utility processes to get stuck, and this harness to hang waiting for those
64 processes, which will never terminate. This doesn't happen on users'
65 machines, because they have an active desktop session and the
66 DBUS_SESSION_BUS_ADDRESS environment variable set, but it can happen on
67 headless environments. This is fixed by glib commit [1], but this workaround
68 will be necessary until the fix rolls into Chromium's CI.
69
Tom Anderson5cc50bc2019-12-03 20:16:2070 [1] f2917459f745bebf931bccd5cc2c33aa81ef4d12
Tom Andersonb9100162019-12-02 22:42:4371
72 Modifies the passed in environment with at least DBUS_SESSION_BUS_ADDRESS and
73 DBUS_SESSION_BUS_PID set.
Tom Anderson5cc50bc2019-12-03 20:16:2074
75 Returns the pid of the dbus-daemon if started, or None otherwise.
Tom Andersonb9100162019-12-02 22:42:4376 """
77 if 'DBUS_SESSION_BUS_ADDRESS' in os.environ:
78 return
79 try:
Stephen McGruer367e9b202021-03-19 13:32:1880 dbus_output = subprocess.check_output(
81 ['dbus-launch'], env=env).decode('utf-8').split('\n')
Tom Andersonb9100162019-12-02 22:42:4382 for line in dbus_output:
83 m = re.match(r'([^=]+)\=(.+)', line)
84 if m:
85 env[m.group(1)] = m.group(2)
Tom Anderson5cc50bc2019-12-03 20:16:2086 return int(env['DBUS_SESSION_BUS_PID'])
87 except (subprocess.CalledProcessError, OSError, KeyError, ValueError) as e:
Luke Zielinskib57f7da2021-02-12 00:13:0988 print('Exception while running dbus_launch: %s' % e)
Tom Andersonb9100162019-12-02 22:42:4389
90
Ilia Samsonova00835302019-04-19 17:37:5991# TODO(crbug.com/949194): Encourage setting flags to False.
92def run_executable(
93 cmd, env, stdoutfile=None, use_openbox=True, use_xcompmgr=True):
Maksim Sisoveb30eff2020-03-11 09:25:5894 """Runs an executable within Weston or Xvfb on Linux or normally on other
95 platforms.
msw11ac9a22015-07-14 23:36:0496
Ilia Samsonova00835302019-04-19 17:37:5997 The method sets SIGUSR1 handler for Xvfb to return SIGUSR1
98 when it is ready for connections.
99 https://www.x.org/archive/X11R7.5/doc/man/man1/Xserver.1.html under Signals.
Trent Aptedb1852432018-01-25 02:15:10100
Ilia Samsonova00835302019-04-19 17:37:59101 Args:
102 cmd: Command to be executed.
Tom Andersonb9369502021-05-19 01:06:40103 env: A copy of environment variables. "DISPLAY" and will be set if Xvfb is
104 used. "WAYLAND_DISPLAY" will be set if Weston is used.
Ilia Samsonova00835302019-04-19 17:37:59105 stdoutfile: If provided, symbolization via script is disabled and stdout
106 is written to this file as well as to stdout.
107 use_openbox: A flag to use openbox process.
108 Some ChromeOS tests need a window manager.
109 use_xcompmgr: A flag to use xcompmgr process.
110 Some tests need a compositing wm to make use of transparent visuals.
111
112 Returns:
113 the exit code of the specified commandline, or 1 on failure.
msw11ac9a22015-07-14 23:36:04114 """
dpranke7dad4682017-04-26 23:14:55115
116 # It might seem counterintuitive to support a --no-xvfb flag in a script
117 # whose only job is to start xvfb, but doing so allows us to consolidate
118 # the logic in the layers of buildbot scripts so that we *always* use
119 # xvfb by default and don't have to worry about the distinction, it
120 # can remain solely under the control of the test invocation itself.
121 use_xvfb = True
122 if '--no-xvfb' in cmd:
123 use_xvfb = False
124 cmd.remove('--no-xvfb')
125
Maksim Sisov7eea23e2019-06-12 04:54:05126 # Tests that run on Linux platforms with Ozone/Wayland backend require
Maksim Sisoveb30eff2020-03-11 09:25:58127 # a Weston instance. However, it is also required to disable xvfb so
128 # that Weston can run in a pure headless environment.
Maksim Sisov7eea23e2019-06-12 04:54:05129 use_weston = False
130 if '--use-weston' in cmd:
Maksim Sisoveb30eff2020-03-11 09:25:58131 if use_xvfb:
Luke Zielinskib57f7da2021-02-12 00:13:09132 print('Unable to use Weston with xvfb.\n', file=sys.stderr)
Maksim Sisov7eea23e2019-06-12 04:54:05133 return 1
134 use_weston = True
135 cmd.remove('--use-weston')
136
Stephen McGruer367e9b202021-03-19 13:32:18137 if sys.platform.startswith('linux') and use_xvfb:
Maksim Sisoveb30eff2020-03-11 09:25:58138 return _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr)
139 elif use_weston:
140 return _run_with_weston(cmd, env, stdoutfile)
thomasanderson8c5f7032016-11-28 22:59:16141 else:
Trent Aptedb1852432018-01-25 02:15:10142 return test_env.run_executable(cmd, env, stdoutfile)
[email protected]0a88a652012-03-09 00:34:45143
144
Maksim Sisoveb30eff2020-03-11 09:25:58145def _run_with_xvfb(cmd, env, stdoutfile, use_openbox, use_xcompmgr):
Maksim Sisoveb30eff2020-03-11 09:25:58146 openbox_proc = None
Maksim Sisov9739e172022-01-21 17:53:53147 openbox_ready = MutableBoolean()
148 def set_openbox_ready(*_):
149 openbox_ready.setvalue(True)
150
Maksim Sisoveb30eff2020-03-11 09:25:58151 xcompmgr_proc = None
152 xvfb_proc = None
153 xvfb_ready = MutableBoolean()
154 def set_xvfb_ready(*_):
155 xvfb_ready.setvalue(True)
156
Stephen McGruer367e9b202021-03-19 13:32:18157 dbus_pid = None
Maksim Sisoveb30eff2020-03-11 09:25:58158 try:
159 signal.signal(signal.SIGTERM, raise_xvfb_error)
160 signal.signal(signal.SIGINT, raise_xvfb_error)
161
Tom Anderson9ae59912021-04-13 21:35:55162 # Before [1], the maximum number of X11 clients was 256. After, the default
163 # limit is 256 with a configurable maximum of 512. On systems with a large
164 # number of CPUs, the old limit of 256 may be hit for certain test suites
165 # [2] [3], so we set the limit to 512 when possible. This flag is not
166 # available on Ubuntu 16.04 or 18.04, so a feature check is required. Xvfb
167 # does not have a '-version' option, so checking the '-help' output is
168 # required.
169 #
170 # [1] d206c240c0b85c4da44f073d6e9a692afb6b96d2
171 # [2] https://crbug.com/1187948
172 # [3] https://crbug.com/1120107
173 xvfb_help = subprocess.check_output(
174 ['Xvfb', '-help'], stderr=subprocess.STDOUT).decode('utf8')
175
Maksim Sisoveb30eff2020-03-11 09:25:58176 # Due to race condition for display number, Xvfb might fail to run.
177 # If it does fail, try again up to 10 times, similarly to xvfb-run.
178 for _ in range(10):
179 xvfb_ready.setvalue(False)
180 display = find_display()
181
Tom Anderson9ae59912021-04-13 21:35:55182 xvfb_cmd = ['Xvfb', display, '-screen', '0', '1280x800x24', '-ac',
183 '-nolisten', 'tcp', '-dpi', '96', '+extension', 'RANDR']
184 if '-maxclients' in xvfb_help:
185 xvfb_cmd += ['-maxclients', '512']
186
Maksim Sisoveb30eff2020-03-11 09:25:58187 # Sets SIGUSR1 to ignore for Xvfb to signal current process
188 # when it is ready. Due to race condition, USR1 signal could be sent
189 # before the process resets the signal handler, we cannot rely on
190 # signal handler to change on time.
191 signal.signal(signal.SIGUSR1, signal.SIG_IGN)
Tom Anderson9ae59912021-04-13 21:35:55192 xvfb_proc = subprocess.Popen(xvfb_cmd, stderr=subprocess.STDOUT, env=env)
Maksim Sisoveb30eff2020-03-11 09:25:58193 signal.signal(signal.SIGUSR1, set_xvfb_ready)
194 for _ in range(10):
195 time.sleep(.1) # gives Xvfb time to start or fail.
196 if xvfb_ready.getvalue() or xvfb_proc.poll() is not None:
197 break # xvfb sent ready signal, or already failed and stopped.
198
199 if xvfb_proc.poll() is None:
200 break # xvfb is running, can proceed.
201 if xvfb_proc.poll() is not None:
202 raise _XvfbProcessError('Failed to start after 10 tries')
203
204 env['DISPLAY'] = display
Camillo Brunie73e2eb82021-11-03 15:59:39205 # Set dummy variable for scripts.
206 env['XVFB_DISPLAY'] = display
Maksim Sisoveb30eff2020-03-11 09:25:58207
208 dbus_pid = launch_dbus(env)
209
210 if use_openbox:
Maksim Sisov9739e172022-01-21 17:53:53211 # Openbox will send a SIGUSR1 signal to the current process notifying the
212 # script it has started up.
213 current_proc_id = os.getpid()
Maksim Sisova4d1cfbe2020-06-16 07:58:37214
Maksim Sisov9739e172022-01-21 17:53:53215 # The CMD that is passed via the --startup flag.
216 openbox_startup_cmd = 'kill --signal SIGUSR1 %s' % str(current_proc_id)
217 # Setup the signal handlers before starting the openbox instance.
218 signal.signal(signal.SIGUSR1, signal.SIG_IGN)
219 signal.signal(signal.SIGUSR1, set_openbox_ready)
Maksim Sisoveb30eff2020-03-11 09:25:58220 openbox_proc = subprocess.Popen(
Maksim Sisov9739e172022-01-21 17:53:53221 ['openbox', '--sm-disable', '--startup',
222 openbox_startup_cmd], stderr=subprocess.STDOUT, env=env)
Maksim Sisoveb30eff2020-03-11 09:25:58223
Maksim Sisov9739e172022-01-21 17:53:53224 for _ in range(10):
225 time.sleep(.1) # gives Openbox time to start or fail.
226 if openbox_ready.getvalue() or openbox_proc.poll() is not None:
227 break # openbox sent ready signal, or failed and stopped.
228
229 if openbox_proc.poll() is not None:
230 raise _XvfbProcessError('Failed to start OpenBox.')
Maksim Sisova4d1cfbe2020-06-16 07:58:37231
Maksim Sisoveb30eff2020-03-11 09:25:58232 if use_xcompmgr:
233 xcompmgr_proc = subprocess.Popen(
234 'xcompmgr', stderr=subprocess.STDOUT, env=env)
235
236 return test_env.run_executable(cmd, env, stdoutfile)
237 except OSError as e:
Luke Zielinskib57f7da2021-02-12 00:13:09238 print('Failed to start Xvfb or Openbox: %s\n' % str(e), file=sys.stderr)
Maksim Sisoveb30eff2020-03-11 09:25:58239 return 1
240 except _XvfbProcessError as e:
Luke Zielinskib57f7da2021-02-12 00:13:09241 print('Xvfb fail: %s\n' % str(e), file=sys.stderr)
Maksim Sisoveb30eff2020-03-11 09:25:58242 return 1
243 finally:
244 kill(openbox_proc, 'openbox')
245 kill(xcompmgr_proc, 'xcompmgr')
246 kill(xvfb_proc, 'Xvfb')
247
248 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
249 # To ensure it exits, use SIGKILL which should be safe since all other
250 # processes that it would have been servicing have exited.
251 if dbus_pid:
252 os.kill(dbus_pid, signal.SIGKILL)
253
254
255# TODO(https://crbug.com/1060466): Write tests.
256def _run_with_weston(cmd, env, stdoutfile):
257 weston_proc = None
258
259 try:
260 signal.signal(signal.SIGTERM, raise_weston_error)
261 signal.signal(signal.SIGINT, raise_weston_error)
262
Maksim Sisov6f007d62021-03-05 18:11:33263 dbus_pid = launch_dbus(env)
264
Maksim Sisov724f3aa2021-02-18 08:28:01265 # The bundled weston (//third_party/weston) is used by Linux Ozone Wayland
266 # CI and CQ testers and compiled by //ui/ozone/platform/wayland whenever
267 # there is a dependency on the Ozone/Wayland and use_bundled_weston is set
268 # in gn args. However, some tests do not require Wayland or do not use
269 # //ui/ozone at all, but still have --use-weston flag set by the
270 # OZONE_WAYLAND variant (see //testing/buildbot/variants.pyl). This results
271 # in failures and those tests cannot be run because of the exception that
272 # informs about missing weston binary. Thus, to overcome the issue before
273 # a better solution is found, add a check for the "weston" binary here and
274 # run tests without Wayland compositor if the weston binary is not found.
275 # TODO(https://1178788): find a better solution.
276 if not os.path.isfile("./weston"):
277 print('Weston is not available. Starting without Wayland compositor')
278 return test_env.run_executable(cmd, env, stdoutfile)
279
Maksim Sisoveb30eff2020-03-11 09:25:58280 # Set $XDG_RUNTIME_DIR if it is not set.
281 _set_xdg_runtime_dir(env)
282
Brian Sheedy781c8ca42021-03-08 22:03:21283 # Weston is compiled along with the Ozone/Wayland platform, and is
284 # fetched as data deps. Thus, run it from the current directory.
285 #
286 # Weston is used with the following flags:
287 # 1) --backend=headless-backend.so - runs Weston in a headless mode
288 # that does not require a real GPU card.
289 # 2) --idle-time=0 - disables idle timeout, which prevents Weston
290 # to enter idle state. Otherwise, Weston stops to send frame callbacks,
291 # and tests start to time out (this typically happens after 300 seconds -
292 # the default time after which Weston enters the idle state).
293 # 3) --width && --height set size of a virtual display: we need to set
294 # an adequate size so that tests can have more room for managing size
295 # of windows.
296 # 4) --use-gl - Runs Weston using hardware acceleration instead of
297 # SwiftShader.
298 weston_cmd = ['./weston', '--backend=headless-backend.so', '--idle-time=0',
299 '--width=1024', '--height=768', '--modules=test-plugin.so']
300
301 if '--weston-use-gl' in cmd:
302 weston_cmd.append('--use-gl')
303 cmd.remove('--weston-use-gl')
304
Brian Sheedy78bd6f42021-03-18 00:49:53305 if '--weston-debug-logging' in cmd:
306 cmd.remove('--weston-debug-logging')
307 env = copy.deepcopy(env)
308 env['WAYLAND_DEBUG'] = '1'
309
Maksim Sisoveb30eff2020-03-11 09:25:58310 weston_proc_display = None
311 for _ in range(10):
Maksim Sisoveb30eff2020-03-11 09:25:58312 weston_proc = subprocess.Popen(
Brian Sheedy781c8ca42021-03-08 22:03:21313 weston_cmd,
Maksim Sisoveb30eff2020-03-11 09:25:58314 stderr=subprocess.STDOUT, env=env)
315
316 # Get the $WAYLAND_DISPLAY set by Weston and pass it to the test launcher.
317 # Please note that this env variable is local for the process. That's the
318 # reason we have to read it from Weston separately.
319 weston_proc_display = _get_display_from_weston(weston_proc.pid)
320 if weston_proc_display is not None:
321 break # Weston could launch and we found the display.
322
323 # If we couldn't find the display after 10 tries, raise an exception.
324 if weston_proc_display is None:
325 raise _WestonProcessError('Failed to start Weston.')
326 env['WAYLAND_DISPLAY'] = weston_proc_display
327 return test_env.run_executable(cmd, env, stdoutfile)
328 except OSError as e:
Luke Zielinskib57f7da2021-02-12 00:13:09329 print('Failed to start Weston: %s\n' % str(e), file=sys.stderr)
Maksim Sisoveb30eff2020-03-11 09:25:58330 return 1
331 except _WestonProcessError as e:
Luke Zielinskib57f7da2021-02-12 00:13:09332 print('Weston fail: %s\n' % str(e), file=sys.stderr)
Maksim Sisoveb30eff2020-03-11 09:25:58333 return 1
334 finally:
335 kill(weston_proc, 'weston')
336
Maksim Sisov6f007d62021-03-05 18:11:33337 # dbus-daemon is not a subprocess, so we can't SIGTERM+waitpid() on it.
338 # To ensure it exits, use SIGKILL which should be safe since all other
339 # processes that it would have been servicing have exited.
340 if dbus_pid:
341 os.kill(dbus_pid, signal.SIGKILL)
Maksim Sisoveb30eff2020-03-11 09:25:58342
343def _get_display_from_weston(weston_proc_pid):
344 """Retrieves $WAYLAND_DISPLAY set by Weston.
345
346 Searches for the child "weston-desktop-shell" process, takes its
347 environmental variables, and returns $WAYLAND_DISPLAY variable set
348 by that process. If the variable is not set, tries up to 10 times
349 and then gives up.
350
351 Args:
352 weston_proc_pid: The process of id of the main Weston process.
353
354 Returns:
355 the display set by Wayland, which clients can use to connect to.
356
357 TODO(https://crbug.com/1060469): This is potentially error prone
358 function. See the bug for further details.
359 """
360
Brian Sheedy298e8be2021-03-30 21:30:02361 # Try 100 times as it is not known when Weston spawn child desktop shell
362 # process. The most seen so far is ~50 checks/~2.5 seconds, but startup
363 # is usually almost instantaneous.
364 for _ in range(100):
Maksim Sisoveb30eff2020-03-11 09:25:58365 # gives weston time to start or fail.
366 time.sleep(.05)
367 # Take the parent process.
368 parent = psutil.Process(weston_proc_pid)
369 if parent is None:
370 break # The process is not found. Give up.
Maksim Sisoveb30eff2020-03-11 09:25:58371
372 # Traverse through all the children processes and find the
373 # "weston-desktop-shell" process that sets local to process env variables
374 # including the $WAYLAND_DISPLAY.
375 children = parent.children(recursive=True)
376 for process in children:
377 if process.name() == "weston-desktop-shell":
378 weston_proc_display = process.environ().get('WAYLAND_DISPLAY')
379 # If display is set, Weston could start successfully and we can use
380 # that display for Wayland connection in Chromium.
381 if weston_proc_display is not None:
382 return weston_proc_display
383 return None
384
385
Ilia Samsonova00835302019-04-19 17:37:59386class MutableBoolean(object):
387 """Simple mutable boolean class. Used to be mutated inside an handler."""
388
389 def __init__(self):
390 self._val = False
391
392 def setvalue(self, val):
393 assert isinstance(val, bool)
394 self._val = val
395
396 def getvalue(self):
397 return self._val
398
399
400def raise_xvfb_error(*_):
401 raise _XvfbProcessError('Terminated')
402
403
Maksim Sisoveb30eff2020-03-11 09:25:58404def raise_weston_error(*_):
405 raise _WestonProcessError('Terminated')
406
407
Ilia Samsonova00835302019-04-19 17:37:59408def find_display():
409 """Iterates through X-lock files to find an available display number.
410
411 The lower bound follows xvfb-run standard at 99, and the upper bound
412 is set to 119.
413
414 Returns:
415 A string of a random available display number for Xvfb ':{99-119}'.
416
417 Raises:
418 _XvfbProcessError: Raised when displays 99 through 119 are unavailable.
419 """
420
421 available_displays = [
422 d for d in range(99, 120)
423 if not os.path.isfile('/tmp/.X{}-lock'.format(d))
424 ]
425 if available_displays:
426 return ':{}'.format(random.choice(available_displays))
427 raise _XvfbProcessError('Failed to find display number')
428
429
Maksim Sisoveb30eff2020-03-11 09:25:58430def _set_xdg_runtime_dir(env):
431 """Sets the $XDG_RUNTIME_DIR variable if it hasn't been set before."""
432 runtime_dir = env.get('XDG_RUNTIME_DIR')
433 if not runtime_dir:
434 runtime_dir = '/tmp/xdg-tmp-dir/'
435 if not os.path.exists(runtime_dir):
Luke Zielinskib57f7da2021-02-12 00:13:09436 os.makedirs(runtime_dir, 0o700)
Maksim Sisoveb30eff2020-03-11 09:25:58437 env['XDG_RUNTIME_DIR'] = runtime_dir
438
439
[email protected]0a88a652012-03-09 00:34:45440def main():
Shahbaz Youssefi1b473182020-06-10 18:05:25441 usage = 'Usage: xvfb.py [command [--no-xvfb or --use-weston] args...]'
thomasanderson3d074282016-12-06 18:21:12442 if len(sys.argv) < 2:
Luke Zielinskib57f7da2021-02-12 00:13:09443 print(usage + '\n', file=sys.stderr)
[email protected]0a88a652012-03-09 00:34:45444 return 2
carloskcac17251a2017-03-15 02:21:07445
446 # If the user still thinks the first argument is the execution directory then
447 # print a friendly error message and quit.
448 if os.path.isdir(sys.argv[1]):
Luke Zielinskib57f7da2021-02-12 00:13:09449 print('Invalid command: \"%s\" is a directory\n' % sys.argv[1],
450 file=sys.stderr)
451 print(usage + '\n', file=sys.stderr)
carloskcac17251a2017-03-15 02:21:07452 return 3
453
Ilia Samsonova00835302019-04-19 17:37:59454 return run_executable(sys.argv[1:], os.environ.copy())
[email protected]0a88a652012-03-09 00:34:45455
456
Ilia Samsonova00835302019-04-19 17:37:59457if __name__ == '__main__':
[email protected]0a88a652012-03-09 00:34:45458 sys.exit(main())