Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 1 | #!/usr/bin/env python3 |
| 2 | # Copyright 2021 The Chromium Authors. All rights reserved. |
| 3 | # 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 | |
| 7 | import argparse |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 8 | import dataclasses |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 9 | import json |
| 10 | import logging |
| 11 | import os |
| 12 | import socket |
| 13 | import subprocess |
| 14 | import sys |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 15 | import threading |
| 16 | import time |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 17 | |
| 18 | sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp')) |
| 19 | from util import server_utils |
| 20 | |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 21 | # TODO(wnwen): Add type annotations. |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 22 | |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 23 | |
| 24 | @dataclasses.dataclass |
| 25 | class Task: |
| 26 | """Class to represent a single build task.""" |
| 27 | name: str |
| 28 | stamp_path: str |
| 29 | proc: subprocess.Popen |
| 30 | terminated: bool = False |
| 31 | |
| 32 | def is_running(self): |
| 33 | return self.proc.poll() is None |
| 34 | |
| 35 | |
| 36 | def _log(msg): |
| 37 | # Subtract one from the total number of threads so we don't count the main |
| 38 | # thread. |
| 39 | num = threading.active_count() - 1 |
| 40 | # \r to return the carriage to the beginning of line. |
| 41 | # \033[K to replace the normal \n to erase until the end of the line. |
| 42 | # Avoid the default line ending so the next \r overwrites the same line just |
| 43 | # like ninja's output. |
| 44 | # TODO(wnwen): When there is just one thread left and it finishes, the last |
| 45 | # output is "[1 thread] FINISHED //...". It may be better to show |
| 46 | # "ALL DONE" or something to that effect. |
| 47 | print(f'\r[{num} thread{"" if num == 1 else "s"}] {msg}\033[K', end='') |
| 48 | |
| 49 | |
| 50 | def _run_when_completed(task): |
| 51 | _log(f'RUNNING {task.name}') |
| 52 | stdout, _ = task.proc.communicate() |
| 53 | |
| 54 | # Avoid printing anything since the task is now outdated. |
| 55 | if task.terminated: |
| 56 | return |
| 57 | |
| 58 | _log(f'FINISHED {task.name}') |
| 59 | if stdout: |
| 60 | # An extra new line is needed since _log does not end with a new line. |
| 61 | print(f'\nFAILED: {task.name}') |
| 62 | print(' '.join(task.proc.args)) |
| 63 | print(stdout.decode('utf-8')) |
| 64 | # Force ninja to always re-run failed tasks. |
| 65 | try: |
| 66 | os.unlink(task.stamp_path) |
| 67 | except FileNotFoundError: |
| 68 | pass |
| 69 | # TODO(wnwen): Reset timestamp for stamp file to when the original request was |
| 70 | # sent since otherwise if a file is edited while the task is |
| 71 | # running, then the target would appear newer than the edit. |
| 72 | |
| 73 | |
| 74 | def _init_task(*, name, cwd, cmd, stamp_file): |
| 75 | _log(f'STARTING {name}') |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 76 | # The environment variable forces the script to actually run in order to avoid |
| 77 | # infinite recursion. |
| 78 | env = os.environ.copy() |
| 79 | env[server_utils.BUILD_SERVER_ENV_VARIABLE] = '1' |
| 80 | # Use os.nice(19) to ensure the lowest priority (idle) for these analysis |
| 81 | # tasks since we want to avoid slowing down the actual build. |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 82 | proc = subprocess.Popen( |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 83 | cmd, |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 84 | stdout=subprocess.PIPE, |
| 85 | stderr=subprocess.STDOUT, # Interleave outputs to match running locally. |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 86 | cwd=cwd, |
| 87 | env=env, |
| 88 | preexec_fn=lambda: os.nice(19), |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 89 | ) |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 90 | task = Task(name=name, stamp_path=os.path.join(cwd, stamp_file), proc=proc) |
| 91 | # Set daemon=True so that one Ctrl-C terminates the server. |
| 92 | # TODO(wnwen): Handle Ctrl-C and updating stamp files before exiting. |
| 93 | io_thread = threading.Thread(target=_run_when_completed, |
| 94 | args=(task, ), |
| 95 | daemon=True) |
| 96 | io_thread.start() |
| 97 | return task |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 98 | |
| 99 | |
| 100 | def _listen_for_request_data(sock): |
| 101 | while True: |
| 102 | conn, _ = sock.accept() |
| 103 | received = [] |
| 104 | with conn: |
| 105 | while True: |
| 106 | data = conn.recv(4096) |
| 107 | if not data: |
| 108 | break |
| 109 | received.append(data) |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 110 | if received: |
| 111 | yield json.loads(b''.join(received)) |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 112 | |
| 113 | |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 114 | def _terminate_task(task): |
| 115 | task.terminated = True |
| 116 | task.proc.terminate() |
| 117 | task.proc.wait() |
| 118 | _log(f'TERMINATED {task.name}') |
| 119 | |
| 120 | |
| 121 | def _process_requests(sock): |
| 122 | tasks = {} |
| 123 | # TODO(wnwen): Record and update start_time whenever we go from 0 to 1 active |
| 124 | # threads so logging can display a reasonable time, e.g. |
| 125 | # "[2 threads : 32.553s ] RUNNING //..." |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 126 | for data in _listen_for_request_data(sock): |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 127 | key = (data['name'], data['cwd']) |
| 128 | task = tasks.get(key) |
| 129 | if task and task.is_running(): |
| 130 | _terminate_task(task) |
| 131 | tasks[key] = _init_task(**data) |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 132 | |
| 133 | |
| 134 | def main(): |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 135 | parser = argparse.ArgumentParser(description=__doc__) |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 136 | args = parser.parse_args() |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 137 | with socket.socket(socket.AF_UNIX) as sock: |
Peter Wen | f409c0c | 2021-02-09 19:33:02 | [diff] [blame^] | 138 | sock.bind(server_utils.SOCKET_ADDRESS) |
| 139 | sock.listen() |
| 140 | _process_requests(sock) |
Peter Wen | b1f3b1d | 2021-02-02 21:30:20 | [diff] [blame] | 141 | |
| 142 | |
| 143 | if __name__ == '__main__': |
| 144 | sys.exit(main()) |