Skip to content

Commit 2eae3e1

Browse files
m0hamedcopybara-github
authored andcommitted
Log server errors to build logfiles
Server errors that are barfed onto a remote terminal might be overridden by siso output. Write the error message to all active build logfiles to make sure the full error is listed somewhere. Also adds a --no-remote-print option to disable remote printing. Change-Id: I9a1e8d938b97ef235352adf818e05af8a10d87d8 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/6253703 Reviewed-by: Andrew Grieve <agrieve@chromium.org> Commit-Queue: Mohamed Heikal <mheikal@chromium.org> Cr-Commit-Position: refs/heads/main@{#1419300} NOKEYCHECK=True GitOrigin-RevId: f73b717a771af5eca42234c7f6c4737ec603475f
1 parent 4a72c38 commit 2eae3e1

File tree

1 file changed

+80
-66
lines changed

1 file changed

+80
-66
lines changed

android/fast_local_dev_server.py

Lines changed: 80 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -95,50 +95,53 @@ def is_quiet(cls):
9595
assert cls._options is not None
9696
return cls._options.quiet
9797

98+
@classmethod
99+
def should_remote_print(cls):
100+
assert cls._options is not None
101+
return not cls._options.no_remote_print
102+
98103

99104
class LogfileManager:
100-
_open_logfiles: dict[str, IO[str]] = {}
105+
_logfiles: dict[str, IO[str]] = {}
106+
_lock = threading.RLock()
101107

102108
@classmethod
103109
def create_logfile(cls, build_id, outdir):
104-
# No lock needed since this is only called by the main thread.
105-
if logfile := cls._open_logfiles.get(build_id, None):
110+
with cls._lock:
111+
if logfile := cls._logfiles.get(build_id, None):
112+
return logfile
113+
114+
outdir = pathlib.Path(outdir)
115+
latest_logfile = outdir / f'{_LOGFILE_NAME}.0'
116+
117+
if latest_logfile.exists():
118+
with latest_logfile.open('rt') as f:
119+
first_line = f.readline()
120+
if log_build_id := BUILD_ID_RE.search(first_line):
121+
# If the newest logfile on disk is referencing the same build we are
122+
# currently processing, we probably crashed previously and we should
123+
# pick up where we left off in the same logfile.
124+
if log_build_id.group('build_id') == build_id:
125+
cls._logfiles[build_id] = latest_logfile.open('at')
126+
return cls._logfiles[build_id]
127+
128+
# Do the logfile name shift.
129+
filenames = os.listdir(outdir)
130+
logfiles = {f for f in filenames if f.startswith(_LOGFILE_NAME)}
131+
for idx in reversed(range(_MAX_LOGFILES)):
132+
current_name = f'{_LOGFILE_NAME}.{idx}'
133+
next_name = f'{_LOGFILE_NAME}.{idx+1}'
134+
if current_name in logfiles:
135+
shutil.move(os.path.join(outdir, current_name),
136+
os.path.join(outdir, next_name))
137+
138+
# Create a new 0th logfile.
139+
logfile = latest_logfile.open('wt')
140+
logfile.write(FIRST_LOG_LINE.format(build_id=build_id, outdir=outdir))
141+
logfile.flush()
142+
cls._logfiles[build_id] = logfile
106143
return logfile
107144

108-
outdir = pathlib.Path(outdir)
109-
latest_logfile = outdir / f'{_LOGFILE_NAME}.0'
110-
111-
if latest_logfile.exists():
112-
with latest_logfile.open('rt') as f:
113-
first_line = f.readline()
114-
if log_build_id := BUILD_ID_RE.search(first_line):
115-
# If the newest logfile on disk is referencing the same build we are
116-
# currently processing, we probably crashed previously and we should
117-
# pick up where we left off in the same logfile.
118-
if log_build_id.group('build_id') == build_id:
119-
cls._open_logfiles[build_id] = latest_logfile.open('at')
120-
return cls._open_logfiles[build_id]
121-
122-
# Do the logfile name shift.
123-
filenames = os.listdir(outdir)
124-
logfiles = {f for f in filenames if f.startswith(_LOGFILE_NAME)}
125-
for idx in reversed(range(_MAX_LOGFILES)):
126-
current_name = f'{_LOGFILE_NAME}.{idx}'
127-
next_name = f'{_LOGFILE_NAME}.{idx+1}'
128-
if current_name in logfiles:
129-
shutil.move(os.path.join(outdir, current_name),
130-
os.path.join(outdir, next_name))
131-
132-
# Create a new 0th logfile.
133-
logfile = latest_logfile.open('wt')
134-
# Logfiles are never closed thus are leaked but there should not be too many
135-
# of them since only one per build is created and the server exits on idle
136-
# in normal operation.
137-
cls._open_logfiles[build_id] = logfile
138-
logfile.write(FIRST_LOG_LINE.format(build_id=build_id, outdir=outdir))
139-
logfile.flush()
140-
return logfile
141-
142145

143146
class TaskStats:
144147
"""Class to keep track of aggregate stats for all tasks across threads."""
@@ -278,27 +281,35 @@ def task_done(self, task: Task, status_string: str):
278281

279282
# We synchronize all terminal title info rather than having it per build
280283
# since if two builds are happening in the same terminal concurrently, both
281-
# builds will be overriding each other's titles continously. Usually we only
282-
# have the one build anyways so it should equivalent in most cases.
284+
# builds will be overriding each other's titles continuously. Usually we
285+
# only have the one build anyways so it should equivalent in most cases.
283286
BuildManager.update_remote_titles()
284-
if not self.is_active():
285-
self._logfile.close()
286-
# Reset in case its the last build.
287-
BuildManager.update_remote_titles('')
287+
with self._lock:
288+
if not self.is_active():
289+
self._logfile.close()
290+
# Reset in case its the last build.
291+
BuildManager.update_remote_titles('')
288292

289293
def process_complete(self):
290294
with self._lock:
291295
self._active_process_count -= 1
292296
TaskStats.remove_process()
293297

294298
def ensure_logfile(self):
295-
if not self._logfile:
296-
assert self.cwd is not None
297-
self._logfile = LogfileManager.create_logfile(self.id, self.cwd)
299+
with self._lock:
300+
if not self._logfile:
301+
assert self.cwd is not None
302+
self._logfile = LogfileManager.create_logfile(self.id, self.cwd)
298303

299304
def log(self, message: str):
300-
self.ensure_logfile()
301-
print(message, file=self._logfile, flush=True)
305+
with self._lock:
306+
self.ensure_logfile()
307+
if self._logfile.closed:
308+
# BuildManager#broadcast can call log after the build is done and the
309+
# log is closed. Might make sense to separate out that flow so we can
310+
# raise an exception here otherwise.
311+
return
312+
print(message, file=self._logfile, flush=True)
302313

303314
def _status_update(self, status_message):
304315
prefix = f'[{TaskStats.prefix(self.id)}] '
@@ -412,12 +423,16 @@ def get_all_builds(cls) -> List[Build]:
412423
def broadcast(cls, msg: str):
413424
with cls._lock:
414425
ttys = list(cls._cached_ttys.values())
415-
for tty, _unused in ttys:
416-
try:
417-
tty.write(msg + '\n')
418-
tty.flush()
419-
except BrokenPipeError:
420-
pass
426+
builds = list(cls._builds_by_id.values())
427+
if OptionsManager.should_remote_print():
428+
for tty, _unused in ttys:
429+
try:
430+
tty.write(msg + '\n')
431+
tty.flush()
432+
except BrokenPipeError:
433+
pass
434+
for build in builds:
435+
build.log(msg)
421436
# Write to the current terminal if we have not written to it yet.
422437
st = os.stat(sys.stderr.fileno())
423438
stderr_key = (st.st_ino, st.st_dev)
@@ -665,24 +680,20 @@ def _complete(self, stdout: str = ''):
665680
self.build.log(message)
666681
server_log(message)
667682

668-
# Add emoji to show that output is from the build server.
669-
preamble = [f'⏩ {line}' for line in preamble]
670-
remote_message = '\n'.join(preamble + [stdout])
671-
# Add a new line at start of message to clearly delineate from previous
672-
# output/text already on the remote tty we are printing to.
673-
self.build.stdout.write(f'\n{remote_message}')
674-
self.build.stdout.flush()
683+
if OptionsManager.should_remote_print():
684+
# Add emoji to show that output is from the build server.
685+
preamble = [f'⏩ {line}' for line in preamble]
686+
remote_message = '\n'.join(preamble + [stdout])
687+
# Add a new line at start of message to clearly delineate from previous
688+
# output/text already on the remote tty we are printing to.
689+
self.build.stdout.write(f'\n{remote_message}')
690+
self.build.stdout.flush()
675691
if delete_stamp:
676692
# Force siso to consider failed targets as dirty.
677693
try:
678694
os.unlink(os.path.join(self.build.cwd, self.stamp_file))
679695
except FileNotFoundError:
680696
pass
681-
else:
682-
# We do not care about the action writing a too new mtime. Siso only cares
683-
# about the mtime that is recorded in its database at the time the
684-
# original action finished.
685-
pass
686697
self.build.task_done(self, status_string)
687698

688699

@@ -1067,6 +1078,9 @@ def main():
10671078
parser.add_argument('--quiet',
10681079
action='store_true',
10691080
help='Do not output status updates.')
1081+
parser.add_argument('--no-remote-print',
1082+
action='store_true',
1083+
help='Do not output errors to remote terminals.')
10701084
parser.add_argument('--wait-for-build',
10711085
metavar='BUILD_ID',
10721086
help='Wait for build server to finish with all tasks '

0 commit comments

Comments
 (0)