Fix: truncated len in lttng_event_rule_user_tracepoint_serialize()
[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
9from typing import Callable, Optional, Tuple, List
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:
26 def __init__(self, prefix: str):
27 self._directory_path = tempfile.mkdtemp(prefix=prefix)
28
29 def __del__(self):
30 shutil.rmtree(self._directory_path, ignore_errors=True)
31
32 @property
33 def path(self) -> pathlib.Path:
34 return pathlib.Path(self._directory_path)
35
36
37class _SignalWaitQueue:
38 """
39 Utility class useful to wait for a signal before proceeding.
40
41 Simply register the `signal` method as the handler for the signal you are
42 interested in and call `wait_for_signal` to wait for its reception.
43
44 Registering a signal:
45 signal.signal(signal.SIGWHATEVER, queue.signal)
46
47 Waiting for the signal:
48 queue.wait_for_signal()
49 """
50
51 def __init__(self):
52 self._queue: queue.Queue = queue.Queue()
53
54 def signal(self, signal_number, frame: Optional[FrameType]):
55 self._queue.put_nowait(signal_number)
56
57 def wait_for_signal(self):
58 self._queue.get(block=True)
59
60
61class WaitTraceTestApplication:
62 """
63 Create an application that waits before tracing. This allows a test to
64 launch an application, get its PID, and get it to start tracing when it
65 has completed its setup.
66 """
67
68 def __init__(
69 self,
70 binary_path: pathlib.Path,
71 event_count: int,
72 environment: "Environment",
73 wait_time_between_events_us: int = 0,
74 ):
75 self._environment: Environment = environment
76 if event_count % 5:
77 # The test application currently produces 5 different events per iteration.
78 raise ValueError("event count must be a multiple of 5")
79 self._iteration_count: int = int(event_count / 5)
80 # File that the application will wait to see before tracing its events.
81 self._app_start_tracing_file_path: pathlib.Path = pathlib.Path(
82 tempfile.mktemp(
83 prefix="app_",
84 suffix="_start_tracing",
85 dir=environment.lttng_home_location,
86 )
87 )
88 self._has_returned = False
89
90 test_app_env = os.environ.copy()
91 test_app_env["LTTNG_HOME"] = str(environment.lttng_home_location)
92 # Make sure the app is blocked until it is properly registered to
93 # the session daemon.
94 test_app_env["LTTNG_UST_REGISTER_TIMEOUT"] = "-1"
95
96 # File that the application will create to indicate it has completed its initialization.
97 app_ready_file_path: str = tempfile.mktemp(
98 prefix="app_", suffix="_ready", dir=environment.lttng_home_location
99 )
100
101 test_app_args = [str(binary_path)]
102 test_app_args.extend(
103 shlex.split(
104 "--iter {iteration_count} --create-in-main {app_ready_file_path} --wait-before-first-event {app_start_tracing_file_path} --wait {wait_time_between_events_us}".format(
105 iteration_count=self._iteration_count,
106 app_ready_file_path=app_ready_file_path,
107 app_start_tracing_file_path=self._app_start_tracing_file_path,
108 wait_time_between_events_us=wait_time_between_events_us,
109 )
110 )
111 )
112
113 self._process: subprocess.Popen = subprocess.Popen(
114 test_app_args,
115 env=test_app_env,
116 )
117
118 # Wait for the application to create the file indicating it has fully
119 # initialized. Make sure the app hasn't crashed in order to not wait
120 # forever.
121 while True:
122 if os.path.exists(app_ready_file_path):
123 break
124
125 if self._process.poll() is not None:
126 # Application has unexepectedly returned.
127 raise RuntimeError(
128 "Test application has unexepectedly returned during its initialization with return code `{return_code}`".format(
129 return_code=self._process.returncode
130 )
131 )
132
133 time.sleep(0.1)
134
135 def trace(self) -> None:
136 if self._process.poll() is not None:
137 # Application has unexepectedly returned.
138 raise RuntimeError(
139 "Test application has unexepectedly before tracing with return code `{return_code}`".format(
140 return_code=self._process.returncode
141 )
142 )
143 open(self._app_start_tracing_file_path, mode="x")
144
145 def wait_for_exit(self) -> None:
146 if self._process.wait() != 0:
147 raise RuntimeError(
148 "Test application has exit with return code `{return_code}`".format(
149 return_code=self._process.returncode
150 )
151 )
152 self._has_returned = True
153
154 @property
155 def vpid(self) -> int:
156 return self._process.pid
157
158 def __del__(self):
159 if not self._has_returned:
160 # This is potentially racy if the pid has been recycled. However,
161 # we can't use pidfd_open since it is only available in python >= 3.9.
162 self._process.kill()
163 self._process.wait()
164
165
166class ProcessOutputConsumer(threading.Thread, logger._Logger):
167 def __init__(
168 self, process: subprocess.Popen, name: str, log: Callable[[str], None]
169 ):
170 threading.Thread.__init__(self)
171 self._prefix = name
172 logger._Logger.__init__(self, log)
173 self._process = process
174
175 def run(self) -> None:
176 while self._process.poll() is None:
177 assert self._process.stdout
178 line = self._process.stdout.readline().decode("utf-8").replace("\n", "")
179 if len(line) != 0:
180 self._log("{prefix}: {line}".format(prefix=self._prefix, line=line))
181
182
183# Generate a temporary environment in which to execute a test.
184class _Environment(logger._Logger):
185 def __init__(
186 self, with_sessiond: bool, log: Optional[Callable[[str], None]] = None
187 ):
188 super().__init__(log)
189 signal.signal(signal.SIGTERM, self._handle_termination_signal)
190 signal.signal(signal.SIGINT, self._handle_termination_signal)
191
192 # Assumes the project's hierarchy to this file is:
193 # tests/utils/python/this_file
194 self._project_root: pathlib.Path = pathlib.Path(__file__).absolute().parents[3]
195 self._lttng_home: Optional[TemporaryDirectory] = TemporaryDirectory(
196 "lttng_test_env_home"
197 )
198
199 self._sessiond: Optional[subprocess.Popen[bytes]] = (
200 self._launch_lttng_sessiond() if with_sessiond else None
201 )
202
203 @property
204 def lttng_home_location(self) -> pathlib.Path:
205 if self._lttng_home is None:
206 raise RuntimeError("Attempt to access LTTng home after clean-up")
207 return self._lttng_home.path
208
209 @property
210 def lttng_client_path(self) -> pathlib.Path:
211 return self._project_root / "src" / "bin" / "lttng" / "lttng"
212
213 def create_temporary_directory(self, prefix: Optional[str] = None) -> pathlib.Path:
214 # Simply return a path that is contained within LTTNG_HOME; it will
215 # be destroyed when the temporary home goes out of scope.
216 assert self._lttng_home
217 return pathlib.Path(
218 tempfile.mkdtemp(
219 prefix="tmp" if prefix is None else prefix,
220 dir=str(self._lttng_home.path),
221 )
222 )
223
224 # Unpack a list of environment variables from a string
225 # such as "HELLO=is_it ME='/you/are/looking/for'"
226 @staticmethod
227 def _unpack_env_vars(env_vars_string: str) -> List[Tuple[str, str]]:
228 unpacked_vars = []
229 for var in shlex.split(env_vars_string):
230 equal_position = var.find("=")
231 # Must have an equal sign and not end with an equal sign
232 if equal_position == -1 or equal_position == len(var) - 1:
233 raise ValueError(
234 "Invalid sessiond environment variable: `{}`".format(var)
235 )
236
237 var_name = var[0:equal_position]
238 var_value = var[equal_position + 1 :]
239 # Unquote any paths
240 var_value = var_value.replace("'", "")
241 var_value = var_value.replace('"', "")
242 unpacked_vars.append((var_name, var_value))
243
244 return unpacked_vars
245
246 def _launch_lttng_sessiond(self) -> Optional[subprocess.Popen]:
247 is_64bits_host = sys.maxsize > 2**32
248
249 sessiond_path = (
250 self._project_root / "src" / "bin" / "lttng-sessiond" / "lttng-sessiond"
251 )
252 consumerd_path_option_name = "--consumerd{bitness}-path".format(
253 bitness="64" if is_64bits_host else "32"
254 )
255 consumerd_path = (
256 self._project_root / "src" / "bin" / "lttng-consumerd" / "lttng-consumerd"
257 )
258
259 no_sessiond_var = os.environ.get("TEST_NO_SESSIOND")
260 if no_sessiond_var and no_sessiond_var == "1":
261 # Run test without a session daemon; the user probably
262 # intends to run one under gdb for example.
263 return None
264
265 # Setup the session daemon's environment
266 sessiond_env_vars = os.environ.get("LTTNG_SESSIOND_ENV_VARS")
267 sessiond_env = os.environ.copy()
268 if sessiond_env_vars:
269 self._log("Additional lttng-sessiond environment variables:")
270 additional_vars = self._unpack_env_vars(sessiond_env_vars)
271 for var_name, var_value in additional_vars:
272 self._log(" {name}={value}".format(name=var_name, value=var_value))
273 sessiond_env[var_name] = var_value
274
275 sessiond_env["LTTNG_SESSION_CONFIG_XSD_PATH"] = str(
276 self._project_root / "src" / "common"
277 )
278
279 assert self._lttng_home is not None
280 sessiond_env["LTTNG_HOME"] = str(self._lttng_home.path)
281
282 wait_queue = _SignalWaitQueue()
283 signal.signal(signal.SIGUSR1, wait_queue.signal)
284
285 self._log(
286 "Launching session daemon with LTTNG_HOME=`{home_dir}`".format(
287 home_dir=str(self._lttng_home.path)
288 )
289 )
290 process = subprocess.Popen(
291 [
292 str(sessiond_path),
293 consumerd_path_option_name,
294 str(consumerd_path),
295 "--sig-parent",
296 ],
297 stdout=subprocess.PIPE,
298 stderr=subprocess.STDOUT,
299 env=sessiond_env,
300 )
301
302 if self._logging_function:
303 self._sessiond_output_consumer: Optional[
304 ProcessOutputConsumer
305 ] = ProcessOutputConsumer(process, "lttng-sessiond", self._logging_function)
306 self._sessiond_output_consumer.daemon = True
307 self._sessiond_output_consumer.start()
308
309 # Wait for SIGUSR1, indicating the sessiond is ready to proceed
310 wait_queue.wait_for_signal()
311 signal.signal(signal.SIGUSR1, wait_queue.signal)
312
313 return process
314
315 def _handle_termination_signal(
316 self, signal_number: int, frame: Optional[FrameType]
317 ) -> None:
318 self._log(
319 "Killed by {signal_name} signal, cleaning-up".format(
320 signal_name=signal.strsignal(signal_number)
321 )
322 )
323 self._cleanup()
324
325 def launch_wait_trace_test_application(
326 self, event_count: int
327 ) -> WaitTraceTestApplication:
328 """
329 Launch an application that will wait before tracing `event_count` events.
330 """
331 return WaitTraceTestApplication(
332 self._project_root
333 / "tests"
334 / "utils"
335 / "testapp"
336 / "gen-ust-nevents"
337 / "gen-ust-nevents",
338 event_count,
339 self,
340 )
341
342 # Clean-up managed processes
343 def _cleanup(self) -> None:
344 if self._sessiond and self._sessiond.poll() is None:
345 # The session daemon is alive; kill it.
346 self._log(
347 "Killing session daemon (pid = {sessiond_pid})".format(
348 sessiond_pid=self._sessiond.pid
349 )
350 )
351
352 self._sessiond.terminate()
353 self._sessiond.wait()
354 if self._sessiond_output_consumer:
355 self._sessiond_output_consumer.join()
356 self._sessiond_output_consumer = None
357
358 self._log("Session daemon killed")
359 self._sessiond = None
360
361 self._lttng_home = None
362
363 def __del__(self):
364 self._cleanup()
365
366
367@contextlib.contextmanager
368def test_environment(with_sessiond: bool, log: Optional[Callable[[str], None]] = None):
369 env = _Environment(with_sessiond, log)
370 try:
371 yield env
372 finally:
373 env._cleanup()
This page took 0.037095 seconds and 4 git commands to generate.