blob: b47d6013ee4ec71040462ad6517ff8a29d1f0187 [file] [log] [blame]
Peter Wenb1f3b1d2021-02-02 21:30:201#!/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
7import argparse
Peter Wenf409c0c2021-02-09 19:33:028import dataclasses
Peter Wenb1f3b1d2021-02-02 21:30:209import json
10import logging
11import os
12import socket
13import subprocess
14import sys
Peter Wenf409c0c2021-02-09 19:33:0215import threading
16import time
Peter Wenb1f3b1d2021-02-02 21:30:2017
18sys.path.append(os.path.join(os.path.dirname(__file__), 'gyp'))
19from util import server_utils
20
Peter Wenf409c0c2021-02-09 19:33:0221# TODO(wnwen): Add type annotations.
Peter Wenb1f3b1d2021-02-02 21:30:2022
Peter Wenf409c0c2021-02-09 19:33:0223
24@dataclasses.dataclass
25class 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
36def _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
50def _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
74def _init_task(*, name, cwd, cmd, stamp_file):
75 _log(f'STARTING {name}')
Peter Wenb1f3b1d2021-02-02 21:30:2076 # 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 Wenf409c0c2021-02-09 19:33:0282 proc = subprocess.Popen(
Peter Wenb1f3b1d2021-02-02 21:30:2083 cmd,
Peter Wenf409c0c2021-02-09 19:33:0284 stdout=subprocess.PIPE,
85 stderr=subprocess.STDOUT, # Interleave outputs to match running locally.
Peter Wenb1f3b1d2021-02-02 21:30:2086 cwd=cwd,
87 env=env,
88 preexec_fn=lambda: os.nice(19),
Peter Wenb1f3b1d2021-02-02 21:30:2089 )
Peter Wenf409c0c2021-02-09 19:33:0290 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 Wenb1f3b1d2021-02-02 21:30:2098
99
100def _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 Wenf409c0c2021-02-09 19:33:02110 if received:
111 yield json.loads(b''.join(received))
Peter Wenb1f3b1d2021-02-02 21:30:20112
113
Peter Wenf409c0c2021-02-09 19:33:02114def _terminate_task(task):
115 task.terminated = True
116 task.proc.terminate()
117 task.proc.wait()
118 _log(f'TERMINATED {task.name}')
119
120
121def _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 Wenb1f3b1d2021-02-02 21:30:20126 for data in _listen_for_request_data(sock):
Peter Wenf409c0c2021-02-09 19:33:02127 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 Wenb1f3b1d2021-02-02 21:30:20132
133
134def main():
Peter Wenf409c0c2021-02-09 19:33:02135 parser = argparse.ArgumentParser(description=__doc__)
Peter Wenb1f3b1d2021-02-02 21:30:20136 args = parser.parse_args()
Peter Wenb1f3b1d2021-02-02 21:30:20137 with socket.socket(socket.AF_UNIX) as sock:
Peter Wenf409c0c2021-02-09 19:33:02138 sock.bind(server_utils.SOCKET_ADDRESS)
139 sock.listen()
140 _process_requests(sock)
Peter Wenb1f3b1d2021-02-02 21:30:20141
142
143if __name__ == '__main__':
144 sys.exit(main())