Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -838,12 +838,21 @@ def run_coroutine(

@staticmethod
def _flush_stdio() -> None:
"""Flush Python's stdout and stderr, swallowing errors from closed/detached streams."""
"""Flush Python and C stdout/stderr, swallowing errors from closed/detached streams."""
for stream in (sys.stdout, sys.stderr):
try:
stream.flush()
except (ValueError, OSError, AttributeError):
pass
# Native Kit output (e.g. ``IApp.print_and_log``) goes through the C/C++ stdio
# buffers, which ``os._exit`` abandons unflushed. ``fflush(NULL)`` flushes every
# open C output stream, including ``std::cout`` (synchronized with C stdio).
try:
import ctypes

ctypes.CDLL(None).fflush(None)
except (OSError, AttributeError):
pass

def close(self, wait_for_replicator: bool = True, skip_cleanup: bool = False, exit_code: int = 0) -> None:
"""Close the running Omniverse Toolkit application.
Expand All @@ -857,8 +866,9 @@ def close(self, wait_for_replicator: bool = True, skip_cleanup: bool = False, ex
skip_cleanup: If True, performs immediate exit without cleanup.
If False, performs graceful shutdown with full cleanup.
exit_code: Process exit status to preserve when fast shutdown terminates
the process. Nonzero values flush stdio and exit with the supplied
status before Kit's fast-shutdown path can replace it with 0.
the process. The same cleanup runs regardless of the value; a nonzero
status is applied at the point where Kit's fast-shutdown path would
otherwise terminate the process with 0.

Example:

Expand All @@ -883,10 +893,6 @@ def close(self, wait_for_replicator: bool = True, skip_cleanup: bool = False, ex
# the interpreter's normal flush-on-exit, so pending print() output would otherwise be lost.
self._flush_stdio()

if exit_code != 0 and self.config.get("fast_shutdown", False):
self._exiting = True
os._exit(exit_code)

# `post_quit()` can already stop Kit's run loop before callers reach `close()`.
# In that state, forcing shutdown may block indefinitely.
if not self._app.is_running():
Expand All @@ -904,6 +910,10 @@ def close(self, wait_for_replicator: bool = True, skip_cleanup: bool = False, ex
_logging = carb.logging.acquire_logging()
_logging.set_log_enabled(False)
self._flush_stdio()
if exit_code != 0 and self.config.get("fast_shutdown", False):
# app.shutdown() under fast shutdown exits the process with status 0;
# preserve the caller's exit status instead.
os._exit(exit_code)
self._arm_shutdown_watchdog()
self._app.shutdown()
return
Expand Down Expand Up @@ -960,6 +970,10 @@ def close(self, wait_for_replicator: bool = True, skip_cleanup: bool = False, ex
# immediately. When false it performs full extension teardown and returns;
# the framework/plugin unload is left to process exit.
self._flush_stdio()
if exit_code != 0 and self.config.get("fast_shutdown", False):
# app.shutdown() under fast shutdown exits the process with status 0;
# preserve the caller's exit status now that cleanup is complete.
os._exit(exit_code)
self._arm_shutdown_watchdog()
self._app.shutdown()

Expand Down
Loading