blob: ba804ab997d824592e7be9c8cfb20e84064963c2 [file] [log] [blame]
Peter Wenb1f3b1d2021-02-02 21:30:201#!/usr/bin/env python3
Avi Drissman73a09d12022-09-08 20:33:382# Copyright 2021 The Chromium Authors
Peter Wenb1f3b1d2021-02-02 21:30:203# Use of this source code is governed by a BSD-style license that can be
4# found in the LICENSE file.
5"""Creates an server to offload non-critical-path GN targets."""
6
Peter Wencd460ff52021-02-23 22:40:057from __future__ import annotations
8
Peter Wenb1f3b1d2021-02-02 21:30:209import argparse
Mohamed Heikalf746b57f2024-11-13 21:20:1710import collections
11import contextlib
Andrew Grieve0d6e8a752025-02-05 21:20:5012import dataclasses
Mohamed Heikalf746b57f2024-11-13 21:20:1713import datetime
Peter Wenb1f3b1d2021-02-02 21:30:2014import os
Mohamed Heikalf746b57f2024-11-13 21:20:1715import pathlib
Mohamed Heikalf746b57f2024-11-13 21:20:1716import re
Mohamed Heikalabf646e2024-12-12 16:06:0517import signal
Andrew Grieved863d0f2024-12-13 20:13:0118import shlex
Peter Wen6e7e52b2021-02-13 02:39:2819import shutil
Peter Wenb1f3b1d2021-02-02 21:30:2020import socket
21import subprocess
22import sys
Peter Wenf409c0c2021-02-09 19:33:0223import threading
Mohamed Heikalb752b772024-11-25 23:05:4424import traceback
Mohamed Heikalf746b57f2024-11-13 21:20:1725import time
26from typing import Callable, Dict, List, Optional, Tuple, IO
Peter Wenb1f3b1d2021-02-02 21:30:2027
28sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp'))
29from util import server_utils
30
Mohamed Heikalabf646e2024-12-12 16:06:0531_SOCKET_TIMEOUT = 60 # seconds
Peter Wen6e7e52b2021-02-13 02:39:2832
Mohamed Heikalf746b57f2024-11-13 21:20:1733_LOGFILE_NAME = 'buildserver.log'
34_MAX_LOGFILES = 6
35
Andrew Grieve0d6e8a752025-02-05 21:20:5036FIRST_LOG_LINE = """\
37#### Start of log for build: {build_id}
38#### CWD: {outdir}
39"""
40BUILD_ID_RE = re.compile(r'^#### Start of log for build: (?P<build_id>.+)')
Mohamed Heikalf746b57f2024-11-13 21:20:1741
42
Mohamed Heikalb752b772024-11-25 23:05:4443def log(msg: str, quiet: bool = False):
44 if quiet:
45 return
46 # Ensure we start our message on a new line.
Mohamed Heikal9984e432024-12-03 18:21:4047 print('\n' + msg)
Mohamed Heikalb752b772024-11-25 23:05:4448
49
Mohamed Heikal9984e432024-12-03 18:21:4050def set_status(msg: str, *, quiet: bool = False, build_id: str = None):
Mohamed Heikalf746b57f2024-11-13 21:20:1751 prefix = f'[{TaskStats.prefix()}] '
52 # if message is specific to a build then also output to its logfile.
53 if build_id:
Mohamed Heikal08b467e02025-01-27 20:54:2554 LogfileManager.log_to_file(f'{prefix}{msg}', build_id=build_id)
Mohamed Heikalf746b57f2024-11-13 21:20:1755
56 # No need to also output to the terminal if quiet.
57 if quiet:
58 return
Peter Wencd460ff52021-02-23 22:40:0559 # Shrink the message (leaving a 2-char prefix and use the rest of the room
60 # for the suffix) according to terminal size so it is always one line.
61 width = shutil.get_terminal_size().columns
Peter Wencd460ff52021-02-23 22:40:0562 max_msg_width = width - len(prefix)
63 if len(msg) > max_msg_width:
64 length_to_show = max_msg_width - 5 # Account for ellipsis and header.
65 msg = f'{msg[:2]}...{msg[-length_to_show:]}'
66 # \r to return the carriage to the beginning of line.
67 # \033[K to replace the normal \n to erase until the end of the line.
68 # Avoid the default line ending so the next \r overwrites the same line just
69 # like ninja's output.
Mohamed Heikal9984e432024-12-03 18:21:4070 print(f'\r{prefix}{msg}\033[K', end='', flush=True)
Peter Wencd460ff52021-02-23 22:40:0571
72
Mohamed Heikalb752b772024-11-25 23:05:4473def _exception_hook(exctype: type, exc: Exception, tb):
Mohamed Heikald764eca2025-01-31 01:06:3574 # Let KeyboardInterrupt through.
75 if issubclass(exctype, KeyboardInterrupt):
76 sys.__excepthook__(exctype, exc, tb)
77 return
78 stacktrace = ''.join(traceback.format_exception(exctype, exc, tb))
79 stacktrace_lines = [f'\n⛔{line}' for line in stacktrace.splitlines()]
Mohamed Heikalb752b772024-11-25 23:05:4480 # Output uncaught exceptions to all live terminals
Andrew Grieve0d6e8a752025-02-05 21:20:5081 # Extra newline since siso's output often erases the current line.
82 BuildManager.broadcast(''.join(stacktrace_lines) + '\n')
Mohamed Heikal9984e432024-12-03 18:21:4083 # Cancel all pending tasks cleanly (i.e. delete stamp files if necessary).
84 TaskManager.deactivate()
Mohamed Heikalb752b772024-11-25 23:05:4485
86
Mohamed Heikal08b467e02025-01-27 20:54:2587class LogfileManager:
88 _open_logfiles: dict[str, IO[str]] = {}
89
90 @classmethod
91 def log_to_file(cls, message: str, build_id: str):
92 # No lock needed since this is only called by threads started after
93 # create_logfile was called on the main thread.
94 logfile = cls._open_logfiles[build_id]
95 print(message, file=logfile, flush=True)
96
97 @classmethod
98 def create_logfile(cls, build_id, outdir):
99 # No lock needed since this is only called by the main thread.
100 if logfile := cls._open_logfiles.get(build_id, None):
101 return logfile
102
103 outdir = pathlib.Path(outdir)
104 latest_logfile = outdir / f'{_LOGFILE_NAME}.0'
105
106 if latest_logfile.exists():
107 with latest_logfile.open('rt') as f:
108 first_line = f.readline()
109 if log_build_id := BUILD_ID_RE.search(first_line):
110 # If the newest logfile on disk is referencing the same build we are
111 # currently processing, we probably crashed previously and we should
112 # pick up where we left off in the same logfile.
113 if log_build_id.group('build_id') == build_id:
114 cls._open_logfiles[build_id] = latest_logfile.open('at')
115 return cls._open_logfiles[build_id]
116
117 # Do the logfile name shift.
118 filenames = os.listdir(outdir)
119 logfiles = {f for f in filenames if f.startswith(_LOGFILE_NAME)}
120 for idx in reversed(range(_MAX_LOGFILES)):
121 current_name = f'{_LOGFILE_NAME}.{idx}'
122 next_name = f'{_LOGFILE_NAME}.{idx+1}'
123 if current_name in logfiles:
124 shutil.move(os.path.join(outdir, current_name),
125 os.path.join(outdir, next_name))
126
127 # Create a new 0th logfile.
128 logfile = latest_logfile.open('wt')
129 # Logfiles are never closed thus are leaked but there should not be too many
130 # of them since only one per build is created and the server exits on idle
131 # in normal operation.
132 cls._open_logfiles[build_id] = logfile
Andrew Grieve0d6e8a752025-02-05 21:20:50133 logfile.write(FIRST_LOG_LINE.format(build_id=build_id, outdir=outdir))
Mohamed Heikal08b467e02025-01-27 20:54:25134 logfile.flush()
Mohamed Heikalf746b57f2024-11-13 21:20:17135 return logfile
136
Mohamed Heikalf746b57f2024-11-13 21:20:17137
Peter Wencd460ff52021-02-23 22:40:05138class TaskStats:
139 """Class to keep track of aggregate stats for all tasks across threads."""
140 _num_processes = 0
141 _completed_tasks = 0
142 _total_tasks = 0
Mohamed Heikalf746b57f2024-11-13 21:20:17143 _total_task_count_per_build = collections.defaultdict(int)
144 _completed_task_count_per_build = collections.defaultdict(int)
145 _running_processes_count_per_build = collections.defaultdict(int)
Andrew Grieved863d0f2024-12-13 20:13:01146 _lock = threading.RLock()
Peter Wen6e7e52b2021-02-13 02:39:28147
148 @classmethod
Peter Wencd460ff52021-02-23 22:40:05149 def no_running_processes(cls):
Mohamed Heikalf746b57f2024-11-13 21:20:17150 with cls._lock:
151 return cls._num_processes == 0
Peter Wen6e7e52b2021-02-13 02:39:28152
153 @classmethod
Andrew Grieve0d6e8a752025-02-05 21:20:50154 def add_task(cls, build_id: str):
Mohamed Heikalf746b57f2024-11-13 21:20:17155 with cls._lock:
156 cls._total_tasks += 1
157 cls._total_task_count_per_build[build_id] += 1
Peter Wencd460ff52021-02-23 22:40:05158
159 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17160 def add_process(cls, build_id: str):
Peter Wencd460ff52021-02-23 22:40:05161 with cls._lock:
162 cls._num_processes += 1
Mohamed Heikalf746b57f2024-11-13 21:20:17163 cls._running_processes_count_per_build[build_id] += 1
Peter Wencd460ff52021-02-23 22:40:05164
165 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17166 def remove_process(cls, build_id: str):
Peter Wencd460ff52021-02-23 22:40:05167 with cls._lock:
168 cls._num_processes -= 1
Mohamed Heikalf746b57f2024-11-13 21:20:17169 cls._running_processes_count_per_build[build_id] -= 1
Peter Wencd460ff52021-02-23 22:40:05170
171 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17172 def complete_task(cls, build_id: str):
Peter Wencd460ff52021-02-23 22:40:05173 with cls._lock:
174 cls._completed_tasks += 1
Mohamed Heikalf746b57f2024-11-13 21:20:17175 cls._completed_task_count_per_build[build_id] += 1
Peter Wencd460ff52021-02-23 22:40:05176
177 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17178 def num_pending_tasks(cls, build_id: str = None):
179 with cls._lock:
180 if build_id:
181 return cls._total_task_count_per_build[
182 build_id] - cls._completed_task_count_per_build[build_id]
183 return cls._total_tasks - cls._completed_tasks
184
185 @classmethod
186 def num_completed_tasks(cls, build_id: str = None):
187 with cls._lock:
188 if build_id:
189 return cls._completed_task_count_per_build[build_id]
190 return cls._completed_tasks
191
192 @classmethod
Andrew Grieve6c764fff2025-01-30 21:02:03193 def total_tasks(cls, build_id: str = None):
194 with cls._lock:
195 if build_id:
196 return cls._total_task_count_per_build[build_id]
197 return cls._total_tasks
198
199 @classmethod
Andrew Grieved863d0f2024-12-13 20:13:01200 def query_build(cls, query_build_id: str = None):
201 with cls._lock:
202 active_builds = BuildManager.get_live_builds()
Andrew Grieve0d6e8a752025-02-05 21:20:50203 active_build_ids = [b.id for b in active_builds]
Andrew Grieved863d0f2024-12-13 20:13:01204 if query_build_id:
205 build_ids = [query_build_id]
206 else:
207 build_ids = sorted(
Andrew Grieve0d6e8a752025-02-05 21:20:50208 set(active_build_ids) | set(cls._total_task_count_per_build))
Andrew Grieved863d0f2024-12-13 20:13:01209 builds = []
210 for build_id in build_ids:
Andrew Grieve0d6e8a752025-02-05 21:20:50211 build = next((b for b in active_builds if b.id == build_id), None)
Andrew Grieved863d0f2024-12-13 20:13:01212 current_tasks = TaskManager.get_current_tasks(build_id)
213 builds.append({
214 'build_id': build_id,
Andrew Grieve0d6e8a752025-02-05 21:20:50215 'is_active': build is not None,
Andrew Grieved863d0f2024-12-13 20:13:01216 'completed_tasks': cls.num_completed_tasks(build_id),
217 'pending_tasks': cls.num_pending_tasks(build_id),
218 'active_tasks': [t.cmd for t in current_tasks],
Andrew Grieve0d6e8a752025-02-05 21:20:50219 'outdir': build.cwd if build else None,
Andrew Grieved863d0f2024-12-13 20:13:01220 })
221 return {
222 'pid': os.getpid(),
223 'builds': builds,
224 }
225
226 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17227 def prefix(cls, build_id: str = None):
Peter Wen6e7e52b2021-02-13 02:39:28228 # Ninja's prefix is: [205 processes, 6/734 @ 6.5/s : 0.922s ]
229 # Time taken and task completion rate are not important for the build server
230 # since it is always running in the background and uses idle priority for
231 # its tasks.
Peter Wencd460ff52021-02-23 22:40:05232 with cls._lock:
Mohamed Heikalf746b57f2024-11-13 21:20:17233 if build_id:
234 _num_processes = cls._running_processes_count_per_build[build_id]
235 _completed_tasks = cls._completed_task_count_per_build[build_id]
236 _total_tasks = cls._total_task_count_per_build[build_id]
237 else:
238 _num_processes = cls._num_processes
239 _completed_tasks = cls._completed_tasks
240 _total_tasks = cls._total_tasks
241 word = 'process' if _num_processes == 1 else 'processes'
242 return (f'{_num_processes} {word}, '
243 f'{_completed_tasks}/{_total_tasks}')
Peter Wenb1f3b1d2021-02-02 21:30:20244
Peter Wenf409c0c2021-02-09 19:33:02245
Mohamed Heikalb752b772024-11-25 23:05:44246def check_pid_alive(pid: int):
247 try:
248 os.kill(pid, 0)
249 except OSError:
250 return False
251 return True
252
253
Andrew Grieve0d6e8a752025-02-05 21:20:50254@dataclasses.dataclass
255class Build:
256 id: str
257 pid: int
258 env: dict
259 isatty: bool
260 stdout: IO[str]
261 cwd: Optional[str] = None
262
263 def set_title(self, title):
264 if self.isatty:
265 self.stdout.write(f'\033]2;{title}\007')
266 self.stdout.flush()
267
268
Mohamed Heikalb752b772024-11-25 23:05:44269class BuildManager:
Andrew Grieve0d6e8a752025-02-05 21:20:50270 _builds_by_id: dict[str, Build] = dict()
Mohamed Heikal08b467e02025-01-27 20:54:25271 _cached_ttys: dict[(int, int), IO[str]] = dict()
Mohamed Heikalb752b772024-11-25 23:05:44272 _lock = threading.RLock()
273
274 @classmethod
Andrew Grieve0d6e8a752025-02-05 21:20:50275 def register_builder(cls, env, pid, cwd):
276 build_id = env['AUTONINJA_BUILD_ID']
277 stdout = cls.open_tty(env['AUTONINJA_STDOUT_NAME'])
278 # Tells the script not to re-delegate to build server.
279 env[server_utils.BUILD_SERVER_ENV_VARIABLE] = '1'
280
Mohamed Heikalb752b772024-11-25 23:05:44281 with cls._lock:
Andrew Grieve0d6e8a752025-02-05 21:20:50282 build = Build(id=build_id,
283 pid=pid,
284 cwd=cwd,
285 env=env,
286 isatty=stdout.isatty(),
287 stdout=stdout)
288 build.set_title('Analysis Steps: 0/0')
289 stdout.flush()
290 cls.maybe_init_cwd(build, cwd)
291 cls._builds_by_id[build_id] = build
292
293 @classmethod
294 def maybe_init_cwd(cls, build, cwd):
295 if cwd is not None:
296 with cls._lock:
297 if build.cwd is None:
298 build.cwd = cwd
299 LogfileManager.create_logfile(build.id, cwd)
300 else:
301 assert cwd == build.cwd, f'{repr(cwd)} != {repr(build.cwd)}'
302
303 @classmethod
304 def get_build(cls, build_id):
305 with cls._lock:
306 return cls._builds_by_id[build_id]
Mohamed Heikalb752b772024-11-25 23:05:44307
308 @classmethod
Mohamed Heikal08b467e02025-01-27 20:54:25309 def open_tty(cls, tty_path):
310 # Do not open the same tty multiple times. Use st_ino and st_dev to compare
311 # file descriptors.
Andrew Grieve0d6e8a752025-02-05 21:20:50312 tty = open(tty_path, 'at')
Mohamed Heikaldb4fd9c2025-01-29 20:56:27313 st = os.stat(tty.fileno())
Mohamed Heikal08b467e02025-01-27 20:54:25314 tty_key = (st.st_ino, st.st_dev)
Mohamed Heikalb752b772024-11-25 23:05:44315 with cls._lock:
Mohamed Heikal08b467e02025-01-27 20:54:25316 # Dedupes ttys
317 if tty_key not in cls._cached_ttys:
318 # TTYs are kept open for the lifetime of the server so that broadcast
319 # messages (e.g. uncaught exceptions) can be sent to them even if they
320 # are not currently building anything.
Mohamed Heikaldb4fd9c2025-01-29 20:56:27321 cls._cached_ttys[tty_key] = tty
322 else:
323 tty.close()
Mohamed Heikal08b467e02025-01-27 20:54:25324 return cls._cached_ttys[tty_key]
Mohamed Heikalb752b772024-11-25 23:05:44325
326 @classmethod
327 def get_live_builds(cls):
328 with cls._lock:
Andrew Grieve0d6e8a752025-02-05 21:20:50329 for build in list(cls._builds_by_id.values()):
330 if not check_pid_alive(build.pid):
331 # Setting an empty title causes most terminals to go back to the
332 # default title (and at least prevents the tab title from being
333 # "Analysis Steps: N/N" forevermore.
334 build.set_title('')
335 del cls._builds_by_id[build.id]
336 return list(cls._builds_by_id.values())
Mohamed Heikalb752b772024-11-25 23:05:44337
338 @classmethod
339 def broadcast(cls, msg: str):
Mohamed Heikalb752b772024-11-25 23:05:44340 with cls._lock:
Mohamed Heikal08b467e02025-01-27 20:54:25341 for tty in cls._cached_ttys.values():
Mohamed Heikalb752b772024-11-25 23:05:44342 try:
343 tty.write(msg + '\n')
344 tty.flush()
345 except BrokenPipeError:
346 pass
Mohamed Heikald764eca2025-01-31 01:06:35347 # Write to the current terminal if we have not written to it yet.
348 st = os.stat(sys.stderr.fileno())
349 stderr_key = (st.st_ino, st.st_dev)
350 if stderr_key not in cls._cached_ttys:
351 print(msg, file=sys.stderr)
Mohamed Heikalb752b772024-11-25 23:05:44352
353 @classmethod
354 def has_live_builds(cls):
355 return bool(cls.get_live_builds())
356
357
Peter Wencd460ff52021-02-23 22:40:05358class TaskManager:
359 """Class to encapsulate a threadsafe queue and handle deactivating it."""
Mohamed Heikalb752b772024-11-25 23:05:44360 _queue: collections.deque[Task] = collections.deque()
Mohamed Heikalabf646e2024-12-12 16:06:05361 _current_tasks: set[Task] = set()
Mohamed Heikalf746b57f2024-11-13 21:20:17362 _deactivated = False
Mohamed Heikalb752b772024-11-25 23:05:44363 _lock = threading.RLock()
Peter Wencd460ff52021-02-23 22:40:05364
Mohamed Heikalf746b57f2024-11-13 21:20:17365 @classmethod
366 def add_task(cls, task: Task, options):
367 assert not cls._deactivated
Andrew Grieve0d6e8a752025-02-05 21:20:50368 TaskStats.add_task(task.build.id)
Mohamed Heikalb752b772024-11-25 23:05:44369 with cls._lock:
370 cls._queue.appendleft(task)
371 set_status(f'QUEUED {task.name}',
372 quiet=options.quiet,
Andrew Grieve0d6e8a752025-02-05 21:20:50373 build_id=task.build.id)
Mohamed Heikalf746b57f2024-11-13 21:20:17374 cls._maybe_start_tasks()
Peter Wencd460ff52021-02-23 22:40:05375
Mohamed Heikalf746b57f2024-11-13 21:20:17376 @classmethod
Mohamed Heikalabf646e2024-12-12 16:06:05377 def task_done(cls, task: Task):
Andrew Grieve0d6e8a752025-02-05 21:20:50378 TaskStats.complete_task(build_id=task.build.id)
379
380 total = TaskStats.total_tasks(task.build.id)
381 completed = TaskStats.num_completed_tasks(task.build.id)
382 task.build.set_title(f'Analysis Steps: {completed}/{total}')
Andrew Grieve6c764fff2025-01-30 21:02:03383
Mohamed Heikalabf646e2024-12-12 16:06:05384 with cls._lock:
Mohamed Heikal651c9922025-01-16 19:12:21385 cls._current_tasks.discard(task)
Mohamed Heikalabf646e2024-12-12 16:06:05386
387 @classmethod
Andrew Grieved863d0f2024-12-13 20:13:01388 def get_current_tasks(cls, build_id):
389 with cls._lock:
Andrew Grieve0d6e8a752025-02-05 21:20:50390 return [t for t in cls._current_tasks if t.build.id == build_id]
Andrew Grieved863d0f2024-12-13 20:13:01391
392 @classmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17393 def deactivate(cls):
394 cls._deactivated = True
Mohamed Heikalabf646e2024-12-12 16:06:05395 tasks_to_terminate: list[Task] = []
Mohamed Heikalb752b772024-11-25 23:05:44396 with cls._lock:
397 while cls._queue:
398 task = cls._queue.pop()
Mohamed Heikalabf646e2024-12-12 16:06:05399 tasks_to_terminate.append(task)
400 # Cancel possibly running tasks.
401 tasks_to_terminate.extend(cls._current_tasks)
402 # Terminate outside lock since task threads need the lock to finish
403 # terminating.
404 for task in tasks_to_terminate:
405 task.terminate()
Mohamed Heikalb752b772024-11-25 23:05:44406
407 @classmethod
408 def cancel_build(cls, build_id):
Mohamed Heikalabf646e2024-12-12 16:06:05409 terminated_pending_tasks: list[Task] = []
410 terminated_current_tasks: list[Task] = []
Mohamed Heikalb752b772024-11-25 23:05:44411 with cls._lock:
Mohamed Heikalabf646e2024-12-12 16:06:05412 # Cancel pending tasks.
Mohamed Heikalb752b772024-11-25 23:05:44413 for task in cls._queue:
Andrew Grieve0d6e8a752025-02-05 21:20:50414 if task.build.id == build_id:
Mohamed Heikalabf646e2024-12-12 16:06:05415 terminated_pending_tasks.append(task)
416 for task in terminated_pending_tasks:
Mohamed Heikalb752b772024-11-25 23:05:44417 cls._queue.remove(task)
Mohamed Heikalabf646e2024-12-12 16:06:05418 # Cancel running tasks.
419 for task in cls._current_tasks:
Andrew Grieve0d6e8a752025-02-05 21:20:50420 if task.build.id == build_id:
Mohamed Heikalabf646e2024-12-12 16:06:05421 terminated_current_tasks.append(task)
422 # Terminate tasks outside lock since task threads need the lock to finish
423 # terminating.
424 for task in terminated_pending_tasks:
425 task.terminate()
426 for task in terminated_current_tasks:
427 task.terminate()
Peter Wencd460ff52021-02-23 22:40:05428
429 @staticmethod
Mohamed Heikalf746b57f2024-11-13 21:20:17430 # pylint: disable=inconsistent-return-statements
Peter Wencd460ff52021-02-23 22:40:05431 def _num_running_processes():
432 with open('/proc/stat') as f:
433 for line in f:
434 if line.startswith('procs_running'):
435 return int(line.rstrip().split()[1])
436 assert False, 'Could not read /proc/stat'
437
Mohamed Heikalf746b57f2024-11-13 21:20:17438 @classmethod
439 def _maybe_start_tasks(cls):
440 if cls._deactivated:
Peter Wencd460ff52021-02-23 22:40:05441 return
442 # Include load avg so that a small dip in the number of currently running
443 # processes will not cause new tasks to be started while the overall load is
444 # heavy.
Mohamed Heikalf746b57f2024-11-13 21:20:17445 cur_load = max(cls._num_running_processes(), os.getloadavg()[0])
Peter Wencd460ff52021-02-23 22:40:05446 num_started = 0
447 # Always start a task if we don't have any running, so that all tasks are
448 # eventually finished. Try starting up tasks when the overall load is light.
449 # Limit to at most 2 new tasks to prevent ramping up too fast. There is a
450 # chance where multiple threads call _maybe_start_tasks and each gets to
451 # spawn up to 2 new tasks, but since the only downside is some build tasks
452 # get worked on earlier rather than later, it is not worth mitigating.
453 while num_started < 2 and (TaskStats.no_running_processes()
454 or num_started + cur_load < os.cpu_count()):
Mohamed Heikalb752b772024-11-25 23:05:44455 with cls._lock:
456 try:
457 next_task = cls._queue.pop()
Mohamed Heikalabf646e2024-12-12 16:06:05458 cls._current_tasks.add(next_task)
Mohamed Heikalb752b772024-11-25 23:05:44459 except IndexError:
460 return
Mohamed Heikalf746b57f2024-11-13 21:20:17461 num_started += next_task.start(cls._maybe_start_tasks)
Peter Wencd460ff52021-02-23 22:40:05462
463
464# TODO(wnwen): Break this into Request (encapsulating what ninja sends) and Task
465# when a Request starts to be run. This would eliminate ambiguity
466# about when and whether _proc/_thread are initialized.
Peter Wenf409c0c2021-02-09 19:33:02467class Task:
Peter Wencd460ff52021-02-23 22:40:05468 """Class to represent one task and operations on it."""
469
Andrew Grieve0d6e8a752025-02-05 21:20:50470 def __init__(self, name: str, build: Build, cmd: List[str], stamp_file: str,
471 options):
Peter Wencd460ff52021-02-23 22:40:05472 self.name = name
Andrew Grieve0d6e8a752025-02-05 21:20:50473 self.build = build
Peter Wencd460ff52021-02-23 22:40:05474 self.cmd = cmd
475 self.stamp_file = stamp_file
Mohamed Heikalf746b57f2024-11-13 21:20:17476 self.options = options
Peter Wencd460ff52021-02-23 22:40:05477 self._terminated = False
Mohamed Heikal9984e432024-12-03 18:21:40478 self._replaced = False
Mohamed Heikalb752b772024-11-25 23:05:44479 self._lock = threading.RLock()
Peter Wencd460ff52021-02-23 22:40:05480 self._proc: Optional[subprocess.Popen] = None
481 self._thread: Optional[threading.Thread] = None
Mohamed Heikalb752b772024-11-25 23:05:44482 self._delete_stamp_thread: Optional[threading.Thread] = None
Peter Wencd460ff52021-02-23 22:40:05483 self._return_code: Optional[int] = None
Peter Wenf409c0c2021-02-09 19:33:02484
Peter Wen6e7e52b2021-02-13 02:39:28485 @property
486 def key(self):
Andrew Grieve0d6e8a752025-02-05 21:20:50487 return (self.build.cwd, self.name)
Peter Wenf409c0c2021-02-09 19:33:02488
Mohamed Heikalabf646e2024-12-12 16:06:05489 def __hash__(self):
Andrew Grieve0d6e8a752025-02-05 21:20:50490 return hash((self.key, self.build.id))
Mohamed Heikalabf646e2024-12-12 16:06:05491
Mohamed Heikalb752b772024-11-25 23:05:44492 def __eq__(self, other):
Andrew Grieve0d6e8a752025-02-05 21:20:50493 return self.key == other.key and self.build is other.build
Mohamed Heikalb752b772024-11-25 23:05:44494
Peter Wencd460ff52021-02-23 22:40:05495 def start(self, on_complete_callback: Callable[[], None]) -> int:
496 """Starts the task if it has not already been terminated.
497
498 Returns the number of processes that have been started. This is called at
499 most once when the task is popped off the task queue."""
Peter Wencd460ff52021-02-23 22:40:05500 with self._lock:
501 if self._terminated:
502 return 0
Mohamed Heikalb752b772024-11-25 23:05:44503
Peter Wencd460ff52021-02-23 22:40:05504 # Use os.nice(19) to ensure the lowest priority (idle) for these analysis
505 # tasks since we want to avoid slowing down the actual build.
506 # TODO(wnwen): Use ionice to reduce resource consumption.
Andrew Grieve0d6e8a752025-02-05 21:20:50507 TaskStats.add_process(self.build.id)
Mohamed Heikalb752b772024-11-25 23:05:44508 set_status(f'STARTING {self.name}',
509 quiet=self.options.quiet,
Andrew Grieve0d6e8a752025-02-05 21:20:50510 build_id=self.build.id)
Peter Wen1cdf05d82022-04-05 17:31:23511 # This use of preexec_fn is sufficiently simple, just one os.nice call.
512 # pylint: disable=subprocess-popen-preexec-fn
Peter Wencd460ff52021-02-23 22:40:05513 self._proc = subprocess.Popen(
514 self.cmd,
515 stdout=subprocess.PIPE,
516 stderr=subprocess.STDOUT,
Andrew Grieve0d6e8a752025-02-05 21:20:50517 cwd=self.build.cwd,
518 env=self.build.env,
Peter Wencd460ff52021-02-23 22:40:05519 text=True,
520 preexec_fn=lambda: os.nice(19),
521 )
522 self._thread = threading.Thread(
523 target=self._complete_when_process_finishes,
524 args=(on_complete_callback, ))
525 self._thread.start()
526 return 1
Peter Wenf409c0c2021-02-09 19:33:02527
Mohamed Heikal9984e432024-12-03 18:21:40528 def terminate(self, replaced=False):
Peter Wencd460ff52021-02-23 22:40:05529 """Can be called multiple times to cancel and ignore the task's output."""
Peter Wencd460ff52021-02-23 22:40:05530 with self._lock:
531 if self._terminated:
532 return
533 self._terminated = True
Mohamed Heikal9984e432024-12-03 18:21:40534 self._replaced = replaced
Mohamed Heikalb752b772024-11-25 23:05:44535
Peter Wencd460ff52021-02-23 22:40:05536 # It is safe to access _proc and _thread outside of _lock since they are
537 # only changed by self.start holding _lock when self._terminate is false.
538 # Since we have just set self._terminate to true inside of _lock, we know
539 # that neither _proc nor _thread will be changed from this point onwards.
Peter Wen6e7e52b2021-02-13 02:39:28540 if self._proc:
541 self._proc.terminate()
542 self._proc.wait()
Peter Wencd460ff52021-02-23 22:40:05543 # Ensure that self._complete is called either by the thread or by us.
Peter Wen6e7e52b2021-02-13 02:39:28544 if self._thread:
545 self._thread.join()
Peter Wencd460ff52021-02-23 22:40:05546 else:
547 self._complete()
Peter Wenf409c0c2021-02-09 19:33:02548
Peter Wencd460ff52021-02-23 22:40:05549 def _complete_when_process_finishes(self,
550 on_complete_callback: Callable[[], None]):
Peter Wen6e7e52b2021-02-13 02:39:28551 assert self._proc
552 # We know Popen.communicate will return a str and not a byte since it is
553 # constructed with text=True.
554 stdout: str = self._proc.communicate()[0]
555 self._return_code = self._proc.returncode
Andrew Grieve0d6e8a752025-02-05 21:20:50556 TaskStats.remove_process(build_id=self.build.id)
Peter Wen6e7e52b2021-02-13 02:39:28557 self._complete(stdout)
Peter Wencd460ff52021-02-23 22:40:05558 on_complete_callback()
Peter Wenf409c0c2021-02-09 19:33:02559
Peter Wencd460ff52021-02-23 22:40:05560 def _complete(self, stdout: str = ''):
561 """Update the user and ninja after the task has run or been terminated.
562
563 This method should only be run once per task. Avoid modifying the task so
564 that this method does not need locking."""
565
Mohamed Heikal9984e432024-12-03 18:21:40566 delete_stamp = False
Mohamed Heikalf746b57f2024-11-13 21:20:17567 status_string = 'FINISHED'
Peter Wen6e7e52b2021-02-13 02:39:28568 if self._terminated:
Mohamed Heikalf746b57f2024-11-13 21:20:17569 status_string = 'TERMINATED'
Mohamed Heikal9984e432024-12-03 18:21:40570 # When tasks are replaced, avoid deleting the stamp file, context:
571 # https://issuetracker.google.com/301961827.
572 if not self._replaced:
573 delete_stamp = True
574 elif stdout or self._return_code != 0:
575 status_string = 'FAILED'
576 delete_stamp = True
577 preamble = [
578 f'FAILED: {self.name}',
579 f'Return code: {self._return_code}',
Andrew Grieve38c80462024-12-17 21:33:27580 'CMD: ' + shlex.join(self.cmd),
Mohamed Heikal9984e432024-12-03 18:21:40581 'STDOUT:',
582 ]
583
584 message = '\n'.join(preamble + [stdout])
Andrew Grieve0d6e8a752025-02-05 21:20:50585 LogfileManager.log_to_file(message, build_id=self.build.id)
Mohamed Heikal9984e432024-12-03 18:21:40586 log(message, quiet=self.options.quiet)
Andrew Grieve0d6e8a752025-02-05 21:20:50587
588 # Add emoji to show that output is from the build server.
589 preamble = [f'⏩ {line}' for line in preamble]
590 remote_message = '\n'.join(preamble + [stdout])
591 # Add a new line at start of message to clearly delineate from previous
592 # output/text already on the remote tty we are printing to.
593 self.build.stdout.write(f'\n{remote_message}')
594 self.build.stdout.flush()
Mohamed Heikal9984e432024-12-03 18:21:40595 if delete_stamp:
596 # Force siso to consider failed targets as dirty.
597 try:
Andrew Grieve0d6e8a752025-02-05 21:20:50598 os.unlink(os.path.join(self.build.cwd, self.stamp_file))
Mohamed Heikal9984e432024-12-03 18:21:40599 except FileNotFoundError:
600 pass
601 else:
602 # We do not care about the action writing a too new mtime. Siso only cares
603 # about the mtime that is recorded in its database at the time the
604 # original action finished.
605 pass
Mohamed Heikalabf646e2024-12-12 16:06:05606 TaskManager.task_done(self)
607 set_status(f'{status_string} {self.name}',
608 quiet=self.options.quiet,
Andrew Grieve0d6e8a752025-02-05 21:20:50609 build_id=self.build.id)
Peter Wenb1f3b1d2021-02-02 21:30:20610
611
Mohamed Heikalb752b772024-11-25 23:05:44612def _handle_add_task(data, current_tasks: Dict[Tuple[str, str], Task], options):
613 """Handle messages of type ADD_TASK."""
Mohamed Heikalf746b57f2024-11-13 21:20:17614 build_id = data['build_id']
Andrew Grieve0d6e8a752025-02-05 21:20:50615 build = BuildManager.get_build(build_id)
616 BuildManager.maybe_init_cwd(build, data.get('cwd'))
Mohamed Heikalb752b772024-11-25 23:05:44617
618 new_task = Task(name=data['name'],
Mohamed Heikalb752b772024-11-25 23:05:44619 cmd=data['cmd'],
Andrew Grieve0d6e8a752025-02-05 21:20:50620 build=build,
Mohamed Heikalb752b772024-11-25 23:05:44621 stamp_file=data['stamp_file'],
622 options=options)
623 existing_task = current_tasks.get(new_task.key)
Mohamed Heikalf746b57f2024-11-13 21:20:17624 if existing_task:
Mohamed Heikal9984e432024-12-03 18:21:40625 existing_task.terminate(replaced=True)
Mohamed Heikalb752b772024-11-25 23:05:44626 current_tasks[new_task.key] = new_task
627
Mohamed Heikalb752b772024-11-25 23:05:44628 TaskManager.add_task(new_task, options)
Mohamed Heikalf746b57f2024-11-13 21:20:17629
630
631def _handle_query_build(data, connection: socket.socket):
Mohamed Heikalb752b772024-11-25 23:05:44632 """Handle messages of type QUERY_BUILD."""
Mohamed Heikalf746b57f2024-11-13 21:20:17633 build_id = data['build_id']
Andrew Grieved863d0f2024-12-13 20:13:01634 response = TaskStats.query_build(build_id)
Mohamed Heikalf746b57f2024-11-13 21:20:17635 try:
636 with connection:
Mohamed Heikalf11b6f32025-01-30 19:44:29637 server_utils.SendMessage(connection, response)
Mohamed Heikalf746b57f2024-11-13 21:20:17638 except BrokenPipeError:
639 # We should not die because the client died.
640 pass
641
642
643def _handle_heartbeat(connection: socket.socket):
Mohamed Heikalb752b772024-11-25 23:05:44644 """Handle messages of type POLL_HEARTBEAT."""
Mohamed Heikalf746b57f2024-11-13 21:20:17645 try:
646 with connection:
Mohamed Heikalf11b6f32025-01-30 19:44:29647 server_utils.SendMessage(connection, {
648 'status': 'OK',
649 'pid': os.getpid(),
650 })
Mohamed Heikalf746b57f2024-11-13 21:20:17651 except BrokenPipeError:
652 # We should not die because the client died.
653 pass
654
655
Mohamed Heikalb752b772024-11-25 23:05:44656def _handle_register_builder(data):
657 """Handle messages of type REGISTER_BUILDER."""
Andrew Grieve0d6e8a752025-02-05 21:20:50658 env = data['env']
659 pid = int(data['builder_pid'])
660 cwd = data['cwd']
661
662 BuildManager.register_builder(env, pid, cwd)
Mohamed Heikalb752b772024-11-25 23:05:44663
664
665def _handle_cancel_build(data):
666 """Handle messages of type CANCEL_BUILD."""
667 build_id = data['build_id']
668 TaskManager.cancel_build(build_id)
669
670
671def _listen_for_request_data(sock: socket.socket):
672 """Helper to encapsulate getting a new message."""
673 while True:
674 conn = sock.accept()[0]
Mohamed Heikalf11b6f32025-01-30 19:44:29675 message = server_utils.ReceiveMessage(conn)
676 if message:
677 yield message, conn
Mohamed Heikalb752b772024-11-25 23:05:44678
679
Mohamed Heikalabf646e2024-12-12 16:06:05680def _register_cleanup_signal_handlers(options):
681 original_sigint_handler = signal.getsignal(signal.SIGINT)
682 original_sigterm_handler = signal.getsignal(signal.SIGTERM)
683
684 def _cleanup(signum, frame):
685 log('STOPPING SERVER...', quiet=options.quiet)
686 # Gracefully shut down the task manager, terminating all queued tasks.
687 TaskManager.deactivate()
688 log('STOPPED', quiet=options.quiet)
689 if signum == signal.SIGINT:
690 if callable(original_sigint_handler):
691 original_sigint_handler(signum, frame)
692 else:
693 raise KeyboardInterrupt()
694 if signum == signal.SIGTERM:
695 # Sometimes sigterm handler is not a callable.
696 if callable(original_sigterm_handler):
697 original_sigterm_handler(signum, frame)
698 else:
699 sys.exit(1)
700
701 signal.signal(signal.SIGINT, _cleanup)
702 signal.signal(signal.SIGTERM, _cleanup)
703
704
Mohamed Heikalf746b57f2024-11-13 21:20:17705def _process_requests(sock: socket.socket, options):
Mohamed Heikalb752b772024-11-25 23:05:44706 """Main loop for build server receiving request messages."""
Peter Wen6e7e52b2021-02-13 02:39:28707 # Since dicts in python can contain anything, explicitly type tasks to help
708 # make static type checking more useful.
709 tasks: Dict[Tuple[str, str], Task] = {}
Mohamed Heikalf746b57f2024-11-13 21:20:17710 log(
711 'READY... Remember to set android_static_analysis="build_server" in '
712 'args.gn files',
713 quiet=options.quiet)
Mohamed Heikalabf646e2024-12-12 16:06:05714 _register_cleanup_signal_handlers(options)
Mohamed Heikalf746b57f2024-11-13 21:20:17715 # pylint: disable=too-many-nested-blocks
Mohamed Heikalabf646e2024-12-12 16:06:05716 while True:
717 try:
718 for data, connection in _listen_for_request_data(sock):
719 message_type = data.get('message_type', server_utils.ADD_TASK)
720 if message_type == server_utils.POLL_HEARTBEAT:
721 _handle_heartbeat(connection)
Mohamed Heikalf11b6f32025-01-30 19:44:29722 elif message_type == server_utils.ADD_TASK:
Mohamed Heikalabf646e2024-12-12 16:06:05723 connection.close()
724 _handle_add_task(data, tasks, options)
Mohamed Heikalf11b6f32025-01-30 19:44:29725 elif message_type == server_utils.QUERY_BUILD:
Mohamed Heikalabf646e2024-12-12 16:06:05726 _handle_query_build(data, connection)
Mohamed Heikalf11b6f32025-01-30 19:44:29727 elif message_type == server_utils.REGISTER_BUILDER:
Mohamed Heikalabf646e2024-12-12 16:06:05728 connection.close()
729 _handle_register_builder(data)
Mohamed Heikalf11b6f32025-01-30 19:44:29730 elif message_type == server_utils.CANCEL_BUILD:
Mohamed Heikalabf646e2024-12-12 16:06:05731 connection.close()
732 _handle_cancel_build(data)
Mohamed Heikalf11b6f32025-01-30 19:44:29733 else:
734 connection.close()
Mohamed Heikalabf646e2024-12-12 16:06:05735 except TimeoutError:
736 # If we have not received a new task in a while and do not have any
737 # pending tasks or running builds, then exit. Otherwise keep waiting.
738 if (TaskStats.num_pending_tasks() == 0
739 and not BuildManager.has_live_builds() and options.exit_on_idle):
Mohamed Heikalb752b772024-11-25 23:05:44740 break
Mohamed Heikalabf646e2024-12-12 16:06:05741 except KeyboardInterrupt:
742 break
Mohamed Heikalf746b57f2024-11-13 21:20:17743
744
Mohamed Heikalf11b6f32025-01-30 19:44:29745def query_build_info(build_id=None):
Mohamed Heikalb752b772024-11-25 23:05:44746 """Communicates with the main server to query build info."""
Mohamed Heikalf11b6f32025-01-30 19:44:29747 return _send_message_with_response({
748 'message_type': server_utils.QUERY_BUILD,
749 'build_id': build_id,
750 })
Mohamed Heikalf746b57f2024-11-13 21:20:17751
752
753def _wait_for_build(build_id):
Mohamed Heikalb752b772024-11-25 23:05:44754 """Comunicates with the main server waiting for a build to complete."""
Mohamed Heikalf746b57f2024-11-13 21:20:17755 start_time = datetime.datetime.now()
756 while True:
Andrew Grieved863d0f2024-12-13 20:13:01757 try:
758 build_info = query_build_info(build_id)['builds'][0]
759 except ConnectionRefusedError:
760 print('No server running. It likely finished all tasks.')
761 print('You can check $OUTDIR/buildserver.log.0 to be sure.')
762 return 0
763
Mohamed Heikalf746b57f2024-11-13 21:20:17764 pending_tasks = build_info['pending_tasks']
Mohamed Heikalf746b57f2024-11-13 21:20:17765
766 if pending_tasks == 0:
767 print(f'\nAll tasks completed for build_id: {build_id}.')
768 return 0
769
770 current_time = datetime.datetime.now()
771 duration = current_time - start_time
772 print(f'\rWaiting for {pending_tasks} tasks [{str(duration)}]\033[K',
773 end='',
774 flush=True)
775 time.sleep(1)
776
777
Mohamed Heikalf11b6f32025-01-30 19:44:29778def _wait_for_idle():
779 """Communicates with the main server waiting for all builds to complete."""
780 start_time = datetime.datetime.now()
781 while True:
782 try:
783 builds = query_build_info()['builds']
784 except ConnectionRefusedError:
785 print('No server running. It likely finished all tasks.')
786 print('You can check $OUTDIR/buildserver.log.0 to be sure.')
787 return 0
788
789 all_pending_tasks = 0
790 all_completed_tasks = 0
791 for build_info in builds:
792 pending_tasks = build_info['pending_tasks']
793 completed_tasks = build_info['completed_tasks']
794 active = build_info['is_active']
795 # Ignore completed builds.
796 if active or pending_tasks:
797 all_pending_tasks += pending_tasks
798 all_completed_tasks += completed_tasks
799 total_tasks = all_pending_tasks + all_completed_tasks
800
801 if all_pending_tasks == 0:
802 print('\nServer Idle, All tasks complete.')
803 return 0
804
805 current_time = datetime.datetime.now()
806 duration = current_time - start_time
807 print(
808 f'\rWaiting for {all_pending_tasks} remaining tasks. '
809 f'({all_completed_tasks}/{total_tasks} tasks complete) '
810 f'[{str(duration)}]\033[K',
811 end='',
812 flush=True)
813 time.sleep(0.5)
814
815
Mohamed Heikalf746b57f2024-11-13 21:20:17816def _check_if_running():
Mohamed Heikalb752b772024-11-25 23:05:44817 """Communicates with the main server to make sure its running."""
Mohamed Heikalf746b57f2024-11-13 21:20:17818 with socket.socket(socket.AF_UNIX) as sock:
819 try:
820 sock.connect(server_utils.SOCKET_ADDRESS)
Mohamed Heikalf11b6f32025-01-30 19:44:29821 except OSError:
Mohamed Heikalf746b57f2024-11-13 21:20:17822 print('Build server is not running and '
823 'android_static_analysis="build_server" is set.\nPlease run '
824 'this command in a separate terminal:\n\n'
825 '$ build/android/fast_local_dev_server.py\n')
826 return 1
827 else:
828 return 0
829
830
Mohamed Heikalb752b772024-11-25 23:05:44831def _send_message_and_close(message_dict):
832 with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock:
833 sock.connect(server_utils.SOCKET_ADDRESS)
Mohamed Heikalf11b6f32025-01-30 19:44:29834 sock.settimeout(1)
835 server_utils.SendMessage(sock, message_dict)
836
837
838def _send_message_with_response(message_dict):
839 with contextlib.closing(socket.socket(socket.AF_UNIX)) as sock:
840 sock.connect(server_utils.SOCKET_ADDRESS)
841 sock.settimeout(1)
842 server_utils.SendMessage(sock, message_dict)
843 return server_utils.ReceiveMessage(sock)
Mohamed Heikalb752b772024-11-25 23:05:44844
845
846def _send_cancel_build(build_id):
847 _send_message_and_close({
848 'message_type': server_utils.CANCEL_BUILD,
849 'build_id': build_id,
850 })
851 return 0
852
853
Andrew Grieve0d6e8a752025-02-05 21:20:50854def _register_builder(build_id, builder_pid, output_directory):
Mohamed Heikalb752b772024-11-25 23:05:44855 for _attempt in range(3):
856 try:
Andrew Grieve0d6e8a752025-02-05 21:20:50857 # Ensure environment variables that the server expects to be there are
858 # present.
859 server_utils.AssertEnvironmentVariables()
860
Mohamed Heikalb752b772024-11-25 23:05:44861 _send_message_and_close({
862 'message_type': server_utils.REGISTER_BUILDER,
Andrew Grieve0d6e8a752025-02-05 21:20:50863 'env': dict(os.environ),
Mohamed Heikalb752b772024-11-25 23:05:44864 'builder_pid': builder_pid,
Andrew Grieve0d6e8a752025-02-05 21:20:50865 'cwd': output_directory,
Mohamed Heikalb752b772024-11-25 23:05:44866 })
867 return 0
Mohamed Heikalf11b6f32025-01-30 19:44:29868 except OSError:
Mohamed Heikalb752b772024-11-25 23:05:44869 time.sleep(0.05)
870 print(f'Failed to register builer for build_id={build_id}.')
871 return 1
872
873
Mohamed Heikalf11b6f32025-01-30 19:44:29874def poll_server(retries=3):
875 """Communicates with the main server to query build info."""
876 for _attempt in range(retries):
877 try:
878 response = _send_message_with_response(
879 {'message_type': server_utils.POLL_HEARTBEAT})
880 if response:
881 break
882 except OSError:
883 time.sleep(0.05)
884 else:
885 return None
886 return response['pid']
887
888
Andrew Grieved863d0f2024-12-13 20:13:01889def _print_build_status_all():
890 try:
891 query_data = query_build_info(None)
892 except ConnectionRefusedError:
893 print('No server running. Consult $OUTDIR/buildserver.log.0')
894 return 0
895 builds = query_data['builds']
896 pid = query_data['pid']
897 all_active_tasks = []
898 print(f'Build server (PID={pid}) has {len(builds)} registered builds')
899 for build_info in builds:
900 build_id = build_info['build_id']
901 pending_tasks = build_info['pending_tasks']
902 completed_tasks = build_info['completed_tasks']
903 active_tasks = build_info['active_tasks']
904 out_dir = build_info['outdir']
905 active = build_info['is_active']
906 total_tasks = pending_tasks + completed_tasks
907 all_active_tasks += active_tasks
908 if total_tasks == 0 and not active:
909 status = 'Finished without any jobs'
910 else:
911 if active:
912 status = 'Siso still running'
913 else:
914 status = 'Siso finished'
915 if out_dir:
916 status += f' in {out_dir}'
917 status += f'. Completed [{completed_tasks}/{total_tasks}].'
918 if completed_tasks < total_tasks:
919 status += f' {len(active_tasks)} tasks currently executing'
920 print(f'{build_id}: {status}')
921 if all_active_tasks:
922 total = len(all_active_tasks)
923 to_show = min(4, total)
924 print(f'Currently executing (showing {to_show} of {total}):')
925 for cmd in sorted(all_active_tasks)[:to_show]:
926 truncated = shlex.join(cmd)
927 if len(truncated) > 200:
928 truncated = truncated[:200] + '...'
929 print(truncated)
930 return 0
931
932
Mohamed Heikal6b56cf62024-12-10 23:14:55933def _print_build_status(build_id):
Andrew Grieved863d0f2024-12-13 20:13:01934 try:
935 build_info = query_build_info(build_id)['builds'][0]
936 except ConnectionRefusedError:
937 print('No server running. Consult $OUTDIR/buildserver.log.0')
938 return 0
Mohamed Heikal6b56cf62024-12-10 23:14:55939 pending_tasks = build_info['pending_tasks']
940 completed_tasks = build_info['completed_tasks']
941 total_tasks = pending_tasks + completed_tasks
942
943 # Print nothing if we never got any tasks.
944 if completed_tasks:
Andrew Grieve52011412025-02-03 18:57:59945 print(f'Build Server Status: [{completed_tasks}/{total_tasks}]')
Mohamed Heikal6b56cf62024-12-10 23:14:55946 if pending_tasks:
Mohamed Heikal6b56cf62024-12-10 23:14:55947 server_path = os.path.relpath(str(server_utils.SERVER_SCRIPT))
Andrew Grieve52011412025-02-03 18:57:59948 print('To wait for jobs:', shlex.join([server_path, '--wait-for-idle']))
Mohamed Heikal6b56cf62024-12-10 23:14:55949 return 0
950
951
Mohamed Heikalf746b57f2024-11-13 21:20:17952def _wait_for_task_requests(args):
953 with socket.socket(socket.AF_UNIX) as sock:
954 sock.settimeout(_SOCKET_TIMEOUT)
955 try:
956 sock.bind(server_utils.SOCKET_ADDRESS)
Mohamed Heikalf11b6f32025-01-30 19:44:29957 except OSError as e:
Mohamed Heikalf746b57f2024-11-13 21:20:17958 # errno 98 is Address already in use
959 if e.errno == 98:
Mohamed Heikal08b467e02025-01-27 20:54:25960 if not args.quiet:
Mohamed Heikalf11b6f32025-01-30 19:44:29961 pid = poll_server()
962 print(f'Another instance is already running (pid: {pid}).',
963 file=sys.stderr)
Mohamed Heikalf746b57f2024-11-13 21:20:17964 return 1
965 raise
966 sock.listen()
967 _process_requests(sock, args)
968 return 0
Peter Wenb1f3b1d2021-02-02 21:30:20969
970
971def main():
Andrew Grieved863d0f2024-12-13 20:13:01972 # pylint: disable=too-many-return-statements
Peter Wenf409c0c2021-02-09 19:33:02973 parser = argparse.ArgumentParser(description=__doc__)
Peter Wend70f4862022-02-02 16:00:16974 parser.add_argument(
975 '--fail-if-not-running',
976 action='store_true',
977 help='Used by GN to fail fast if the build server is not running.')
Mohamed Heikalf746b57f2024-11-13 21:20:17978 parser.add_argument(
979 '--exit-on-idle',
980 action='store_true',
981 help='Server started on demand. Exit when all tasks run out.')
982 parser.add_argument('--quiet',
983 action='store_true',
984 help='Do not output status updates.')
985 parser.add_argument('--wait-for-build',
986 metavar='BUILD_ID',
987 help='Wait for build server to finish with all tasks '
988 'for BUILD_ID and output any pending messages.')
Mohamed Heikalf11b6f32025-01-30 19:44:29989 parser.add_argument('--wait-for-idle',
990 action='store_true',
991 help='Wait for build server to finish with all '
992 'pending tasks.')
Mohamed Heikal6b56cf62024-12-10 23:14:55993 parser.add_argument('--print-status',
994 metavar='BUILD_ID',
995 help='Print the current state of a build.')
Andrew Grieved863d0f2024-12-13 20:13:01996 parser.add_argument('--print-status-all',
997 action='store_true',
998 help='Print the current state of all active builds.')
Mohamed Heikalb752b772024-11-25 23:05:44999 parser.add_argument(
1000 '--register-build-id',
1001 metavar='BUILD_ID',
1002 help='Inform the build server that a new build has started.')
Andrew Grieve0d6e8a752025-02-05 21:20:501003 parser.add_argument('--output-directory',
1004 help='Build directory (use with --register-build-id)')
Mohamed Heikalb752b772024-11-25 23:05:441005 parser.add_argument('--builder-pid',
1006 help='Builder process\'s pid for build BUILD_ID.')
1007 parser.add_argument('--cancel-build',
1008 metavar='BUILD_ID',
1009 help='Cancel all pending and running tasks for BUILD_ID.')
Peter Wend70f4862022-02-02 16:00:161010 args = parser.parse_args()
1011 if args.fail_if_not_running:
Mohamed Heikalf746b57f2024-11-13 21:20:171012 return _check_if_running()
1013 if args.wait_for_build:
1014 return _wait_for_build(args.wait_for_build)
Mohamed Heikalf11b6f32025-01-30 19:44:291015 if args.wait_for_idle:
1016 return _wait_for_idle()
Mohamed Heikal6b56cf62024-12-10 23:14:551017 if args.print_status:
1018 return _print_build_status(args.print_status)
Andrew Grieved863d0f2024-12-13 20:13:011019 if args.print_status_all:
1020 return _print_build_status_all()
Mohamed Heikalb752b772024-11-25 23:05:441021 if args.register_build_id:
Andrew Grieve0d6e8a752025-02-05 21:20:501022 return _register_builder(args.register_build_id, args.builder_pid,
1023 args.output_directory)
Mohamed Heikalb752b772024-11-25 23:05:441024 if args.cancel_build:
1025 return _send_cancel_build(args.cancel_build)
Mohamed Heikalf746b57f2024-11-13 21:20:171026 return _wait_for_task_requests(args)
Peter Wenb1f3b1d2021-02-02 21:30:201027
1028
1029if __name__ == '__main__':
Mohamed Heikalb752b772024-11-25 23:05:441030 sys.excepthook = _exception_hook
Peter Wenb1f3b1d2021-02-02 21:30:201031 sys.exit(main())