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 | |
12 | import signal | |
13 | import subprocess | |
14 | import shlex | |
15 | import shutil | |
16 | import os | |
17 | import queue | |
18 | import tempfile | |
19 | from . import logger | |
20 | import time | |
21 | import threading | |
22 | import contextlib | |
23 | ||
24 | ||
25 | class 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 | ||
39 | class _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 | 80 | class _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 |
234 | class 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 | ||
311 | class _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 |
353 | class 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. | |
375 | class _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 | ||
09a872ef | 615 | def launch_test_application(self, subpath): |
873d3601 | 616 | # type () -> TraceTestApplication |
da1e97c9 MD |
617 | """ |
618 | Launch an application that will trace from within constructors. | |
619 | """ | |
c661f2f4 | 620 | return _TraceTestApplication( |
9a28bc04 | 621 | self._project_root / "tests" / "utils" / "testapp" / subpath, |
da1e97c9 MD |
622 | self, |
623 | ) | |
624 | ||
9a28bc04 KS |
625 | def _terminate_relayd(self): |
626 | if self._relayd and self._relayd.poll() is None: | |
627 | self._relayd.terminate() | |
628 | self._relayd.wait() | |
629 | if self._relayd_output_consumer: | |
630 | self._relayd_output_consumer.join() | |
631 | self._relayd_output_consumer = None | |
632 | self._log("Relayd killed") | |
633 | self._relayd = None | |
634 | ||
ef945e4d | 635 | # Clean-up managed processes |
ce8470c9 MJ |
636 | def _cleanup(self): |
637 | # type: () -> None | |
ef945e4d JG |
638 | if self._sessiond and self._sessiond.poll() is None: |
639 | # The session daemon is alive; kill it. | |
640 | self._log( | |
641 | "Killing session daemon (pid = {sessiond_pid})".format( | |
642 | sessiond_pid=self._sessiond.pid | |
643 | ) | |
644 | ) | |
645 | ||
646 | self._sessiond.terminate() | |
647 | self._sessiond.wait() | |
648 | if self._sessiond_output_consumer: | |
649 | self._sessiond_output_consumer.join() | |
650 | self._sessiond_output_consumer = None | |
651 | ||
652 | self._log("Session daemon killed") | |
653 | self._sessiond = None | |
654 | ||
9a28bc04 | 655 | self._terminate_relayd() |
45ce5eed | 656 | |
ef945e4d JG |
657 | self._lttng_home = None |
658 | ||
659 | def __del__(self): | |
660 | self._cleanup() | |
661 | ||
662 | ||
663 | @contextlib.contextmanager | |
45ce5eed KS |
664 | def test_environment(with_sessiond, log=None, with_relayd=False): |
665 | # type: (bool, Optional[Callable[[str], None]], bool) -> Iterator[_Environment] | |
666 | env = _Environment(with_sessiond, log, with_relayd) | |
ef945e4d JG |
667 | try: |
668 | yield env | |
669 | finally: | |
670 | env._cleanup() |