6.0 KB199 lines
Blame
1/**
2 * Copyright (c) Meta Platforms, Inc. and affiliates.
3 *
4 * This source code is licensed under the MIT license found in the
5 * LICENSE file in the root directory of this source tree.
6 */
7
8import type {CommitInfo, SmartlogCommits} from 'isl/src/types';
9import type {EjecaChildProcess, EjecaError, EjecaReturn} from 'shared/ejeca';
10
11import os from 'node:os';
12import {truncate} from 'shared/utils';
13
14export function sleep(timeMs: number): Promise<void> {
15 return new Promise(res => setTimeout(res, timeMs));
16}
17
18export function firstOfIterable<T>(iterable: IterableIterator<T>): T | undefined {
19 return iterable.next().value;
20}
21
22/**
23 * Limits async function execution parallelism to only one at a time.
24 * Hence, if a call is already running, it will wait for it to finish,
25 * then start the next async execution, but if called again while not finished,
26 * it will return the scheduled execution promise.
27 *
28 * Sample Usage:
29 * ```
30 * let i = 1;
31 * const oneExecAtATime = serializeAsyncCall(() => {
32 * return new Promise((resolve, reject) => {
33 * setTimeout(200, () => resolve(i++));
34 * });
35 * });
36 *
37 * const result1Promise = oneExecAtATime(); // Start an async, and resolve to 1 in 200 ms.
38 * const result2Promise = oneExecAtATime(); // Schedule the next async, and resolve to 2 in 400 ms.
39 * const result3Promise = oneExecAtATime(); // Reuse scheduled promise and resolve to 2 in 400 ms.
40 * ```
41 */
42export function serializeAsyncCall<T>(asyncFun: () => Promise<T>): () => Promise<T> {
43 let scheduledCall: Promise<T> | undefined = undefined;
44 let pendingCall: Promise<undefined> | undefined = undefined;
45 const startAsyncCall = () => {
46 const resultPromise = asyncFun();
47 pendingCall = resultPromise.then(
48 () => (pendingCall = undefined),
49 () => (pendingCall = undefined),
50 );
51 return resultPromise;
52 };
53 const callNext = () => {
54 scheduledCall = undefined;
55 return startAsyncCall();
56 };
57 const scheduleNextCall = () => {
58 if (scheduledCall == null) {
59 if (pendingCall == null) {
60 throw new Error('pendingCall must not be null!');
61 }
62 scheduledCall = pendingCall.then(callNext, callNext);
63 }
64 return scheduledCall;
65 };
66 return () => {
67 if (pendingCall == null) {
68 return startAsyncCall();
69 } else {
70 return scheduleNextCall();
71 }
72 };
73}
74
75/**
76 * Kill `child` on `AbortSignal`.
77 *
78 * This is slightly more robust than execa 6.0 and nodejs' `signal` support:
79 * if a process was stopped (by `SIGTSTP` or `SIGSTOP`), it can still be killed.
80 */
81export function handleAbortSignalOnProcess(child: EjecaChildProcess, signal: AbortSignal) {
82 signal.addEventListener('abort', () => {
83 if (os.platform() == 'win32') {
84 // Signals are ignored on Windows.
85 // execa's default forceKillAfterTimeout behavior does not
86 // make sense for Windows. Disable it explicitly.
87 child.kill('SIGKILL', {forceKillAfterTimeout: false});
88 } else {
89 // If the process is stopped (ex. Ctrl+Z, kill -STOP), make it
90 // continue first so it can respond to signals including SIGKILL.
91 child.kill('SIGCONT');
92 // A good citizen process should exit soon after receiving SIGTERM.
93 // In case it doesn't, send SIGKILL after 5 seconds.
94 child.kill('SIGTERM', {forceKillAfterTimeout: 5000});
95 }
96 });
97}
98
99/**
100 * Given a list of commits and a starting commit,
101 * traverse up the chain of `parents` until we find a public commit
102 */
103export function findPublicAncestor(
104 allCommits: SmartlogCommits | undefined,
105 from: CommitInfo,
106): CommitInfo | undefined {
107 let publicCommit: CommitInfo | undefined;
108 if (allCommits != null) {
109 const map = new Map(allCommits.map(commit => [commit.hash, commit]));
110
111 let current: CommitInfo | undefined = from;
112 while (current != null) {
113 if (current.phase === 'public') {
114 publicCommit = current;
115 break;
116 }
117 if (current.parents[0] == null) {
118 break;
119 }
120 current = map.get(current.parents[0]);
121 }
122 }
123
124 return publicCommit;
125}
126
127/**
128 * Run a command that is expected to produce JSON output.
129 * Return a JSON object. On error, the JSON object has property "error".
130 */
131export function parseExecJson<T>(
132 exec: Promise<EjecaReturn>,
133 reply: (parsed?: T, error?: string) => void,
134) {
135 exec
136 .then(result => {
137 const stdout = result.stdout;
138 try {
139 const parsed = JSON.parse(stdout);
140 if (parsed.error != null) {
141 reply(undefined, parsed.error);
142 } else {
143 reply(parsed as T);
144 }
145 } catch (err) {
146 const msg = `Cannot parse ${truncate(
147 result.escapedCommand,
148 )} output. (error: ${err}, stdout: ${stdout})`;
149 reply(undefined, msg);
150 }
151 })
152 .catch(err => {
153 // Try extracting error from stdout '{error: message}'.
154 try {
155 const parsed = JSON.parse(err.stdout);
156 if (parsed.error != null) {
157 reply(undefined, parsed.error);
158 return;
159 }
160 } catch {}
161 // Fallback to general error.
162 const msg = `Cannot run ${truncate(err.escapedCommand)}. (error: ${err})`;
163 reply(undefined, msg);
164 });
165}
166
167export type EjecaSpawnError = Error & {code: string; path: string};
168
169/**
170 * True if an Ejeca spawned process exits non-zero or is killed.
171 * @see {EjecaSpawnError} for when a process fails to spawn in the first place (e.g. ENOENT).
172 */
173export function isEjecaError(s: unknown): s is EjecaError {
174 return s != null && typeof s === 'object' && 'exitCode' in s;
175}
176
177/** True when Ejeca fails to spawn a process, e.g. ENOENT.
178 * (as opposed to the command spawning, then exiting non-zero) */
179export function isEjecaSpawnError(s: unknown): s is EjecaSpawnError {
180 return s != null && typeof s === 'object' && 'code' in s;
181}
182
183export function fromEntries<V>(entries: Array<[string, V]>): {
184 [key: string]: V;
185} {
186 // Object.fromEntries() is available in Node v12 and later.
187 if (typeof Object.fromEntries === 'function') {
188 return Object.fromEntries(entries);
189 }
190
191 const obj: {
192 [key: string]: V;
193 } = {};
194 for (const [key, value] of entries) {
195 obj[key] = value;
196 }
197 return obj;
198}
199