Commit | Line | Data |
---|---|---|
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 | ||
8 | from types import FrameType | |
0ac0f70e | 9 | from typing import Callable, Iterator, Optional, Tuple, List, Generator |
ef945e4d JG |
10 | import sys |
11 | import pathlib | |
03775d44 KS |
12 | import pwd |
13 | import random | |
ef945e4d | 14 | import signal |
91118dcc | 15 | import socket |
ef945e4d JG |
16 | import subprocess |
17 | import shlex | |
18 | import shutil | |
03775d44 KS |
19 | import stat |
20 | import string | |
ef945e4d JG |
21 | import os |
22 | import queue | |
23 | import tempfile | |
24 | from . import logger | |
25 | import time | |
26 | import threading | |
27 | import contextlib | |
28 | ||
91118dcc KS |
29 | import bt2 |
30 | ||
ef945e4d JG |
31 | |
32 | class TemporaryDirectory: | |
ce8470c9 MJ |
33 | def __init__(self, prefix): |
34 | # type: (str) -> None | |
ef945e4d JG |
35 | self._directory_path = tempfile.mkdtemp(prefix=prefix) |
36 | ||
37 | def __del__(self): | |
03775d44 KS |
38 | if os.getenv("LTTNG_TEST_PRESERVE_TEST_ENV", "0") != "1": |
39 | shutil.rmtree(self._directory_path, ignore_errors=True) | |
ef945e4d JG |
40 | |
41 | @property | |
ce8470c9 MJ |
42 | def path(self): |
43 | # type: () -> pathlib.Path | |
ef945e4d JG |
44 | return pathlib.Path(self._directory_path) |
45 | ||
46 | ||
47 | class _SignalWaitQueue: | |
48 | """ | |
49 | Utility class useful to wait for a signal before proceeding. | |
50 | ||
51 | Simply register the `signal` method as the handler for the signal you are | |
52 | interested in and call `wait_for_signal` to wait for its reception. | |
53 | ||
54 | Registering a signal: | |
55 | signal.signal(signal.SIGWHATEVER, queue.signal) | |
56 | ||
57 | Waiting for the signal: | |
58 | queue.wait_for_signal() | |
59 | """ | |
60 | ||
61 | def __init__(self): | |
ce8470c9 | 62 | self._queue = queue.Queue() # type: queue.Queue |
ef945e4d | 63 | |
ce8470c9 MJ |
64 | def signal( |
65 | self, | |
66 | signal_number, | |
67 | frame, # type: Optional[FrameType] | |
68 | ): | |
ef945e4d JG |
69 | self._queue.put_nowait(signal_number) |
70 | ||
71 | def wait_for_signal(self): | |
72 | self._queue.get(block=True) | |
73 | ||
0ac0f70e JG |
74 | @contextlib.contextmanager |
75 | def intercept_signal(self, signal_number): | |
76 | # type: (int) -> Generator[None, None, None] | |
77 | original_handler = signal.getsignal(signal_number) | |
78 | signal.signal(signal_number, self.signal) | |
79 | try: | |
80 | yield | |
81 | except: | |
82 | # Restore the original signal handler and forward the exception. | |
83 | raise | |
84 | finally: | |
85 | signal.signal(signal_number, original_handler) | |
86 | ||
ef945e4d | 87 | |
91118dcc KS |
88 | class _LiveViewer: |
89 | """ | |
90 | Create a babeltrace2 live viewer. | |
91 | """ | |
92 | ||
93 | def __init__( | |
94 | self, | |
95 | environment, # type: Environment | |
96 | session, # type: str | |
97 | hostname=None, # type: Optional[str] | |
98 | ): | |
99 | self._environment = environment | |
100 | self._session = session | |
101 | self._hostname = hostname | |
102 | if self._hostname is None: | |
103 | self._hostname = socket.gethostname() | |
104 | self._events = [] | |
105 | ||
106 | ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"] | |
107 | self._live_iterator = bt2.TraceCollectionMessageIterator( | |
108 | bt2.ComponentSpec( | |
109 | ctf_live_cc, | |
110 | { | |
111 | "inputs": [ | |
112 | "net://localhost:{}/host/{}/{}".format( | |
113 | environment.lttng_relayd_live_port, | |
114 | self._hostname, | |
115 | session, | |
116 | ) | |
117 | ], | |
118 | "session-not-found-action": "end", | |
119 | }, | |
120 | ) | |
121 | ) | |
122 | ||
123 | try: | |
124 | # Cause the connection to be initiated since tests | |
125 | # tend to wait for a viewer to be connected before proceeding. | |
126 | msg = next(self._live_iterator) | |
127 | self._events.append(msg) | |
128 | except bt2.TryAgain: | |
129 | pass | |
130 | ||
131 | @property | |
132 | def output(self): | |
133 | return self._events | |
134 | ||
135 | @property | |
136 | def messages(self): | |
137 | return [x for x in self._events if type(x) is bt2._EventMessageConst] | |
138 | ||
139 | def _drain(self, retry=False): | |
140 | while True: | |
141 | try: | |
142 | for msg in self._live_iterator: | |
c6183a33 KS |
143 | if type(msg) is bt2._MessageIteratorInactivityMessageConst: |
144 | break | |
91118dcc KS |
145 | self._events.append(msg) |
146 | break | |
147 | except bt2.TryAgain as e: | |
148 | if retry: | |
149 | time.sleep(0.01) | |
150 | continue | |
151 | else: | |
152 | break | |
153 | ||
b0c95099 | 154 | def is_connected(self): |
91118dcc KS |
155 | ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"] |
156 | self._environment._log( | |
157 | "Checking for connected clients at 'net://localhost:{}'".format( | |
158 | self._environment.lttng_relayd_live_port | |
159 | ) | |
160 | ) | |
161 | query_executor = bt2.QueryExecutor( | |
162 | ctf_live_cc, | |
163 | "sessions", | |
164 | params={ | |
165 | "url": "net://localhost:{}".format( | |
166 | self._environment.lttng_relayd_live_port | |
167 | ) | |
168 | }, | |
169 | ) | |
b0c95099 KS |
170 | |
171 | for live_session in query_executor.query(): | |
172 | if ( | |
173 | live_session["session-name"] == self._session | |
174 | and live_session["client-count"] >= 1 | |
175 | ): | |
176 | self._environment._log( | |
177 | "Session '{}' has {} connected clients".format( | |
178 | live_session["session-name"], live_session["client-count"] | |
179 | ) | |
180 | ) | |
181 | return True | |
182 | return False | |
183 | ||
7b874e71 KS |
184 | def _wait_until(self, desired_state: bool, timeout=0): |
185 | connected_state = not desired_state | |
91118dcc | 186 | started = time.time() |
7b874e71 | 187 | while connected_state != desired_state: |
91118dcc KS |
188 | try: |
189 | if timeout != 0 and (time.time() - started) > timeout: | |
190 | raise RuntimeError( | |
191 | "Timed out waiting for connected clients on session '{}' after {}s".format( | |
192 | self._session, time.time() - started | |
193 | ) | |
194 | ) | |
b0c95099 | 195 | |
7b874e71 | 196 | connected_state = self.is_connected() |
91118dcc KS |
197 | except bt2._Error: |
198 | time.sleep(0.01) | |
199 | continue | |
7b874e71 KS |
200 | return connected_state |
201 | ||
202 | def wait_until_disconnected(self, timeout=0): | |
203 | return self._wait_until(False, timeout) | |
204 | ||
205 | def wait_until_connected(self, timeout=0): | |
206 | return self._wait_until(True, timeout) | |
91118dcc KS |
207 | |
208 | def wait(self): | |
209 | if self._live_iterator: | |
210 | self._drain(retry=True) | |
211 | del self._live_iterator | |
212 | self._live_iterator = None | |
213 | ||
214 | def __del__(self): | |
215 | pass | |
216 | ||
217 | ||
c661f2f4 | 218 | class _WaitTraceTestApplication: |
ef945e4d JG |
219 | """ |
220 | Create an application that waits before tracing. This allows a test to | |
221 | launch an application, get its PID, and get it to start tracing when it | |
222 | has completed its setup. | |
223 | """ | |
224 | ||
225 | def __init__( | |
226 | self, | |
ce8470c9 MJ |
227 | binary_path, # type: pathlib.Path |
228 | event_count, # type: int | |
229 | environment, # type: Environment | |
230 | wait_time_between_events_us=0, # type: int | |
c661f2f4 JG |
231 | wait_before_exit=False, # type: bool |
232 | wait_before_exit_file_path=None, # type: Optional[pathlib.Path] | |
03775d44 | 233 | run_as=None, # type: Optional[str] |
ef945e4d | 234 | ): |
cebde614 | 235 | self._process = None |
ce8470c9 | 236 | self._environment = environment # type: Environment |
ef07b7ae | 237 | self._iteration_count = event_count |
ef945e4d | 238 | # File that the application will wait to see before tracing its events. |
11ababae KS |
239 | dir = ( |
240 | self._compat_pathlike(environment.lttng_home_location) | |
241 | if environment.lttng_home_location | |
242 | else None | |
243 | ) | |
03775d44 KS |
244 | if run_as is not None: |
245 | dir = os.path.join(dir, run_as) | |
ce8470c9 | 246 | self._app_start_tracing_file_path = pathlib.Path( |
ef945e4d JG |
247 | tempfile.mktemp( |
248 | prefix="app_", | |
249 | suffix="_start_tracing", | |
03775d44 | 250 | dir=dir, |
ef945e4d JG |
251 | ) |
252 | ) | |
03775d44 | 253 | |
c661f2f4 JG |
254 | # File that the application will create when all events have been emitted. |
255 | self._app_tracing_done_file_path = pathlib.Path( | |
256 | tempfile.mktemp( | |
257 | prefix="app_", | |
258 | suffix="_done_tracing", | |
03775d44 | 259 | dir=dir, |
c661f2f4 JG |
260 | ) |
261 | ) | |
262 | ||
263 | if wait_before_exit and wait_before_exit_file_path is None: | |
264 | wait_before_exit_file_path = pathlib.Path( | |
265 | tempfile.mktemp( | |
266 | prefix="app_", | |
267 | suffix="_exit", | |
c0aaf21b | 268 | dir=dir, |
c661f2f4 JG |
269 | ) |
270 | ) | |
c0aaf21b | 271 | self._wait_before_exit_file_path = wait_before_exit_file_path |
ef945e4d | 272 | self._has_returned = False |
c0aaf21b | 273 | self._tracing_started = False |
ef945e4d JG |
274 | |
275 | test_app_env = os.environ.copy() | |
11ababae KS |
276 | if environment.lttng_home_location is not None: |
277 | test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location) | |
ef945e4d JG |
278 | # Make sure the app is blocked until it is properly registered to |
279 | # the session daemon. | |
280 | test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1" | |
281 | ||
282 | # File that the application will create to indicate it has completed its initialization. | |
8466f071 | 283 | app_ready_file_path = tempfile.mktemp( |
2d2198ca MJ |
284 | prefix="app_", |
285 | suffix="_ready", | |
03775d44 | 286 | dir=dir, |
ce8470c9 | 287 | ) # type: str |
ef945e4d JG |
288 | |
289 | test_app_args = [str(binary_path)] | |
c661f2f4 | 290 | test_app_args.extend(["--iter", str(event_count)]) |
ef945e4d | 291 | test_app_args.extend( |
c661f2f4 JG |
292 | ["--sync-application-in-main-touch", str(app_ready_file_path)] |
293 | ) | |
294 | test_app_args.extend( | |
295 | ["--sync-before-first-event", str(self._app_start_tracing_file_path)] | |
296 | ) | |
297 | test_app_args.extend( | |
298 | ["--sync-before-exit-touch", str(self._app_tracing_done_file_path)] | |
ef945e4d | 299 | ) |
c0aaf21b KS |
300 | if wait_before_exit: |
301 | test_app_args.extend( | |
302 | ["--sync-before-exit", str(self._wait_before_exit_file_path)] | |
303 | ) | |
c661f2f4 JG |
304 | if wait_time_between_events_us != 0: |
305 | test_app_args.extend(["--wait", str(wait_time_between_events_us)]) | |
ef945e4d | 306 | |
03775d44 KS |
307 | if run_as is not None: |
308 | # When running as root and reducing the permissions to run as another | |
309 | # user, the test binary needs to be readable and executable by the | |
310 | # world; however, the file may be in a deep path or on systems where | |
311 | # we don't want to modify the filesystem state (eg. for a person who | |
312 | # has downloaded and ran the tests manually). | |
313 | # Therefore, the binary_path is copied to a temporary file in the | |
314 | # `run_as` user's home directory | |
315 | new_binary_path = os.path.join( | |
316 | str(environment.lttng_home_location), | |
317 | run_as, | |
318 | os.path.basename(str(binary_path)), | |
319 | ) | |
320 | ||
321 | if not os.path.exists(new_binary_path): | |
322 | shutil.copy(str(binary_path), new_binary_path) | |
323 | ||
324 | test_app_args[0] = new_binary_path | |
325 | ||
326 | lib_dir = environment.lttng_home_location / run_as / "lib" | |
327 | if not os.path.isdir(str(lib_dir)): | |
328 | os.mkdir(str(lib_dir)) | |
329 | # When running dropping privileges, the libraries built in the | |
330 | # root-owned directories may not be reachable and readable by | |
331 | # the loader running as an unprivileged user. These should also be | |
332 | # copied. | |
333 | _ldd = subprocess.Popen( | |
334 | ["ldd", new_binary_path], | |
335 | stdout=subprocess.PIPE, | |
336 | stderr=subprocess.PIPE, | |
337 | ) | |
338 | if _ldd.wait() != 0: | |
339 | raise RuntimeError( | |
340 | "Error while using `ldd` to determine test application dependencies: `{}`".format( | |
341 | stderr.read().decode("utf-8") | |
342 | ) | |
343 | ) | |
344 | libs = [ | |
345 | x.decode("utf-8").split(sep="=>") for x in _ldd.stdout.readlines() | |
346 | ] | |
347 | libs = [ | |
348 | x[1].split(sep=" ")[1] | |
349 | for x in libs | |
350 | if len(x) >= 2 and x[1].find("lttng") != -1 | |
351 | ] | |
352 | for lib in libs: | |
353 | shutil.copy(lib, lib_dir) | |
354 | ||
355 | test_app_env["LD_LIBRARY_PATH"] = "{}:{}".format( | |
356 | test_app_env["LD_LIBRARY_PATH"], | |
357 | str(lib_dir), | |
358 | ) | |
359 | ||
360 | # As of python 3.9, subprocess.Popen supports a user parameter which | |
361 | # runs `setreuid()` before executing the proces and will be preferable | |
362 | # when support for older python versions is no longer required. | |
363 | test_app_args = [ | |
364 | "runuser", | |
365 | "-u", | |
366 | run_as, | |
367 | "--", | |
368 | ] + test_app_args | |
369 | ||
370 | self._environment._log( | |
371 | "Launching test application: '{}'".format( | |
372 | self._compat_shlex_join(test_app_args) | |
373 | ) | |
374 | ) | |
ce8470c9 | 375 | self._process = subprocess.Popen( |
ef945e4d JG |
376 | test_app_args, |
377 | env=test_app_env, | |
c661f2f4 | 378 | stdout=subprocess.PIPE, |
91118dcc | 379 | stderr=subprocess.PIPE, |
ce8470c9 | 380 | ) # type: subprocess.Popen |
ef945e4d JG |
381 | |
382 | # Wait for the application to create the file indicating it has fully | |
383 | # initialized. Make sure the app hasn't crashed in order to not wait | |
384 | # forever. | |
c661f2f4 JG |
385 | self._wait_for_file_to_be_created(pathlib.Path(app_ready_file_path)) |
386 | ||
387 | def _wait_for_file_to_be_created(self, sync_file_path): | |
388 | # type: (pathlib.Path) -> None | |
ef945e4d | 389 | while True: |
8a5e3824 | 390 | if os.path.exists(self._compat_pathlike(sync_file_path)): |
ef945e4d JG |
391 | break |
392 | ||
393 | if self._process.poll() is not None: | |
394 | # Application has unexepectedly returned. | |
395 | raise RuntimeError( | |
03775d44 KS |
396 | "Test application has unexepectedly returned while waiting for synchronization file to be created: sync_file=`{sync_file}`, return_code=`{return_code}`, output=`{output}`".format( |
397 | sync_file=sync_file_path, | |
398 | return_code=self._process.returncode, | |
399 | output=self._process.stderr.read().decode("utf-8"), | |
ef945e4d JG |
400 | ) |
401 | ) | |
402 | ||
c661f2f4 | 403 | time.sleep(0.001) |
ef945e4d | 404 | |
c0aaf21b KS |
405 | def touch_exit_file(self): |
406 | open(self._compat_pathlike(self._wait_before_exit_file_path), mode="x") | |
407 | ||
ce8470c9 MJ |
408 | def trace(self): |
409 | # type: () -> None | |
ef945e4d JG |
410 | if self._process.poll() is not None: |
411 | # Application has unexepectedly returned. | |
412 | raise RuntimeError( | |
413 | "Test application has unexepectedly before tracing with return code `{return_code}`".format( | |
414 | return_code=self._process.returncode | |
415 | ) | |
416 | ) | |
8a5e3824 | 417 | open(self._compat_pathlike(self._app_start_tracing_file_path), mode="x") |
c0aaf21b KS |
418 | self._environment._log("[{}] Tracing started".format(self.vpid)) |
419 | self._tracing_started = True | |
ef945e4d | 420 | |
c661f2f4 JG |
421 | def wait_for_tracing_done(self): |
422 | # type: () -> None | |
c0aaf21b KS |
423 | if not self._tracing_started: |
424 | raise RuntimeError("Tracing hasn't been started") | |
c661f2f4 | 425 | self._wait_for_file_to_be_created(self._app_tracing_done_file_path) |
c0aaf21b | 426 | self._environment._log("[{}] Tracing done".format(self.vpid)) |
c661f2f4 | 427 | |
ce8470c9 MJ |
428 | def wait_for_exit(self): |
429 | # type: () -> None | |
ef945e4d JG |
430 | if self._process.wait() != 0: |
431 | raise RuntimeError( | |
03775d44 KS |
432 | "Test application [{pid}] has exit with return code `{return_code}`, output=`{output}`".format( |
433 | pid=self.vpid, | |
434 | return_code=self._process.returncode, | |
435 | output=self._process.stderr.read().decode("utf-8"), | |
ef945e4d JG |
436 | ) |
437 | ) | |
438 | self._has_returned = True | |
439 | ||
440 | @property | |
ce8470c9 MJ |
441 | def vpid(self): |
442 | # type: () -> int | |
ef945e4d JG |
443 | return self._process.pid |
444 | ||
2d2198ca | 445 | @staticmethod |
8a5e3824 | 446 | def _compat_pathlike(path): |
ce8470c9 | 447 | # type: (pathlib.Path) -> pathlib.Path | str |
2d2198ca | 448 | """ |
8a5e3824 MJ |
449 | The builtin open() and many methods of the 'os' library in Python >= 3.6 |
450 | expect a path-like object while prior versions expect a string or | |
451 | bytes object. Return the correct type based on the presence of the | |
452 | "__fspath__" attribute specified in PEP-519. | |
2d2198ca MJ |
453 | """ |
454 | if hasattr(path, "__fspath__"): | |
455 | return path | |
456 | else: | |
457 | return str(path) | |
458 | ||
03775d44 KS |
459 | @staticmethod |
460 | def _compat_shlex_join(args): | |
461 | # type: list[str] -> str | |
462 | # shlex.join was added in python 3.8 | |
463 | return " ".join([shlex.quote(x) for x in args]) | |
464 | ||
ef945e4d | 465 | def __del__(self): |
cebde614 | 466 | if self._process is not None and not self._has_returned: |
ef945e4d JG |
467 | # This is potentially racy if the pid has been recycled. However, |
468 | # we can't use pidfd_open since it is only available in python >= 3.9. | |
469 | self._process.kill() | |
470 | self._process.wait() | |
471 | ||
472 | ||
c661f2f4 JG |
473 | class WaitTraceTestApplicationGroup: |
474 | def __init__( | |
475 | self, | |
476 | environment, # type: Environment | |
477 | application_count, # type: int | |
478 | event_count, # type: int | |
479 | wait_time_between_events_us=0, # type: int | |
480 | wait_before_exit=False, # type: bool | |
481 | ): | |
482 | self._wait_before_exit_file_path = ( | |
483 | pathlib.Path( | |
484 | tempfile.mktemp( | |
485 | prefix="app_group_", | |
486 | suffix="_exit", | |
8a5e3824 | 487 | dir=_WaitTraceTestApplication._compat_pathlike( |
c661f2f4 JG |
488 | environment.lttng_home_location |
489 | ), | |
490 | ) | |
491 | ) | |
492 | if wait_before_exit | |
493 | else None | |
494 | ) | |
495 | ||
496 | self._apps = [] | |
497 | self._consumers = [] | |
498 | for i in range(application_count): | |
499 | new_app = environment.launch_wait_trace_test_application( | |
500 | event_count, | |
501 | wait_time_between_events_us, | |
502 | wait_before_exit, | |
503 | self._wait_before_exit_file_path, | |
504 | ) | |
505 | ||
506 | # Attach an output consumer to log the application's error output (if any). | |
507 | if environment._logging_function: | |
508 | app_output_consumer = ProcessOutputConsumer( | |
509 | new_app._process, | |
510 | "app-{}".format(str(new_app.vpid)), | |
511 | environment._logging_function, | |
512 | ) # type: Optional[ProcessOutputConsumer] | |
513 | app_output_consumer.daemon = True | |
514 | app_output_consumer.start() | |
515 | self._consumers.append(app_output_consumer) | |
516 | ||
517 | self._apps.append(new_app) | |
518 | ||
519 | def trace(self): | |
520 | # type: () -> None | |
521 | for app in self._apps: | |
522 | app.trace() | |
523 | ||
524 | def exit( | |
525 | self, wait_for_apps=False # type: bool | |
526 | ): | |
527 | if self._wait_before_exit_file_path is None: | |
528 | raise RuntimeError( | |
529 | "Can't call exit on an application group created with `wait_before_exit=False`" | |
530 | ) | |
531 | ||
532 | # Wait for apps to have produced all of their events so that we can | |
533 | # cause the death of all apps to happen within a short time span. | |
534 | for app in self._apps: | |
535 | app.wait_for_tracing_done() | |
536 | ||
c0aaf21b KS |
537 | self._apps[0].touch_exit_file() |
538 | ||
c661f2f4 JG |
539 | # Performed in two passes to allow tests to stress the unregistration of many applications. |
540 | # Waiting for each app to exit turn-by-turn would defeat the purpose here. | |
541 | if wait_for_apps: | |
542 | for app in self._apps: | |
543 | app.wait_for_exit() | |
544 | ||
545 | ||
546 | class _TraceTestApplication: | |
da1e97c9 | 547 | """ |
e88109fc JG |
548 | Create an application that emits events as soon as it is launched. In most |
549 | scenarios, it is preferable to use a WaitTraceTestApplication. | |
da1e97c9 MD |
550 | """ |
551 | ||
873d3601 MJ |
552 | def __init__(self, binary_path, environment): |
553 | # type: (pathlib.Path, Environment) | |
cebde614 | 554 | self._process = None |
873d3601 | 555 | self._environment = environment # type: Environment |
da1e97c9 MD |
556 | self._has_returned = False |
557 | ||
558 | test_app_env = os.environ.copy() | |
559 | test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location) | |
560 | # Make sure the app is blocked until it is properly registered to | |
561 | # the session daemon. | |
562 | test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1" | |
563 | ||
564 | test_app_args = [str(binary_path)] | |
565 | ||
47ddc6e5 | 566 | self._process = subprocess.Popen( |
da1e97c9 | 567 | test_app_args, env=test_app_env |
47ddc6e5 | 568 | ) # type: subprocess.Popen |
da1e97c9 | 569 | |
873d3601 MJ |
570 | def wait_for_exit(self): |
571 | # type: () -> None | |
da1e97c9 MD |
572 | if self._process.wait() != 0: |
573 | raise RuntimeError( | |
574 | "Test application has exit with return code `{return_code}`".format( | |
575 | return_code=self._process.returncode | |
576 | ) | |
577 | ) | |
578 | self._has_returned = True | |
579 | ||
580 | def __del__(self): | |
cebde614 | 581 | if self._process is not None and not self._has_returned: |
da1e97c9 MD |
582 | # This is potentially racy if the pid has been recycled. However, |
583 | # we can't use pidfd_open since it is only available in python >= 3.9. | |
584 | self._process.kill() | |
585 | self._process.wait() | |
586 | ||
587 | ||
ef945e4d JG |
588 | class ProcessOutputConsumer(threading.Thread, logger._Logger): |
589 | def __init__( | |
ce8470c9 MJ |
590 | self, |
591 | process, # type: subprocess.Popen | |
592 | name, # type: str | |
593 | log, # type: Callable[[str], None] | |
ef945e4d JG |
594 | ): |
595 | threading.Thread.__init__(self) | |
596 | self._prefix = name | |
597 | logger._Logger.__init__(self, log) | |
598 | self._process = process | |
599 | ||
ce8470c9 MJ |
600 | def run(self): |
601 | # type: () -> None | |
ef945e4d JG |
602 | while self._process.poll() is None: |
603 | assert self._process.stdout | |
604 | line = self._process.stdout.readline().decode("utf-8").replace("\n", "") | |
605 | if len(line) != 0: | |
606 | self._log("{prefix}: {line}".format(prefix=self._prefix, line=line)) | |
607 | ||
608 | ||
91118dcc KS |
609 | class SavingProcessOutputConsumer(ProcessOutputConsumer): |
610 | def __init__(self, process, name, log): | |
611 | self._lines = [] | |
612 | super().__init__(process=process, name=name, log=log) | |
613 | ||
614 | def run(self): | |
615 | # type: () -> None | |
616 | while self._process.poll() is None: | |
617 | assert self._process.stdout | |
618 | line = self._process.stdout.readline().decode("utf-8").replace("\n", "") | |
619 | if len(line) != 0: | |
620 | self._lines.append(line) | |
621 | self._log("{prefix}: {line}".format(prefix=self._prefix, line=line)) | |
622 | ||
623 | @property | |
624 | def output(self): | |
625 | return self._lines | |
626 | ||
627 | ||
ef945e4d JG |
628 | # Generate a temporary environment in which to execute a test. |
629 | class _Environment(logger._Logger): | |
630 | def __init__( | |
ce8470c9 MJ |
631 | self, |
632 | with_sessiond, # type: bool | |
633 | log=None, # type: Optional[Callable[[str], None]] | |
45ce5eed | 634 | with_relayd=False, # type: bool |
11ababae KS |
635 | extra_env_vars=dict(), # type: dict |
636 | skip_temporary_lttng_home=False, # type: bool | |
ef945e4d JG |
637 | ): |
638 | super().__init__(log) | |
639 | signal.signal(signal.SIGTERM, self._handle_termination_signal) | |
640 | signal.signal(signal.SIGINT, self._handle_termination_signal) | |
641 | ||
55aec41f KS |
642 | if os.getenv("LTTNG_TEST_VERBOSE_BABELTRACE", "0") == "1": |
643 | # @TODO: Is there a way to feed the logging output to | |
644 | # the logger._Logger instead of directly to stderr? | |
645 | bt2.set_global_logging_level(bt2.LoggingLevel.TRACE) | |
646 | ||
ef945e4d JG |
647 | # Assumes the project's hierarchy to this file is: |
648 | # tests/utils/python/this_file | |
ce8470c9 MJ |
649 | self._project_root = ( |
650 | pathlib.Path(__file__).absolute().parents[3] | |
651 | ) # type: pathlib.Path | |
03775d44 | 652 | |
11ababae KS |
653 | self._extra_env_vars = extra_env_vars |
654 | ||
655 | # There are times when we need to exercise default configurations | |
656 | # that don't set LTTNG_HOME. When doing this, it makes it impossible | |
657 | # to safely run parallel tests. | |
658 | self._lttng_home = None | |
659 | if not skip_temporary_lttng_home: | |
660 | self._lttng_home = TemporaryDirectory( | |
661 | "lttng_test_env_home" | |
662 | ) # type: Optional[TemporaryDirectory] | |
663 | os.chmod( | |
664 | str(self._lttng_home.path), | |
665 | stat.S_IRUSR | |
666 | | stat.S_IWUSR | |
667 | | stat.S_IXUSR | |
668 | | stat.S_IROTH | |
669 | | stat.S_IXOTH, | |
670 | ) | |
ef945e4d | 671 | |
45ce5eed KS |
672 | self._relayd = ( |
673 | self._launch_lttng_relayd() if with_relayd else None | |
674 | ) # type: Optional[subprocess.Popen[bytes]] | |
675 | self._relayd_output_consumer = None | |
676 | ||
ce8470c9 | 677 | self._sessiond = ( |
ef945e4d | 678 | self._launch_lttng_sessiond() if with_sessiond else None |
ce8470c9 | 679 | ) # type: Optional[subprocess.Popen[bytes]] |
ef945e4d | 680 | |
03775d44 KS |
681 | self._dummy_users = {} # type: Dictionary[int, string] |
682 | self._preserve_test_env = os.getenv("LTTNG_TEST_PRESERVE_TEST_ENV", "0") != "1" | |
683 | ||
ef945e4d | 684 | @property |
ce8470c9 MJ |
685 | def lttng_home_location(self): |
686 | # type: () -> pathlib.Path | |
11ababae KS |
687 | if self._lttng_home is not None: |
688 | return self._lttng_home.path | |
689 | return None | |
ef945e4d JG |
690 | |
691 | @property | |
ce8470c9 MJ |
692 | def lttng_client_path(self): |
693 | # type: () -> pathlib.Path | |
ef945e4d JG |
694 | return self._project_root / "src" / "bin" / "lttng" / "lttng" |
695 | ||
45ce5eed KS |
696 | @property |
697 | def lttng_relayd_control_port(self): | |
698 | # type: () -> int | |
699 | return 5400 | |
700 | ||
701 | @property | |
702 | def lttng_relayd_data_port(self): | |
703 | # type: () -> int | |
704 | return 5401 | |
705 | ||
706 | @property | |
707 | def lttng_relayd_live_port(self): | |
708 | # type: () -> int | |
709 | return 5402 | |
710 | ||
03775d44 KS |
711 | @property |
712 | def preserve_test_env(self): | |
713 | # type: () -> bool | |
714 | return self._preserve_test_env | |
715 | ||
716 | @staticmethod | |
717 | def allows_destructive(): | |
718 | # type: () -> bool | |
719 | return os.getenv("LTTNG_ENABLE_DESTRUCTIVE_TESTS", "") == "will-break-my-system" | |
720 | ||
721 | def create_dummy_user(self): | |
722 | # type: () -> (int, str) | |
723 | # Create a dummy user. The uid and username will be eturned in a tuple. | |
724 | # If the name already exists, an exception will be thrown. | |
725 | # The users will be removed when the environment is cleaned up. | |
726 | name = "".join([random.choice(string.ascii_lowercase) for x in range(10)]) | |
727 | ||
728 | try: | |
729 | entry = pwd.getpwnam(name) | |
730 | raise Exception("User '{}' already exists".format(name)) | |
731 | except KeyError: | |
732 | pass | |
733 | ||
734 | # Create user | |
735 | proc = subprocess.Popen( | |
736 | [ | |
737 | "useradd", | |
738 | "--base-dir", | |
739 | str(self._lttng_home.path), | |
740 | "--create-home", | |
741 | "--no-user-group", | |
742 | "--shell", | |
743 | "/bin/sh", | |
744 | name, | |
745 | ] | |
746 | ) | |
747 | proc.wait() | |
748 | if proc.returncode != 0: | |
749 | raise Exception( | |
750 | "Failed to create user '{}', useradd returned {}".format( | |
751 | name, proc.returncode | |
752 | ) | |
753 | ) | |
754 | ||
755 | entry = pwd.getpwnam(name) | |
756 | self._dummy_users[entry[2]] = name | |
757 | return (entry[2], name) | |
758 | ||
ce8470c9 MJ |
759 | def create_temporary_directory(self, prefix=None): |
760 | # type: (Optional[str]) -> pathlib.Path | |
ef945e4d JG |
761 | # Simply return a path that is contained within LTTNG_HOME; it will |
762 | # be destroyed when the temporary home goes out of scope. | |
ef945e4d JG |
763 | return pathlib.Path( |
764 | tempfile.mkdtemp( | |
765 | prefix="tmp" if prefix is None else prefix, | |
11ababae | 766 | dir=str(self.lttng_home_location) if self.lttng_home_location else None, |
ef945e4d JG |
767 | ) |
768 | ) | |
769 | ||
11ababae KS |
770 | @staticmethod |
771 | def run_kernel_tests(): | |
772 | # type: () -> bool | |
773 | return ( | |
774 | os.getenv("LTTNG_TOOLS_DISABLE_KERNEL_TESTS", "0") != "1" | |
775 | and os.getuid() == 0 | |
776 | ) | |
777 | ||
ef945e4d JG |
778 | # Unpack a list of environment variables from a string |
779 | # such as "HELLO=is_it ME='/you/are/looking/for'" | |
780 | @staticmethod | |
ce8470c9 MJ |
781 | def _unpack_env_vars(env_vars_string): |
782 | # type: (str) -> List[Tuple[str, str]] | |
ef945e4d JG |
783 | unpacked_vars = [] |
784 | for var in shlex.split(env_vars_string): | |
785 | equal_position = var.find("=") | |
786 | # Must have an equal sign and not end with an equal sign | |
787 | if equal_position == -1 or equal_position == len(var) - 1: | |
788 | raise ValueError( | |
789 | "Invalid sessiond environment variable: `{}`".format(var) | |
790 | ) | |
791 | ||
792 | var_name = var[0:equal_position] | |
793 | var_value = var[equal_position + 1 :] | |
794 | # Unquote any paths | |
795 | var_value = var_value.replace("'", "") | |
796 | var_value = var_value.replace('"', "") | |
797 | unpacked_vars.append((var_name, var_value)) | |
798 | ||
799 | return unpacked_vars | |
800 | ||
45ce5eed KS |
801 | def _launch_lttng_relayd(self): |
802 | # type: () -> Optional[subprocess.Popen] | |
803 | relayd_path = ( | |
804 | self._project_root / "src" / "bin" / "lttng-relayd" / "lttng-relayd" | |
805 | ) | |
806 | if os.environ.get("LTTNG_TEST_NO_RELAYD", "0") == "1": | |
807 | # Run without a relay daemon; the user may be running one | |
808 | # under gdb, for example. | |
809 | return None | |
810 | ||
811 | relayd_env_vars = os.environ.get("LTTNG_RELAYD_ENV_VARS") | |
812 | relayd_env = os.environ.copy() | |
11ababae | 813 | relayd_env.update(self._extra_env_vars) |
45ce5eed KS |
814 | if relayd_env_vars: |
815 | self._log("Additional lttng-relayd environment variables:") | |
816 | for name, value in self._unpack_env_vars(relayd_env_vars): | |
817 | self._log("{}={}".format(name, value)) | |
818 | relayd_env[name] = value | |
819 | ||
11ababae KS |
820 | if self.lttng_home_location is not None: |
821 | relayd_env["LTTNG_HOME"] = str(self.lttng_home_location) | |
45ce5eed | 822 | self._log( |
11ababae KS |
823 | "Launching relayd with LTTNG_HOME='${}'".format( |
824 | str(self.lttng_home_location) | |
825 | ) | |
45ce5eed | 826 | ) |
55aec41f KS |
827 | verbose = [] |
828 | if os.environ.get("LTTNG_TEST_VERBOSE_RELAYD") is not None: | |
829 | verbose = ["-vvv"] | |
45ce5eed KS |
830 | process = subprocess.Popen( |
831 | [ | |
832 | str(relayd_path), | |
833 | "-C", | |
834 | "tcp://0.0.0.0:{}".format(self.lttng_relayd_control_port), | |
835 | "-D", | |
836 | "tcp://0.0.0.0:{}".format(self.lttng_relayd_data_port), | |
837 | "-L", | |
838 | "tcp://localhost:{}".format(self.lttng_relayd_live_port), | |
55aec41f KS |
839 | ] |
840 | + verbose, | |
45ce5eed KS |
841 | stdout=subprocess.PIPE, |
842 | stderr=subprocess.STDOUT, | |
843 | env=relayd_env, | |
844 | ) | |
845 | ||
846 | if self._logging_function: | |
847 | self._relayd_output_consumer = ProcessOutputConsumer( | |
848 | process, "lttng-relayd", self._logging_function | |
849 | ) | |
850 | self._relayd_output_consumer.daemon = True | |
851 | self._relayd_output_consumer.start() | |
852 | ||
c1eb72c6 KS |
853 | if os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD") is not None: |
854 | subprocess.Popen( | |
855 | [ | |
856 | "gdbserver", | |
857 | "--attach", | |
858 | "localhost:{}".format( | |
859 | os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_PORT", "1025") | |
860 | ), | |
861 | str(process.pid), | |
862 | ] | |
863 | ) | |
864 | ||
865 | if os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_WAIT", ""): | |
866 | input("Waiting for user input. Press `Enter` to continue") | |
867 | else: | |
868 | subprocess.Popen( | |
869 | [ | |
870 | "gdb", | |
871 | "--batch-silent", | |
872 | "-ex", | |
873 | "target remote localhost:{}".format( | |
874 | os.environ.get("LTTNG_TEST_GDBSERVER_RELAYD_PORT", "1025") | |
875 | ), | |
876 | "-ex", | |
877 | "continue", | |
878 | "-ex", | |
879 | "disconnect", | |
880 | ] | |
881 | ) | |
882 | ||
45ce5eed KS |
883 | return process |
884 | ||
ce8470c9 MJ |
885 | def _launch_lttng_sessiond(self): |
886 | # type: () -> Optional[subprocess.Popen] | |
ef945e4d JG |
887 | is_64bits_host = sys.maxsize > 2**32 |
888 | ||
889 | sessiond_path = ( | |
890 | self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond" | |
891 | ) | |
892 | consumerd_path_option_name = "--consumerd{bitness}-path".format( | |
893 | bitness="64" if is_64bits_host else "32" | |
894 | ) | |
895 | consumerd_path = ( | |
896 | self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd" | |
897 | ) | |
898 | ||
899 | no_sessiond_var = os.environ.get("TEST_NO_SESSIOND") | |
900 | if no_sessiond_var and no_sessiond_var == "1": | |
901 | # Run test without a session daemon; the user probably | |
902 | # intends to run one under gdb for example. | |
903 | return None | |
904 | ||
905 | # Setup the session daemon's environment | |
906 | sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS") | |
907 | sessiond_env = os.environ.copy() | |
11ababae | 908 | sessiond_env.update(self._extra_env_vars) |
ef945e4d JG |
909 | if sessiond_env_vars: |
910 | self._log("Additional lttng-sessiond environment variables:") | |
911 | additional_vars = self._unpack_env_vars(sessiond_env_vars) | |
912 | for var_name, var_value in additional_vars: | |
913 | self._log(" {name}={value}".format(name=var_name, value=var_value)) | |
914 | sessiond_env[var_name] = var_value | |
915 | ||
916 | sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str( | |
917 | self._project_root / "src" / "common" | |
918 | ) | |
919 | ||
11ababae KS |
920 | if self.lttng_home_location is not None: |
921 | sessiond_env["LTTNG_HOME"] = str(self.lttng_home_location) | |
ef945e4d JG |
922 | |
923 | wait_queue = _SignalWaitQueue() | |
0ac0f70e JG |
924 | with wait_queue.intercept_signal(signal.SIGUSR1): |
925 | self._log( | |
926 | "Launching session daemon with LTTNG_HOME=`{home_dir}`".format( | |
11ababae | 927 | home_dir=str(self.lttng_home_location) |
0ac0f70e JG |
928 | ) |
929 | ) | |
55aec41f KS |
930 | verbose = [] |
931 | if os.environ.get("LTTNG_TEST_VERBOSE_SESSIOND") is not None: | |
932 | verbose = ["-vvv", "--verbose-consumer"] | |
0ac0f70e JG |
933 | process = subprocess.Popen( |
934 | [ | |
935 | str(sessiond_path), | |
936 | consumerd_path_option_name, | |
937 | str(consumerd_path), | |
938 | "--sig-parent", | |
55aec41f KS |
939 | ] |
940 | + verbose, | |
0ac0f70e JG |
941 | stdout=subprocess.PIPE, |
942 | stderr=subprocess.STDOUT, | |
943 | env=sessiond_env, | |
ef945e4d | 944 | ) |
ef945e4d | 945 | |
0ac0f70e JG |
946 | if self._logging_function: |
947 | self._sessiond_output_consumer = ProcessOutputConsumer( | |
948 | process, "lttng-sessiond", self._logging_function | |
949 | ) # type: Optional[ProcessOutputConsumer] | |
950 | self._sessiond_output_consumer.daemon = True | |
951 | self._sessiond_output_consumer.start() | |
ef945e4d | 952 | |
0ac0f70e JG |
953 | # Wait for SIGUSR1, indicating the sessiond is ready to proceed |
954 | wait_queue.wait_for_signal() | |
ef945e4d | 955 | |
c1eb72c6 KS |
956 | if os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND") is not None: |
957 | subprocess.Popen( | |
958 | [ | |
959 | "gdbserver", | |
960 | "--attach", | |
961 | "localhost:{}".format( | |
962 | os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_PORT", "1024") | |
963 | ), | |
964 | str(process.pid), | |
965 | ] | |
966 | ) | |
967 | ||
968 | if os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_WAIT", ""): | |
969 | input("Waiting for user input. Press `Enter` to continue") | |
970 | else: | |
971 | subprocess.Popen( | |
972 | [ | |
973 | "gdb", | |
974 | "--batch-silent", | |
975 | "-ex", | |
976 | "target remote localhost:{}".format( | |
977 | os.environ.get("LTTNG_TEST_GDBSERVER_SESSIOND_PORT", "1024") | |
978 | ), | |
979 | "-ex", | |
980 | "continue", | |
981 | "-ex", | |
982 | "disconnect", | |
983 | ] | |
984 | ) | |
985 | ||
ef945e4d JG |
986 | return process |
987 | ||
ce8470c9 MJ |
988 | def _handle_termination_signal(self, signal_number, frame): |
989 | # type: (int, Optional[FrameType]) -> None | |
ef945e4d JG |
990 | self._log( |
991 | "Killed by {signal_name} signal, cleaning-up".format( | |
992 | signal_name=signal.strsignal(signal_number) | |
993 | ) | |
994 | ) | |
995 | self._cleanup() | |
996 | ||
91118dcc KS |
997 | def launch_live_viewer(self, session, hostname=None): |
998 | # Make sure the relayd is ready | |
999 | ready = False | |
1000 | ctf_live_cc = bt2.find_plugin("ctf").source_component_classes["lttng-live"] | |
1001 | query_executor = bt2.QueryExecutor( | |
1002 | ctf_live_cc, | |
1003 | "sessions", | |
1004 | params={"url": "net://localhost:{}".format(self.lttng_relayd_live_port)}, | |
1005 | ) | |
1006 | while not ready: | |
1007 | try: | |
1008 | query_result = query_executor.query() | |
1009 | except bt2._Error: | |
1010 | time.sleep(0.1) | |
1011 | continue | |
1012 | for live_session in query_result: | |
1013 | if live_session["session-name"] == session: | |
1014 | ready = True | |
1015 | self._log( | |
1016 | "Session '{}' is available at net://localhost:{}".format( | |
1017 | session, self.lttng_relayd_live_port | |
1018 | ) | |
1019 | ) | |
1020 | break | |
1021 | return _LiveViewer(self, session, hostname) | |
1022 | ||
c661f2f4 JG |
1023 | def launch_wait_trace_test_application( |
1024 | self, | |
1025 | event_count, # type: int | |
1026 | wait_time_between_events_us=0, | |
1027 | wait_before_exit=False, | |
1028 | wait_before_exit_file_path=None, | |
03775d44 | 1029 | run_as=None, |
c661f2f4 | 1030 | ): |
03775d44 | 1031 | # type: (int, int, bool, Optional[pathlib.Path], Optional[str]) -> _WaitTraceTestApplication |
ef945e4d JG |
1032 | """ |
1033 | Launch an application that will wait before tracing `event_count` events. | |
1034 | """ | |
c661f2f4 | 1035 | return _WaitTraceTestApplication( |
ef945e4d JG |
1036 | self._project_root |
1037 | / "tests" | |
1038 | / "utils" | |
1039 | / "testapp" | |
ef07b7ae JG |
1040 | / "gen-ust-events" |
1041 | / "gen-ust-events", | |
ef945e4d JG |
1042 | event_count, |
1043 | self, | |
c661f2f4 JG |
1044 | wait_time_between_events_us, |
1045 | wait_before_exit, | |
1046 | wait_before_exit_file_path, | |
03775d44 | 1047 | run_as, |
ef945e4d JG |
1048 | ) |
1049 | ||
09a872ef | 1050 | def launch_test_application(self, subpath): |
873d3601 | 1051 | # type () -> TraceTestApplication |
da1e97c9 MD |
1052 | """ |
1053 | Launch an application that will trace from within constructors. | |
1054 | """ | |
c661f2f4 | 1055 | return _TraceTestApplication( |
9a28bc04 | 1056 | self._project_root / "tests" / "utils" / "testapp" / subpath, |
da1e97c9 MD |
1057 | self, |
1058 | ) | |
1059 | ||
9a28bc04 KS |
1060 | def _terminate_relayd(self): |
1061 | if self._relayd and self._relayd.poll() is None: | |
1062 | self._relayd.terminate() | |
1063 | self._relayd.wait() | |
1064 | if self._relayd_output_consumer: | |
1065 | self._relayd_output_consumer.join() | |
1066 | self._relayd_output_consumer = None | |
1067 | self._log("Relayd killed") | |
1068 | self._relayd = None | |
1069 | ||
ef945e4d | 1070 | # Clean-up managed processes |
ce8470c9 MJ |
1071 | def _cleanup(self): |
1072 | # type: () -> None | |
ef945e4d JG |
1073 | if self._sessiond and self._sessiond.poll() is None: |
1074 | # The session daemon is alive; kill it. | |
1075 | self._log( | |
1076 | "Killing session daemon (pid = {sessiond_pid})".format( | |
1077 | sessiond_pid=self._sessiond.pid | |
1078 | ) | |
1079 | ) | |
1080 | ||
1081 | self._sessiond.terminate() | |
1082 | self._sessiond.wait() | |
1083 | if self._sessiond_output_consumer: | |
1084 | self._sessiond_output_consumer.join() | |
1085 | self._sessiond_output_consumer = None | |
1086 | ||
1087 | self._log("Session daemon killed") | |
1088 | self._sessiond = None | |
1089 | ||
9a28bc04 | 1090 | self._terminate_relayd() |
45ce5eed | 1091 | |
03775d44 KS |
1092 | # The user accounts will always be deleted, but the home directories will |
1093 | # be retained unless the user has opted to preserve the test environment. | |
1094 | userdel = ["userdel"] | |
1095 | if not self.preserve_test_env: | |
1096 | userdel += ["--remove"] | |
1097 | for uid, name in self._dummy_users.items(): | |
1098 | # When subprocess is run during the interpreter teardown, ImportError | |
1099 | # may be raised; however, the commands seem to execute correctly. | |
1100 | # Eg. | |
1101 | # | |
1102 | # Exception ignored in: <function _Environment.__del__ at 0x7f2d62e3b9c0> | |
1103 | # Traceback (most recent call last): | |
1104 | # File "tests/utils/lttngtest/environment.py", line 1024, in __del__ | |
1105 | # File "tests/utils/lttngtest/environment.py", line 1016, in _cleanup | |
1106 | # File "/usr/lib/python3.11/subprocess.py", line 1026, in __init__ | |
1107 | # File "/usr/lib/python3.11/subprocess.py", line 1880, in _execute_child | |
1108 | # File "<frozen os>", line 629, in get_exec_path | |
1109 | # ImportError: sys.meta_path is None, Python is likely shutting down | |
1110 | # | |
1111 | try: | |
1112 | _proc = subprocess.Popen( | |
1113 | ["pkill", "--uid", str(uid)], stderr=subprocess.PIPE | |
1114 | ) | |
1115 | _proc.wait() | |
1116 | except ImportError: | |
1117 | pass | |
1118 | try: | |
1119 | _proc = subprocess.Popen(userdel + [name], stderr=subprocess.PIPE) | |
1120 | _proc.wait() | |
1121 | except ImportError: | |
1122 | pass | |
1123 | ||
ef945e4d JG |
1124 | self._lttng_home = None |
1125 | ||
1126 | def __del__(self): | |
1127 | self._cleanup() | |
1128 | ||
1129 | ||
1130 | @contextlib.contextmanager | |
11ababae KS |
1131 | def test_environment( |
1132 | with_sessiond, | |
1133 | log=None, | |
1134 | with_relayd=False, | |
1135 | extra_env_vars=dict(), | |
1136 | skip_temporary_lttng_home=False, | |
1137 | ): | |
45ce5eed | 1138 | # type: (bool, Optional[Callable[[str], None]], bool) -> Iterator[_Environment] |
11ababae KS |
1139 | env = _Environment( |
1140 | with_sessiond, log, with_relayd, extra_env_vars, skip_temporary_lttng_home | |
1141 | ) | |
ef945e4d JG |
1142 | try: |
1143 | yield env | |
1144 | finally: | |
1145 | env._cleanup() |