1818import dataclasses
1919import datetime
2020import enum
21+ import getpass
2122import glob
2223import io
2324import logging
4748
4849MATTER_DEVELOPMENT_PAA_ROOT_CERTS = "credentials/development/paa-root-certs"
4950
51+ TAG_PROCESS_MON = f"[{ Fore .GREEN } MON { Style .RESET_ALL } ]" .encode ()
5052TAG_PROCESS_APP = f"[{ Fore .GREEN } APP { Style .RESET_ALL } ]" .encode ()
5153TAG_PROCESS_TEST = f"[{ Fore .GREEN } TEST{ Style .RESET_ALL } ]" .encode ()
5254TAG_STDOUT = f"[{ Fore .YELLOW } STDOUT{ Style .RESET_ALL } ]" .encode ()
@@ -70,6 +72,10 @@ def process_chip_output(line: bytes, is_stderr: bool, process_tag: bytes = b"")
7072 return f"[{ timestamp } ]" .encode () + process_tag + (TAG_STDERR if is_stderr else TAG_STDOUT ) + line
7173
7274
75+ def process_mon_output (line , is_stderr ):
76+ return process_chip_output (line , is_stderr , TAG_PROCESS_MON )
77+
78+
7379def process_chip_app_output (line , is_stderr ):
7480 return process_chip_output (line , is_stderr , TAG_PROCESS_APP )
7581
@@ -150,6 +156,33 @@ def get_process(self):
150156 return self .app_process
151157
152158
159+ class IpPacketCaptureManager ():
160+ def __init__ (self , dump_filename : pathlib .Path ):
161+ self .tcpdump_process = None
162+ self .dump_filename = dump_filename
163+ self .interface = 'any'
164+ self .keep_dumpfile = True
165+
166+ def start (self ):
167+ # Create directory for dump files
168+ self .dump_filename .parent .mkdir (parents = True , exist_ok = True )
169+
170+ cmd = ['tcpdump' , '-qn' , '-i' , self .interface , '-w' , str (self .dump_filename ), '-Z' , getpass .getuser ()]
171+ if os .getuid () != 0 :
172+ cmd = ['sudo' , '-n' ] + cmd
173+ self .tcpdump_process = Subprocess (cmd [0 ], * cmd [1 :], output_cb = process_mon_output )
174+
175+ self .tcpdump_process .start ()
176+
177+ def stop (self ):
178+ if self .tcpdump_process :
179+ self .tcpdump_process .terminate ()
180+ self .tcpdump_process = None
181+ if not self .keep_dumpfile :
182+ log .info ("Deleting capture file '%s'" , self .dump_filename )
183+ self .dump_filename .unlink (missing_ok = True )
184+
185+
153186@click .command ()
154187@click .option ("--app" , type = click .Path (exists = True ), default = None ,
155188 help = 'Path to local application to use, omit to use external apps.' )
@@ -178,10 +211,14 @@ def get_process(self):
178211 help = "Do not print output from passing tests. Use this flag in CI to keep GitHub log size manageable." )
179212@click .option ("--load-from-env" , default = None , help = "YAML file that contains values for environment variables." )
180213@click .option ("--run" , type = str , multiple = True , help = "Run only the specified test run(s)." )
214+ @click .option ("--ip-packet-capture/--no-ip-packet-capture" , is_flag = True , default = False , help = "Enable IP packet capture." )
215+ @click .option ("--ip-packet-capture-dir" , type = click .Path (file_okay = False , writable = True , path_type = pathlib .Path ),
216+ default = pathlib .Path .cwd () / "out/ip_packet_captures" , help = "Storage for capture files." )
181217@click .option ("--app-filter" , type = str , default = None , help = "Run only for the specified app(s). Comma separated." )
182218def main (app : str , factory_reset : bool , factory_reset_app_only : bool , app_args : str ,
183219 app_ready_pattern : str , app_stdin_pipe : str , script : str , script_args : str ,
184- script_gdb : bool , quiet : bool , load_from_env , run , app_filter ):
220+ script_gdb : bool , quiet : bool , load_from_env , run , ip_packet_capture : bool , ip_packet_capture_dir : pathlib .Path ,
221+ app_filter ):
185222 if load_from_env :
186223 reader = MetadataReader (load_from_env )
187224 runs = reader .parse_script (script )
@@ -228,12 +265,14 @@ def main(app: str, factory_reset: bool, factory_reset_app_only: bool, app_args:
228265 for run in runs :
229266 log .info ("Executing '%s' '%s'" , run .py_script_path .split ('/' )[- 1 ], run .run )
230267 main_impl (run .app , run .factory_reset , run .factory_reset_app_only , run .app_args or "" , run .app_ready_pattern ,
231- run .app_stdin_pipe , run .py_script_path , run .script_args or "" , run .script_gdb , run .quiet )
268+ run .app_stdin_pipe , run .py_script_path , run .script_args or "" , run .script_gdb , ip_packet_capture ,
269+ ip_packet_capture_dir , run .quiet , run .run )
232270
233271
234272def main_impl (app : str , factory_reset : bool , factory_reset_app_only : bool , app_args : str ,
235273 app_ready_pattern : str , app_stdin_pipe : str , script : str , script_args : str ,
236- script_gdb : bool , quiet : bool ):
274+ script_gdb : bool , ip_packet_capture : bool , ip_packet_capture_dir : pathlib .Path ,
275+ quiet : bool , run_name : str ):
237276
238277 app_args = app_args .replace ('{SCRIPT_BASE_NAME}' , os .path .splitext (os .path .basename (script ))[0 ])
239278 script_args = script_args .replace ('{SCRIPT_BASE_NAME}' , os .path .splitext (os .path .basename (script ))[0 ])
@@ -242,6 +281,14 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
242281 test_run_id = str (uuid .uuid4 ())[:8 ] # Use first 8 characters for shorter paths
243282 restart_flag_file = f"/tmp/chip_test_restart_app_{ test_run_id } "
244283
284+ script_name = pathlib .Path (script ).name .removesuffix ('.py' )
285+ tcpdump_capture_filename = ip_packet_capture_dir / f"tcpdump_{ script_name } -{ os .getpid ()} -{ run_name } .pcap"
286+
287+ tcpdump = IpPacketCaptureManager (pathlib .Path (tcpdump_capture_filename ))
288+
289+ if ip_packet_capture :
290+ tcpdump .start ()
291+
245292 # Remove app config and storage if factory reset is requested
246293 if factory_reset or factory_reset_app_only :
247294 reset_type = FactoryResetType .AppAndController if factory_reset else FactoryResetType .AppOnly
@@ -328,6 +375,10 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
328375 # We expect both app and test script should exit with 0
329376 exit_code = test_script_exit_code or app_exit_code
330377
378+ if tcpdump and exit_code == 0 :
379+ # Delete packet captures from successful runs
380+ tcpdump .keep_dumpfile = False
381+
331382 if quiet :
332383 if exit_code :
333384 sys .stdout .write (stream_output .getvalue ().decode ('utf-8' , errors = 'replace' ))
@@ -346,6 +397,8 @@ def main_impl(app: str, factory_reset: bool, factory_reset_app_only: bool, app_a
346397 log .info ("Stopping app restart monitor thread" )
347398 restart_monitor_thread .join (2.0 )
348399
400+ tcpdump .stop ()
401+
349402 # Clean up any leftover flag files if they exist - ensure this always executes
350403 log .info ("Cleaning up flag files" )
351404 if os .path .exists (restart_flag_file ):
0 commit comments