3 # Copyright (C) 2022 Jérémie Galarneau <jeremie.galarneau@efficios.com>
5 # SPDX-License-Identifier: GPL-2.0-only
8 from types
import FrameType
9 from typing
import Callable
, Iterator
, Optional
, Tuple
, List
, Generator
25 class TemporaryDirectory
:
26 def __init__(self
, prefix
):
28 self
._directory
_path
= tempfile
.mkdtemp(prefix
=prefix
)
31 shutil
.rmtree(self
._directory
_path
, ignore_errors
=True)
35 # type: () -> pathlib.Path
36 return pathlib
.Path(self
._directory
_path
)
39 class _SignalWaitQueue
:
41 Utility class useful to wait for a signal before proceeding.
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.
47 signal.signal(signal.SIGWHATEVER, queue.signal)
49 Waiting for the signal:
50 queue.wait_for_signal()
54 self
._queue
= queue
.Queue() # type: queue.Queue
59 frame
, # type: Optional[FrameType]
61 self
._queue
.put_nowait(signal_number
)
63 def wait_for_signal(self
):
64 self
._queue
.get(block
=True)
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
)
74 # Restore the original signal handler and forward the exception.
77 signal
.signal(signal_number
, original_handler
)
80 class _WaitTraceTestApplication
:
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.
89 binary_path
, # type: pathlib.Path
90 event_count
, # type: int
91 environment
, # type: Environment
92 wait_time_between_events_us
=0, # type: int
93 wait_before_exit
=False, # type: bool
94 wait_before_exit_file_path
=None, # type: Optional[pathlib.Path]
97 self
._environment
= environment
# type: Environment
98 self
._iteration
_count
= event_count
99 # File that the application will wait to see before tracing its events.
100 self
._app
_start
_tracing
_file
_path
= pathlib
.Path(
103 suffix
="_start_tracing",
104 dir=self
._compat
_pathlike
(environment
.lttng_home_location
),
107 # File that the application will create when all events have been emitted.
108 self
._app
_tracing
_done
_file
_path
= pathlib
.Path(
111 suffix
="_done_tracing",
112 dir=self
._compat
_pathlike
(environment
.lttng_home_location
),
116 if wait_before_exit
and wait_before_exit_file_path
is None:
117 wait_before_exit_file_path
= pathlib
.Path(
121 dir=self
._compat
_pathlike
(environment
.lttng_home_location
),
125 self
._has
_returned
= False
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"
133 # File that the application will create to indicate it has completed its initialization.
134 app_ready_file_path
= tempfile
.mktemp(
137 dir=self
._compat
_pathlike
(environment
.lttng_home_location
),
140 test_app_args
= [str(binary_path
)]
141 test_app_args
.extend(["--iter", str(event_count
)])
142 test_app_args
.extend(
143 ["--sync-application-in-main-touch", str(app_ready_file_path
)]
145 test_app_args
.extend(
146 ["--sync-before-first-event", str(self
._app
_start
_tracing
_file
_path
)]
148 test_app_args
.extend(
149 ["--sync-before-exit-touch", str(self
._app
_tracing
_done
_file
_path
)]
151 if wait_time_between_events_us
!= 0:
152 test_app_args
.extend(["--wait", str(wait_time_between_events_us
)])
154 self
._process
= subprocess
.Popen(
157 stdout
=subprocess
.PIPE
,
158 stderr
=subprocess
.STDOUT
,
159 ) # type: subprocess.Popen
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
164 self
._wait
_for
_file
_to
_be
_created
(pathlib
.Path(app_ready_file_path
))
166 def _wait_for_file_to_be_created(self
, sync_file_path
):
167 # type: (pathlib.Path) -> None
169 if os
.path
.exists(self
._compat
_pathlike
(sync_file_path
)):
172 if self
._process
.poll() is not None:
173 # Application has unexepectedly returned.
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
184 if self
._process
.poll() is not None:
185 # Application has unexepectedly returned.
187 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
188 return_code
=self
._process
.returncode
191 open(self
._compat
_pathlike
(self
._app
_start
_tracing
_file
_path
), mode
="x")
193 def wait_for_tracing_done(self
):
195 self
._wait
_for
_file
_to
_be
_created
(self
._app
_tracing
_done
_file
_path
)
197 def wait_for_exit(self
):
199 if self
._process
.wait() != 0:
201 "Test application has exit with return code `{return_code}`".format(
202 return_code
=self
._process
.returncode
205 self
._has
_returned
= True
210 return self
._process
.pid
213 def _compat_pathlike(path
):
214 # type: (pathlib.Path) -> pathlib.Path | str
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.
221 if hasattr(path
, "__fspath__"):
227 if self
._process
is not None and not self
._has
_returned
:
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.
234 class WaitTraceTestApplicationGroup
:
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
243 self
._wait
_before
_exit
_file
_path
= (
248 dir=_WaitTraceTestApplication
._compat
_pathlike
(
249 environment
.lttng_home_location
259 for i
in range(application_count
):
260 new_app
= environment
.launch_wait_trace_test_application(
262 wait_time_between_events_us
,
264 self
._wait
_before
_exit
_file
_path
,
267 # Attach an output consumer to log the application's error output (if any).
268 if environment
._logging
_function
:
269 app_output_consumer
= ProcessOutputConsumer(
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
)
278 self
._apps
.append(new_app
)
282 for app
in self
._apps
:
286 self
, wait_for_apps
=False # type: bool
288 if self
._wait
_before
_exit
_file
_path
is None:
290 "Can't call exit on an application group created with `wait_before_exit=False`"
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()
299 _WaitTraceTestApplication
._compat
_pathlike
(
300 self
._wait
_before
_exit
_file
_path
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.
307 for app
in self
._apps
:
311 class _TraceTestApplication
:
313 Create an application that emits events as soon as it is launched. In most
314 scenarios, it is preferable to use a WaitTraceTestApplication.
317 def __init__(self
, binary_path
, environment
):
318 # type: (pathlib.Path, Environment)
320 self
._environment
= environment
# type: Environment
321 self
._has
_returned
= False
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"
329 test_app_args
= [str(binary_path
)]
331 self
._process
= subprocess
.Popen(
332 test_app_args
, env
=test_app_env
333 ) # type: subprocess.Popen
335 def wait_for_exit(self
):
337 if self
._process
.wait() != 0:
339 "Test application has exit with return code `{return_code}`".format(
340 return_code
=self
._process
.returncode
343 self
._has
_returned
= True
346 if self
._process
is not None and not self
._has
_returned
:
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.
353 class ProcessOutputConsumer(threading
.Thread
, logger
._Logger
):
356 process
, # type: subprocess.Popen
358 log
, # type: Callable[[str], None]
360 threading
.Thread
.__init
__(self
)
362 logger
._Logger
.__init
__(self
, log
)
363 self
._process
= process
367 while self
._process
.poll() is None:
368 assert self
._process
.stdout
369 line
= self
._process
.stdout
.readline().decode("utf-8").replace("\n", "")
371 self
._log
("{prefix}: {line}".format(prefix
=self
._prefix
, line
=line
))
374 # Generate a temporary environment in which to execute a test.
375 class _Environment(logger
._Logger
):
378 with_sessiond
, # type: bool
379 log
=None, # type: Optional[Callable[[str], None]]
380 with_relayd
=False, # type: bool
382 super().__init
__(log
)
383 signal
.signal(signal
.SIGTERM
, self
._handle
_termination
_signal
)
384 signal
.signal(signal
.SIGINT
, self
._handle
_termination
_signal
)
386 # Assumes the project's hierarchy to this file is:
387 # tests/utils/python/this_file
388 self
._project
_root
= (
389 pathlib
.Path(__file__
).absolute().parents
[3]
390 ) # type: pathlib.Path
391 self
._lttng
_home
= TemporaryDirectory(
392 "lttng_test_env_home"
393 ) # type: Optional[TemporaryDirectory]
396 self
._launch
_lttng
_relayd
() if with_relayd
else None
397 ) # type: Optional[subprocess.Popen[bytes]]
398 self
._relayd
_output
_consumer
= None
401 self
._launch
_lttng
_sessiond
() if with_sessiond
else None
402 ) # type: Optional[subprocess.Popen[bytes]]
405 def lttng_home_location(self
):
406 # type: () -> pathlib.Path
407 if self
._lttng
_home
is None:
408 raise RuntimeError("Attempt to access LTTng home after clean-up")
409 return self
._lttng
_home
.path
412 def lttng_client_path(self
):
413 # type: () -> pathlib.Path
414 return self
._project
_root
/ "src" / "bin" / "lttng" / "lttng"
417 def lttng_relayd_control_port(self
):
422 def lttng_relayd_data_port(self
):
427 def lttng_relayd_live_port(self
):
431 def create_temporary_directory(self
, prefix
=None):
432 # type: (Optional[str]) -> pathlib.Path
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
438 prefix
="tmp" if prefix
is None else prefix
,
439 dir=str(self
._lttng
_home
.path
),
443 # Unpack a list of environment variables from a string
444 # such as "HELLO=is_it ME='/you/are/looking/for'"
446 def _unpack_env_vars(env_vars_string
):
447 # type: (str) -> List[Tuple[str, str]]
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:
454 "Invalid sessiond environment variable: `{}`".format(var
)
457 var_name
= var
[0:equal_position
]
458 var_value
= var
[equal_position
+ 1 :]
460 var_value
= var_value
.replace("'", "")
461 var_value
= var_value
.replace('"', "")
462 unpacked_vars
.append((var_name
, var_value
))
466 def _launch_lttng_relayd(self
):
467 # type: () -> Optional[subprocess.Popen]
469 self
._project
_root
/ "src" / "bin" / "lttng-relayd" / "lttng-relayd"
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.
476 relayd_env_vars
= os
.environ
.get("LTTNG_RELAYD_ENV_VARS")
477 relayd_env
= os
.environ
.copy()
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
484 assert self
._lttng
_home
is not None
485 relayd_env
["LTTNG_HOME"] = str(self
._lttng
_home
.path
)
487 "Launching relayd with LTTNG_HOME='${}'".format(str(self
._lttng
_home
.path
))
489 process
= subprocess
.Popen(
493 "tcp://0.0.0.0:{}".format(self
.lttng_relayd_control_port
),
495 "tcp://0.0.0.0:{}".format(self
.lttng_relayd_data_port
),
497 "tcp://localhost:{}".format(self
.lttng_relayd_live_port
),
499 stdout
=subprocess
.PIPE
,
500 stderr
=subprocess
.STDOUT
,
504 if self
._logging
_function
:
505 self
._relayd
_output
_consumer
= ProcessOutputConsumer(
506 process
, "lttng-relayd", self
._logging
_function
508 self
._relayd
_output
_consumer
.daemon
= True
509 self
._relayd
_output
_consumer
.start()
513 def _launch_lttng_sessiond(self
):
514 # type: () -> Optional[subprocess.Popen]
515 is_64bits_host
= sys
.maxsize
> 2**32
518 self
._project
_root
/ "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
520 consumerd_path_option_name
= "--consumerd{bitness}-path".format(
521 bitness
="64" if is_64bits_host
else "32"
524 self
._project
_root
/ "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
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.
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
543 sessiond_env
["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
544 self
._project
_root
/ "src" / "common"
547 assert self
._lttng
_home
is not None
548 sessiond_env
["LTTNG_HOME"] = str(self
._lttng
_home
.path
)
550 wait_queue
= _SignalWaitQueue()
551 with wait_queue
.intercept_signal(signal
.SIGUSR1
):
553 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
554 home_dir
=str(self
._lttng
_home
.path
)
557 process
= subprocess
.Popen(
560 consumerd_path_option_name
,
564 stdout
=subprocess
.PIPE
,
565 stderr
=subprocess
.STDOUT
,
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()
576 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
577 wait_queue
.wait_for_signal()
581 def _handle_termination_signal(self
, signal_number
, frame
):
582 # type: (int, Optional[FrameType]) -> None
584 "Killed by {signal_name} signal, cleaning-up".format(
585 signal_name
=signal
.strsignal(signal_number
)
590 def launch_wait_trace_test_application(
592 event_count
, # type: int
593 wait_time_between_events_us
=0,
594 wait_before_exit
=False,
595 wait_before_exit_file_path
=None,
597 # type: (int, int, bool, Optional[pathlib.Path]) -> _WaitTraceTestApplication
599 Launch an application that will wait before tracing `event_count` events.
601 return _WaitTraceTestApplication(
610 wait_time_between_events_us
,
612 wait_before_exit_file_path
,
615 def launch_test_application(self
, subpath
):
616 # type () -> TraceTestApplication
618 Launch an application that will trace from within constructors.
620 return _TraceTestApplication(
621 self
._project
_root
/ "tests" / "utils" / "testapp" / subpath
,
625 def _terminate_relayd(self
):
626 if self
._relayd
and self
._relayd
.poll() is None:
627 self
._relayd
.terminate()
629 if self
._relayd
_output
_consumer
:
630 self
._relayd
_output
_consumer
.join()
631 self
._relayd
_output
_consumer
= None
632 self
._log
("Relayd killed")
635 # Clean-up managed processes
638 if self
._sessiond
and self
._sessiond
.poll() is None:
639 # The session daemon is alive; kill it.
641 "Killing session daemon (pid = {sessiond_pid})".format(
642 sessiond_pid
=self
._sessiond
.pid
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
652 self
._log
("Session daemon killed")
653 self
._sessiond
= None
655 self
._terminate
_relayd
()
657 self
._lttng
_home
= None
663 @contextlib.contextmanager
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
)