tests: Ensure `_process` is set in _TraceTestApplications
[lttng-tools.git] / tests / utils / lttngtest / environment.py
CommitLineData
ef945e4d
JG
1#!/usr/bin/env python3
2#
3# Copyright (C) 2022 Jérémie Galarneau <jeremie.galarneau@efficios.com>
4#
5# SPDX-License-Identifier: GPL-2.0-only
6#
7
8from types import FrameType
0ac0f70e 9from typing import Callable, Iterator, Optional, Tuple, List, Generator
ef945e4d
JG
10import sys
11import pathlib
12import signal
13import subprocess
14import shlex
15import shutil
16import os
17import queue
18import tempfile
19from . import logger
20import time
21import threading
22import contextlib
23
24
25class TemporaryDirectory:
ce8470c9
MJ
26 def __init__(self, prefix):
27 # type: (str) -> None
ef945e4d
JG
28 self._directory_path = tempfile.mkdtemp(prefix=prefix)
29
30 def __del__(self):
31 shutil.rmtree(self._directory_path, ignore_errors=True)
32
33 @property
ce8470c9
MJ
34 def path(self):
35 # type: () -> pathlib.Path
ef945e4d
JG
36 return pathlib.Path(self._directory_path)
37
38
39class _SignalWaitQueue:
40 """
41 Utility class useful to wait for a signal before proceeding.
42
43 Simply register the `signal` method as the handler for the signal you are
44 interested in and call `wait_for_signal` to wait for its reception.
45
46 Registering a signal:
47 signal.signal(signal.SIGWHATEVER, queue.signal)
48
49 Waiting for the signal:
50 queue.wait_for_signal()
51 """
52
53 def __init__(self):
ce8470c9 54 self._queue = queue.Queue() # type: queue.Queue
ef945e4d 55
ce8470c9
MJ
56 def signal(
57 self,
58 signal_number,
59 frame, # type: Optional[FrameType]
60 ):
ef945e4d
JG
61 self._queue.put_nowait(signal_number)
62
63 def wait_for_signal(self):
64 self._queue.get(block=True)
65
0ac0f70e
JG
66 @contextlib.contextmanager
67 def intercept_signal(self, signal_number):
68 # type: (int) -> Generator[None, None, None]
69 original_handler = signal.getsignal(signal_number)
70 signal.signal(signal_number, self.signal)
71 try:
72 yield
73 except:
74 # Restore the original signal handler and forward the exception.
75 raise
76 finally:
77 signal.signal(signal_number, original_handler)
78
ef945e4d 79
c661f2f4 80class _WaitTraceTestApplication:
ef945e4d
JG
81 """
82 Create an application that waits before tracing. This allows a test to
83 launch an application, get its PID, and get it to start tracing when it
84 has completed its setup.
85 """
86
87 def __init__(
88 self,
ce8470c9
MJ
89 binary_path, # type: pathlib.Path
90 event_count, # type: int
91 environment, # type: Environment
92 wait_time_between_events_us=0, # type: int
c661f2f4
JG
93 wait_before_exit=False, # type: bool
94 wait_before_exit_file_path=None, # type: Optional[pathlib.Path]
ef945e4d 95 ):
cebde614 96 self._process = None
ce8470c9 97 self._environment = environment # type: Environment
ef07b7ae 98 self._iteration_count = event_count
ef945e4d 99 # File that the application will wait to see before tracing its events.
ce8470c9 100 self._app_start_tracing_file_path = pathlib.Path(
ef945e4d
JG
101 tempfile.mktemp(
102 prefix="app_",
103 suffix="_start_tracing",
8a5e3824 104 dir=self._compat_pathlike(environment.lttng_home_location),
ef945e4d
JG
105 )
106 )
c661f2f4
JG
107 # File that the application will create when all events have been emitted.
108 self._app_tracing_done_file_path = pathlib.Path(
109 tempfile.mktemp(
110 prefix="app_",
111 suffix="_done_tracing",
8a5e3824 112 dir=self._compat_pathlike(environment.lttng_home_location),
c661f2f4
JG
113 )
114 )
115
116 if wait_before_exit and wait_before_exit_file_path is None:
117 wait_before_exit_file_path = pathlib.Path(
118 tempfile.mktemp(
119 prefix="app_",
120 suffix="_exit",
8a5e3824 121 dir=self._compat_pathlike(environment.lttng_home_location),
c661f2f4
JG
122 )
123 )
124
ef945e4d
JG
125 self._has_returned = False
126
127 test_app_env = os.environ.copy()
128 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
129 # Make sure the app is blocked until it is properly registered to
130 # the session daemon.
131 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
132
133 # File that the application will create to indicate it has completed its initialization.
8466f071 134 app_ready_file_path = tempfile.mktemp(
2d2198ca
MJ
135 prefix="app_",
136 suffix="_ready",
8a5e3824 137 dir=self._compat_pathlike(environment.lttng_home_location),
ce8470c9 138 ) # type: str
ef945e4d
JG
139
140 test_app_args = [str(binary_path)]
c661f2f4 141 test_app_args.extend(["--iter", str(event_count)])
ef945e4d 142 test_app_args.extend(
c661f2f4
JG
143 ["--sync-application-in-main-touch", str(app_ready_file_path)]
144 )
145 test_app_args.extend(
146 ["--sync-before-first-event", str(self._app_start_tracing_file_path)]
147 )
148 test_app_args.extend(
149 ["--sync-before-exit-touch", str(self._app_tracing_done_file_path)]
ef945e4d 150 )
c661f2f4
JG
151 if wait_time_between_events_us != 0:
152 test_app_args.extend(["--wait", str(wait_time_between_events_us)])
ef945e4d 153
ce8470c9 154 self._process = subprocess.Popen(
ef945e4d
JG
155 test_app_args,
156 env=test_app_env,
c661f2f4
JG
157 stdout=subprocess.PIPE,
158 stderr=subprocess.STDOUT,
ce8470c9 159 ) # type: subprocess.Popen
ef945e4d
JG
160
161 # Wait for the application to create the file indicating it has fully
162 # initialized. Make sure the app hasn't crashed in order to not wait
163 # forever.
c661f2f4
JG
164 self._wait_for_file_to_be_created(pathlib.Path(app_ready_file_path))
165
166 def _wait_for_file_to_be_created(self, sync_file_path):
167 # type: (pathlib.Path) -> None
ef945e4d 168 while True:
8a5e3824 169 if os.path.exists(self._compat_pathlike(sync_file_path)):
ef945e4d
JG
170 break
171
172 if self._process.poll() is not None:
173 # Application has unexepectedly returned.
174 raise RuntimeError(
c661f2f4
JG
175 "Test application has unexepectedly returned while waiting for synchronization file to be created: sync_file=`{sync_file}`, return_code=`{return_code}`".format(
176 sync_file=sync_file_path, return_code=self._process.returncode
ef945e4d
JG
177 )
178 )
179
c661f2f4 180 time.sleep(0.001)
ef945e4d 181
ce8470c9
MJ
182 def trace(self):
183 # type: () -> None
ef945e4d
JG
184 if self._process.poll() is not None:
185 # Application has unexepectedly returned.
186 raise RuntimeError(
187 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
188 return_code=self._process.returncode
189 )
190 )
8a5e3824 191 open(self._compat_pathlike(self._app_start_tracing_file_path), mode="x")
ef945e4d 192
c661f2f4
JG
193 def wait_for_tracing_done(self):
194 # type: () -> None
195 self._wait_for_file_to_be_created(self._app_tracing_done_file_path)
196
ce8470c9
MJ
197 def wait_for_exit(self):
198 # type: () -> None
ef945e4d
JG
199 if self._process.wait() != 0:
200 raise RuntimeError(
201 "Test application has exit with return code `{return_code}`".format(
202 return_code=self._process.returncode
203 )
204 )
205 self._has_returned = True
206
207 @property
ce8470c9
MJ
208 def vpid(self):
209 # type: () -> int
ef945e4d
JG
210 return self._process.pid
211
2d2198ca 212 @staticmethod
8a5e3824 213 def _compat_pathlike(path):
ce8470c9 214 # type: (pathlib.Path) -> pathlib.Path | str
2d2198ca 215 """
8a5e3824
MJ
216 The builtin open() and many methods of the 'os' library in Python >= 3.6
217 expect a path-like object while prior versions expect a string or
218 bytes object. Return the correct type based on the presence of the
219 "__fspath__" attribute specified in PEP-519.
2d2198ca
MJ
220 """
221 if hasattr(path, "__fspath__"):
222 return path
223 else:
224 return str(path)
225
ef945e4d 226 def __del__(self):
cebde614 227 if self._process is not None and not self._has_returned:
ef945e4d
JG
228 # This is potentially racy if the pid has been recycled. However,
229 # we can't use pidfd_open since it is only available in python >= 3.9.
230 self._process.kill()
231 self._process.wait()
232
233
c661f2f4
JG
234class WaitTraceTestApplicationGroup:
235 def __init__(
236 self,
237 environment, # type: Environment
238 application_count, # type: int
239 event_count, # type: int
240 wait_time_between_events_us=0, # type: int
241 wait_before_exit=False, # type: bool
242 ):
243 self._wait_before_exit_file_path = (
244 pathlib.Path(
245 tempfile.mktemp(
246 prefix="app_group_",
247 suffix="_exit",
8a5e3824 248 dir=_WaitTraceTestApplication._compat_pathlike(
c661f2f4
JG
249 environment.lttng_home_location
250 ),
251 )
252 )
253 if wait_before_exit
254 else None
255 )
256
257 self._apps = []
258 self._consumers = []
259 for i in range(application_count):
260 new_app = environment.launch_wait_trace_test_application(
261 event_count,
262 wait_time_between_events_us,
263 wait_before_exit,
264 self._wait_before_exit_file_path,
265 )
266
267 # Attach an output consumer to log the application's error output (if any).
268 if environment._logging_function:
269 app_output_consumer = ProcessOutputConsumer(
270 new_app._process,
271 "app-{}".format(str(new_app.vpid)),
272 environment._logging_function,
273 ) # type: Optional[ProcessOutputConsumer]
274 app_output_consumer.daemon = True
275 app_output_consumer.start()
276 self._consumers.append(app_output_consumer)
277
278 self._apps.append(new_app)
279
280 def trace(self):
281 # type: () -> None
282 for app in self._apps:
283 app.trace()
284
285 def exit(
286 self, wait_for_apps=False # type: bool
287 ):
288 if self._wait_before_exit_file_path is None:
289 raise RuntimeError(
290 "Can't call exit on an application group created with `wait_before_exit=False`"
291 )
292
293 # Wait for apps to have produced all of their events so that we can
294 # cause the death of all apps to happen within a short time span.
295 for app in self._apps:
296 app.wait_for_tracing_done()
297
298 open(
8a5e3824 299 _WaitTraceTestApplication._compat_pathlike(
c661f2f4
JG
300 self._wait_before_exit_file_path
301 ),
302 mode="x",
303 )
304 # Performed in two passes to allow tests to stress the unregistration of many applications.
305 # Waiting for each app to exit turn-by-turn would defeat the purpose here.
306 if wait_for_apps:
307 for app in self._apps:
308 app.wait_for_exit()
309
310
311class _TraceTestApplication:
da1e97c9 312 """
e88109fc
JG
313 Create an application that emits events as soon as it is launched. In most
314 scenarios, it is preferable to use a WaitTraceTestApplication.
da1e97c9
MD
315 """
316
873d3601
MJ
317 def __init__(self, binary_path, environment):
318 # type: (pathlib.Path, Environment)
cebde614 319 self._process = None
873d3601 320 self._environment = environment # type: Environment
da1e97c9
MD
321 self._has_returned = False
322
323 test_app_env = os.environ.copy()
324 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
325 # Make sure the app is blocked until it is properly registered to
326 # the session daemon.
327 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
328
329 test_app_args = [str(binary_path)]
330
47ddc6e5 331 self._process = subprocess.Popen(
da1e97c9 332 test_app_args, env=test_app_env
47ddc6e5 333 ) # type: subprocess.Popen
da1e97c9 334
873d3601
MJ
335 def wait_for_exit(self):
336 # type: () -> None
da1e97c9
MD
337 if self._process.wait() != 0:
338 raise RuntimeError(
339 "Test application has exit with return code `{return_code}`".format(
340 return_code=self._process.returncode
341 )
342 )
343 self._has_returned = True
344
345 def __del__(self):
cebde614 346 if self._process is not None and not self._has_returned:
da1e97c9
MD
347 # This is potentially racy if the pid has been recycled. However,
348 # we can't use pidfd_open since it is only available in python >= 3.9.
349 self._process.kill()
350 self._process.wait()
351
352
ef945e4d
JG
353class ProcessOutputConsumer(threading.Thread, logger._Logger):
354 def __init__(
ce8470c9
MJ
355 self,
356 process, # type: subprocess.Popen
357 name, # type: str
358 log, # type: Callable[[str], None]
ef945e4d
JG
359 ):
360 threading.Thread.__init__(self)
361 self._prefix = name
362 logger._Logger.__init__(self, log)
363 self._process = process
364
ce8470c9
MJ
365 def run(self):
366 # type: () -> None
ef945e4d
JG
367 while self._process.poll() is None:
368 assert self._process.stdout
369 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
370 if len(line) != 0:
371 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
372
373
374# Generate a temporary environment in which to execute a test.
375class _Environment(logger._Logger):
376 def __init__(
ce8470c9
MJ
377 self,
378 with_sessiond, # type: bool
379 log=None, # type: Optional[Callable[[str], None]]
45ce5eed 380 with_relayd=False, # type: bool
ef945e4d
JG
381 ):
382 super().__init__(log)
383 signal.signal(signal.SIGTERM, self._handle_termination_signal)
384 signal.signal(signal.SIGINT, self._handle_termination_signal)
385
386 # Assumes the project's hierarchy to this file is:
387 # tests/utils/python/this_file
ce8470c9
MJ
388 self._project_root = (
389 pathlib.Path(__file__).absolute().parents[3]
390 ) # type: pathlib.Path
391 self._lttng_home = TemporaryDirectory(
ef945e4d 392 "lttng_test_env_home"
ce8470c9 393 ) # type: Optional[TemporaryDirectory]
ef945e4d 394
45ce5eed
KS
395 self._relayd = (
396 self._launch_lttng_relayd() if with_relayd else None
397 ) # type: Optional[subprocess.Popen[bytes]]
398 self._relayd_output_consumer = None
399
ce8470c9 400 self._sessiond = (
ef945e4d 401 self._launch_lttng_sessiond() if with_sessiond else None
ce8470c9 402 ) # type: Optional[subprocess.Popen[bytes]]
ef945e4d
JG
403
404 @property
ce8470c9
MJ
405 def lttng_home_location(self):
406 # type: () -> pathlib.Path
ef945e4d
JG
407 if self._lttng_home is None:
408 raise RuntimeError("Attempt to access LTTng home after clean-up")
409 return self._lttng_home.path
410
411 @property
ce8470c9
MJ
412 def lttng_client_path(self):
413 # type: () -> pathlib.Path
ef945e4d
JG
414 return self._project_root / "src" / "bin" / "lttng" / "lttng"
415
45ce5eed
KS
416 @property
417 def lttng_relayd_control_port(self):
418 # type: () -> int
419 return 5400
420
421 @property
422 def lttng_relayd_data_port(self):
423 # type: () -> int
424 return 5401
425
426 @property
427 def lttng_relayd_live_port(self):
428 # type: () -> int
429 return 5402
430
ce8470c9
MJ
431 def create_temporary_directory(self, prefix=None):
432 # type: (Optional[str]) -> pathlib.Path
ef945e4d
JG
433 # Simply return a path that is contained within LTTNG_HOME; it will
434 # be destroyed when the temporary home goes out of scope.
435 assert self._lttng_home
436 return pathlib.Path(
437 tempfile.mkdtemp(
438 prefix="tmp" if prefix is None else prefix,
439 dir=str(self._lttng_home.path),
440 )
441 )
442
443 # Unpack a list of environment variables from a string
444 # such as "HELLO=is_it ME='/you/are/looking/for'"
445 @staticmethod
ce8470c9
MJ
446 def _unpack_env_vars(env_vars_string):
447 # type: (str) -> List[Tuple[str, str]]
ef945e4d
JG
448 unpacked_vars = []
449 for var in shlex.split(env_vars_string):
450 equal_position = var.find("=")
451 # Must have an equal sign and not end with an equal sign
452 if equal_position == -1 or equal_position == len(var) - 1:
453 raise ValueError(
454 "Invalid sessiond environment variable: `{}`".format(var)
455 )
456
457 var_name = var[0:equal_position]
458 var_value = var[equal_position + 1 :]
459 # Unquote any paths
460 var_value = var_value.replace("'", "")
461 var_value = var_value.replace('"', "")
462 unpacked_vars.append((var_name, var_value))
463
464 return unpacked_vars
465
45ce5eed
KS
466 def _launch_lttng_relayd(self):
467 # type: () -> Optional[subprocess.Popen]
468 relayd_path = (
469 self._project_root / "src" / "bin" / "lttng-relayd" / "lttng-relayd"
470 )
471 if os.environ.get("LTTNG_TEST_NO_RELAYD", "0") == "1":
472 # Run without a relay daemon; the user may be running one
473 # under gdb, for example.
474 return None
475
476 relayd_env_vars = os.environ.get("LTTNG_RELAYD_ENV_VARS")
477 relayd_env = os.environ.copy()
478 if relayd_env_vars:
479 self._log("Additional lttng-relayd environment variables:")
480 for name, value in self._unpack_env_vars(relayd_env_vars):
481 self._log("{}={}".format(name, value))
482 relayd_env[name] = value
483
484 assert self._lttng_home is not None
485 relayd_env["LTTNG_HOME"] = str(self._lttng_home.path)
486 self._log(
487 "Launching relayd with LTTNG_HOME='${}'".format(str(self._lttng_home.path))
488 )
489 process = subprocess.Popen(
490 [
491 str(relayd_path),
492 "-C",
493 "tcp://0.0.0.0:{}".format(self.lttng_relayd_control_port),
494 "-D",
495 "tcp://0.0.0.0:{}".format(self.lttng_relayd_data_port),
496 "-L",
497 "tcp://localhost:{}".format(self.lttng_relayd_live_port),
498 ],
499 stdout=subprocess.PIPE,
500 stderr=subprocess.STDOUT,
501 env=relayd_env,
502 )
503
504 if self._logging_function:
505 self._relayd_output_consumer = ProcessOutputConsumer(
506 process, "lttng-relayd", self._logging_function
507 )
508 self._relayd_output_consumer.daemon = True
509 self._relayd_output_consumer.start()
510
511 return process
512
ce8470c9
MJ
513 def _launch_lttng_sessiond(self):
514 # type: () -> Optional[subprocess.Popen]
ef945e4d
JG
515 is_64bits_host = sys.maxsize > 2**32
516
517 sessiond_path = (
518 self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
519 )
520 consumerd_path_option_name = "--consumerd{bitness}-path".format(
521 bitness="64" if is_64bits_host else "32"
522 )
523 consumerd_path = (
524 self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
525 )
526
527 no_sessiond_var = os.environ.get("TEST_NO_SESSIOND")
528 if no_sessiond_var and no_sessiond_var == "1":
529 # Run test without a session daemon; the user probably
530 # intends to run one under gdb for example.
531 return None
532
533 # Setup the session daemon's environment
534 sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS")
535 sessiond_env = os.environ.copy()
536 if sessiond_env_vars:
537 self._log("Additional lttng-sessiond environment variables:")
538 additional_vars = self._unpack_env_vars(sessiond_env_vars)
539 for var_name, var_value in additional_vars:
540 self._log(" {name}={value}".format(name=var_name, value=var_value))
541 sessiond_env[var_name] = var_value
542
543 sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
544 self._project_root / "src" / "common"
545 )
546
547 assert self._lttng_home is not None
548 sessiond_env["LTTNG_HOME"] = str(self._lttng_home.path)
549
550 wait_queue = _SignalWaitQueue()
0ac0f70e
JG
551 with wait_queue.intercept_signal(signal.SIGUSR1):
552 self._log(
553 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
554 home_dir=str(self._lttng_home.path)
555 )
556 )
557 process = subprocess.Popen(
558 [
559 str(sessiond_path),
560 consumerd_path_option_name,
561 str(consumerd_path),
562 "--sig-parent",
563 ],
564 stdout=subprocess.PIPE,
565 stderr=subprocess.STDOUT,
566 env=sessiond_env,
ef945e4d 567 )
ef945e4d 568
0ac0f70e
JG
569 if self._logging_function:
570 self._sessiond_output_consumer = ProcessOutputConsumer(
571 process, "lttng-sessiond", self._logging_function
572 ) # type: Optional[ProcessOutputConsumer]
573 self._sessiond_output_consumer.daemon = True
574 self._sessiond_output_consumer.start()
ef945e4d 575
0ac0f70e
JG
576 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
577 wait_queue.wait_for_signal()
ef945e4d
JG
578
579 return process
580
ce8470c9
MJ
581 def _handle_termination_signal(self, signal_number, frame):
582 # type: (int, Optional[FrameType]) -> None
ef945e4d
JG
583 self._log(
584 "Killed by {signal_name} signal, cleaning-up".format(
585 signal_name=signal.strsignal(signal_number)
586 )
587 )
588 self._cleanup()
589
c661f2f4
JG
590 def launch_wait_trace_test_application(
591 self,
592 event_count, # type: int
593 wait_time_between_events_us=0,
594 wait_before_exit=False,
595 wait_before_exit_file_path=None,
596 ):
597 # type: (int, int, bool, Optional[pathlib.Path]) -> _WaitTraceTestApplication
ef945e4d
JG
598 """
599 Launch an application that will wait before tracing `event_count` events.
600 """
c661f2f4 601 return _WaitTraceTestApplication(
ef945e4d
JG
602 self._project_root
603 / "tests"
604 / "utils"
605 / "testapp"
ef07b7ae
JG
606 / "gen-ust-events"
607 / "gen-ust-events",
ef945e4d
JG
608 event_count,
609 self,
c661f2f4
JG
610 wait_time_between_events_us,
611 wait_before_exit,
612 wait_before_exit_file_path,
ef945e4d
JG
613 )
614
873d3601
MJ
615 def launch_trace_test_constructor_application(self):
616 # type () -> TraceTestApplication
da1e97c9
MD
617 """
618 Launch an application that will trace from within constructors.
619 """
c661f2f4 620 return _TraceTestApplication(
da1e97c9
MD
621 self._project_root
622 / "tests"
623 / "utils"
624 / "testapp"
625 / "gen-ust-events-constructor"
626 / "gen-ust-events-constructor",
627 self,
628 )
629
ef945e4d 630 # Clean-up managed processes
ce8470c9
MJ
631 def _cleanup(self):
632 # type: () -> None
ef945e4d
JG
633 if self._sessiond and self._sessiond.poll() is None:
634 # The session daemon is alive; kill it.
635 self._log(
636 "Killing session daemon (pid = {sessiond_pid})".format(
637 sessiond_pid=self._sessiond.pid
638 )
639 )
640
641 self._sessiond.terminate()
642 self._sessiond.wait()
643 if self._sessiond_output_consumer:
644 self._sessiond_output_consumer.join()
645 self._sessiond_output_consumer = None
646
647 self._log("Session daemon killed")
648 self._sessiond = None
649
45ce5eed
KS
650 if self._relayd and self._relayd.poll() is None:
651 self._relayd.terminate()
652 self._relayd.wait()
653 if self._relayd_output_consumer:
654 self._relayd_output_consumer.join()
655 self._relayd_output_consumer = None
656 self._log("Relayd killed")
657 self._relayd = None
658
ef945e4d
JG
659 self._lttng_home = None
660
661 def __del__(self):
662 self._cleanup()
663
664
665@contextlib.contextmanager
45ce5eed
KS
666def test_environment(with_sessiond, log=None, with_relayd=False):
667 # type: (bool, Optional[Callable[[str], None]], bool) -> Iterator[_Environment]
668 env = _Environment(with_sessiond, log, with_relayd)
ef945e4d
JG
669 try:
670 yield env
671 finally:
672 env._cleanup()
This page took 0.059949 seconds and 4 git commands to generate.