Skip to content

Commit aed58ac

Browse files
authored
Merge pull request #46 from UiPath/fix/stdout_redirect
fix: stdout redirect to file
2 parents 822b06c + 1d31b70 commit aed58ac

File tree

4 files changed

+68
-17
lines changed

4 files changed

+68
-17
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
[project]
22
name = "uipath-runtime"
3-
version = "0.2.3"
3+
version = "0.2.4"
44
description = "Runtime abstractions and interfaces for building agents and automation scripts in the UiPath ecosystem"
55
readme = { file = "README.md", content-type = "text/markdown" }
66
requires-python = ">=3.11"

src/uipath/runtime/logging/_interceptor.py

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Main logging interceptor for execution context."""
22

3+
import io
34
import logging
45
import os
56
import sys
@@ -41,6 +42,7 @@ def __init__(
4142
min_level = min_level or "INFO"
4243
self.job_id = job_id
4344
self.execution_id = execution_id
45+
self._owns_handler: bool = log_handler is None
4446

4547
# Convert to numeric level for consistent comparison
4648
self.numeric_min_level = getattr(logging, min_level.upper(), logging.INFO)
@@ -67,8 +69,25 @@ def __init__(
6769
else:
6870
# Create either file handler (runtime) or stdout handler (debug)
6971
if not job_id:
70-
# Use stdout handler when not running as a job or eval
71-
self.log_handler = logging.StreamHandler(sys.stdout)
72+
# Only wrap if stdout is using a problematic encoding (like cp1252 on Windows)
73+
if (
74+
hasattr(sys.stdout, "encoding")
75+
and hasattr(sys.stdout, "buffer")
76+
and sys.stdout.encoding
77+
and sys.stdout.encoding.lower() not in ("utf-8", "utf8")
78+
):
79+
# Wrap stdout with UTF-8 encoding for the handler
80+
self.utf8_stdout = io.TextIOWrapper(
81+
sys.stdout.buffer,
82+
encoding="utf-8",
83+
errors="replace",
84+
line_buffering=True,
85+
)
86+
self.log_handler = logging.StreamHandler(self.utf8_stdout)
87+
else:
88+
# stdout already has good encoding, use it directly
89+
self.log_handler = logging.StreamHandler(sys.stdout)
90+
7291
formatter = logging.Formatter("%(message)s")
7392
self.log_handler.setFormatter(formatter)
7493
else:
@@ -214,7 +233,11 @@ def teardown(self) -> None:
214233
if handler not in self.root_logger.handlers:
215234
self.root_logger.addHandler(handler)
216235

217-
self.log_handler.close()
236+
if self._owns_handler:
237+
self.log_handler.close()
238+
239+
if hasattr(self, "utf8_stdout"):
240+
self.utf8_stdout.close()
218241

219242
# Only restore streams if we redirected them
220243
if self.original_stdout and self.original_stderr:

src/uipath/runtime/logging/_writers.py

Lines changed: 40 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -20,23 +20,48 @@ def __init__(
2020
self.min_level = min_level
2121
self.buffer = ""
2222
self.sys_file = sys_file
23+
self._in_logging = False # Recursion guard
2324

2425
def write(self, message: str) -> None:
2526
"""Write message to the logger, buffering until newline."""
26-
self.buffer += message
27-
while "\n" in self.buffer:
28-
line, self.buffer = self.buffer.split("\n", 1)
29-
# Only log if the message is not empty and the level is sufficient
30-
if line and self.level >= self.min_level:
31-
# The context variable is automatically available here
32-
self.logger._log(self.level, line, ())
27+
# Prevent infinite recursion when logging.handleError writes to stderr
28+
if self._in_logging:
29+
if self.sys_file:
30+
try:
31+
self.sys_file.write(message)
32+
except (OSError, IOError):
33+
pass # Fail silently if we can't write
34+
return
35+
36+
try:
37+
self._in_logging = True
38+
self.buffer += message
39+
while "\n" in self.buffer:
40+
line, self.buffer = self.buffer.split("\n", 1)
41+
# Only log if the message is not empty and the level is sufficient
42+
if line and self.level >= self.min_level:
43+
self.logger._log(self.level, line, ())
44+
finally:
45+
self._in_logging = False
3346

3447
def flush(self) -> None:
3548
"""Flush any remaining buffered messages to the logger."""
36-
# Log any remaining content in the buffer on flush
37-
if self.buffer and self.level >= self.min_level:
38-
self.logger._log(self.level, self.buffer, ())
39-
self.buffer = ""
49+
if self._in_logging:
50+
if self.sys_file:
51+
try:
52+
self.sys_file.flush()
53+
except (OSError, IOError):
54+
pass # Fail silently if we can't flush
55+
return
56+
57+
try:
58+
self._in_logging = True
59+
# Log any remaining content in the buffer on flush
60+
if self.buffer and self.level >= self.min_level:
61+
self.logger._log(self.level, self.buffer, ())
62+
self.buffer = ""
63+
finally:
64+
self._in_logging = False
4065

4166
def fileno(self) -> int:
4267
"""Get the file descriptor of the original sys.stdout/sys.stderr."""
@@ -47,7 +72,10 @@ def fileno(self) -> int:
4772

4873
def isatty(self) -> bool:
4974
"""Check if the original sys.stdout/sys.stderr is a TTY."""
50-
return hasattr(self.sys_file, "isatty") and self.sys_file.isatty()
75+
try:
76+
return hasattr(self.sys_file, "isatty") and self.sys_file.isatty()
77+
except (AttributeError, OSError, ValueError):
78+
return False
5179

5280
def writable(self) -> bool:
5381
"""Check if the original sys.stdout/sys.stderr is writable."""

uv.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)