1616
1717import asyncio
1818import io
19+ import logging
1920import os
2021import platform
2122import signal
@@ -91,6 +92,7 @@ def __init__(
9192 emulate_tty : bool = False ,
9293 output : Text = 'log' ,
9394 output_format : Text = '[{this.process_description.final_name}] {line}' ,
95+ cached_output : bool = False ,
9496 log_cmd : bool = False ,
9597 on_exit : Optional [Union [
9698 SomeActionsType ,
@@ -176,6 +178,8 @@ def __init__(
176178 :param: log_cmd if True, prints the final cmd before executing the
177179 process, which is useful for debugging when substitutions are
178180 involved.
181+ :param: cached_output if `True`, both stdout and stderr will be cached.
182+ Use get_stdout() and get_stderr() to read the buffered output.
179183 :param: on_exit list of actions to execute upon process exit.
180184 :param: respawn if 'True', relaunch the process that abnormally died.
181185 Defaults to 'False'.
@@ -191,6 +195,7 @@ def __init__(
191195 self .__output_format = output_format
192196
193197 self .__log_cmd = log_cmd
198+ self .__cached_output = cached_output
194199 self .__on_exit = on_exit
195200 self .__respawn = respawn
196201 self .__respawn_delay = respawn_delay
@@ -329,59 +334,32 @@ def __on_process_stdin(
329334 cast (ProcessStdin , event )
330335 return None
331336
332- def __on_process_stdout (
333- self , event : ProcessIO
337+ def __on_process_output (
338+ self , event : ProcessIO , buffer : io . TextIOBase , logger : logging . Logger
334339 ) -> Optional [SomeActionsType ]:
335340 to_write = event .text .decode (errors = 'replace' )
336- if self . __stdout_buffer .closed :
337- # __stdout_buffer was probably closed by __flush_buffers on shutdown. Output without
341+ if buffer .closed :
342+ # buffer was probably closed by __flush_buffers on shutdown. Output without
338343 # buffering.
339- self .__stdout_logger .info (
340- self .__output_format .format (line = to_write , this = self )
341- )
342- else :
343- self .__stdout_buffer .write (to_write )
344- self .__stdout_buffer .seek (0 )
345- last_line = None
346- for line in self .__stdout_buffer :
347- if line .endswith (os .linesep ):
348- self .__stdout_logger .info (
349- self .__output_format .format (line = line [:- len (os .linesep )], this = self )
350- )
351- else :
352- last_line = line
353- break
354- self .__stdout_buffer .seek (0 )
355- self .__stdout_buffer .truncate (0 )
356- if last_line is not None :
357- self .__stdout_buffer .write (last_line )
358-
359- def __on_process_stderr (
360- self , event : ProcessIO
361- ) -> Optional [SomeActionsType ]:
362- to_write = event .text .decode (errors = 'replace' )
363- if self .__stderr_buffer .closed :
364- # __stderr buffer was probably closed by __flush_buffers on shutdown. Output without
365- # buffering.
366- self .__stderr_logger .info (
344+ buffer .info (
367345 self .__output_format .format (line = to_write , this = self )
368346 )
369347 else :
370- self . __stderr_buffer .write (to_write )
371- self . __stderr_buffer .seek (0 )
348+ buffer .write (to_write )
349+ buffer .seek (0 )
372350 last_line = None
373- for line in self . __stderr_buffer :
351+ for line in buffer :
374352 if line .endswith (os .linesep ):
375- self . __stderr_logger .info (
353+ logger .info (
376354 self .__output_format .format (line = line [:- len (os .linesep )], this = self )
377355 )
378356 else :
379357 last_line = line
380358 break
381- self . __stderr_buffer .seek (0 )
382- self . __stderr_buffer .truncate (0 )
359+ buffer .seek (0 )
360+ buffer .truncate (0 )
383361 if last_line is not None :
384- self . __stderr_buffer .write (last_line )
362+ buffer .write (last_line )
385363
386364 def __flush_buffers (self , event , context ):
387365 line = self .__stdout_buffer .getvalue ()
@@ -407,6 +385,35 @@ def __flush_buffers(self, event, context):
407385 self .__stderr_buffer .seek (0 )
408386 self .__stderr_buffer .truncate (0 )
409387
388+ def __on_process_output_cached (
389+ self , event : ProcessIO , buffer , logger
390+ ) -> Optional [SomeActionsType ]:
391+ to_write = event .text .decode (errors = 'replace' )
392+ last_cursor = buffer .tell ()
393+ buffer .seek (0 , os .SEEK_END ) # go to end of buffer
394+ buffer .write (to_write )
395+ buffer .seek (last_cursor )
396+ new_cursor = last_cursor
397+ for line in buffer :
398+ if not line .endswith (os .linesep ):
399+ break
400+ new_cursor = buffer .tell ()
401+ logger .info (
402+ self .__output_format .format (line = line [:- len (os .linesep )], this = self )
403+ )
404+ buffer .seek (new_cursor )
405+
406+ def __flush_cached_buffers (self , event , context ):
407+ for line in self .__stdout_buffer :
408+ self .__stdout_logger .info (
409+ self .__output_format .format (line = line , this = self )
410+ )
411+
412+ for line in self .__stderr_buffer :
413+ self .__stderr_logger .info (
414+ self .__output_format .format (line = line , this = self )
415+ )
416+
410417 def __on_shutdown (self , event : Event , context : LaunchContext ) -> Optional [SomeActionsType ]:
411418 due_to_sigint = cast (Shutdown , event ).due_to_sigint
412419 return self ._shutdown_process (
@@ -614,6 +621,13 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
614621 # If shutdown starts before execution can start, don't start execution.
615622 return None
616623
624+ if self .__cached_output :
625+ on_output_method = self .__on_process_output_cached
626+ flush_buffers_method = self .__flush_cached_buffers
627+ else :
628+ on_output_method = self .__on_process_output
629+ flush_buffers_method = self .__flush_buffers
630+
617631 event_handlers = [
618632 EventHandler (
619633 matcher = lambda event : is_a_subclass (event , ShutdownProcess ),
@@ -626,8 +640,10 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
626640 OnProcessIO (
627641 target_action = self ,
628642 on_stdin = self .__on_process_stdin ,
629- on_stdout = self .__on_process_stdout ,
630- on_stderr = self .__on_process_stderr
643+ on_stdout = lambda event : on_output_method (
644+ event , self .__stdout_buffer , self .__stdout_logger ),
645+ on_stderr = lambda event : on_output_method (
646+ event , self .__stderr_buffer , self .__stderr_logger ),
631647 ),
632648 OnShutdown (
633649 on_shutdown = self .__on_shutdown ,
@@ -638,7 +654,7 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
638654 ),
639655 OnProcessExit (
640656 target_action = self ,
641- on_exit = self . __flush_buffers ,
657+ on_exit = flush_buffers_method ,
642658 ),
643659 ]
644660 for event_handler in event_handlers :
@@ -660,3 +676,34 @@ def execute(self, context: LaunchContext) -> Optional[List[LaunchDescriptionEnti
660676 def get_asyncio_future (self ) -> Optional [asyncio .Future ]:
661677 """Return an asyncio Future, used to let the launch system know when we're done."""
662678 return self .__completed_future
679+
680+ def get_stdout (self ):
681+ """
682+ Get cached stdout.
683+
684+ :raises RuntimeError: if cached_output is false.
685+ """
686+ if not self .__cached_output :
687+ raise RuntimeError (
688+ 'cached output must be true to be able to get stdout,'
689+ f" proc '{ self .__process_description .name } '" )
690+ return self .__stdout_buffer .getvalue ()
691+
692+ def get_stderr (self ):
693+ """
694+ Get cached stdout.
695+
696+ :raises RuntimeError: if cached_output is false.
697+ """
698+ if not self .__cached_output :
699+ raise RuntimeError (
700+ 'cached output must be true to be able to get stderr, proc'
701+ f" '{ self .__process_description .name } '" )
702+ return self .__stderr_buffer .getvalue ()
703+
704+ @property
705+ def return_code (self ):
706+ """Get the process return code, None if it hasn't finished."""
707+ if self ._subprocess_transport is None :
708+ return None
709+ return self ._subprocess_transport .get_returncode ()
0 commit comments