-
Notifications
You must be signed in to change notification settings - Fork 16
Expand file tree
/
Copy pathcodeql-cli.ts
More file actions
260 lines (232 loc) · 7.78 KB
/
Copy pathcodeql-cli.ts
File metadata and controls
260 lines (232 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
import { ChildProcessWithoutNullStreams, spawn } from "node:child_process";
import { EOL } from "node:os";
import { Writable } from "node:stream";
import { debug, error } from "@actions/core";
import { getExecOutput } from "@actions/exec";
import { asError } from "./errors";
interface CodeqlCliOutput {
exitCode: number;
stdout: string;
stderr: string;
}
export interface CodeqlCli {
run(args: string[]): Promise<CodeqlCliOutput>;
}
export class BaseCodeqlCli implements CodeqlCli {
constructor(private readonly codeqlPath: string) {}
async run(args: string[]): Promise<CodeqlCliOutput> {
const { stdout, stderr, exitCode } = await getExecOutput(
this.codeqlPath,
args,
);
return { stdout, stderr, exitCode };
}
}
export class CodeqlCliServer implements CodeqlCli {
/**
* The process for the cli server, or undefined if one doesn't exist yet
*/
private process?: ChildProcessWithoutNullStreams;
/**
* Queue of future commands
*/
private readonly commandQueue: Array<() => void> = [];
/**
* Whether a command is running
*/
private commandInProcess: boolean = false;
/**
* A buffer with a single null byte.
*/
private readonly nullBuffer: Buffer = Buffer.alloc(1);
constructor(private readonly codeqlPath: string) {}
run(args: string[]): Promise<CodeqlCliOutput> {
return new Promise((resolve, reject) => {
const callback = (): void => {
try {
// eslint-disable-next-line github/no-then -- we might not run immediately
this.runCommandImmediately(args).then(resolve, reject);
} catch (err) {
reject(asError(err));
}
};
// If the server is not running a command, then run the given command immediately,
// otherwise add to the queue
if (this.commandInProcess) {
this.commandQueue.push(callback);
} else {
callback();
}
});
}
shutdown() {
this.killProcessIfRunning();
}
/**
* Launch the cli server
*/
private launchProcess(): ChildProcessWithoutNullStreams {
const args = ["execute", "cli-server"];
// Start the server process.
const argsString = args.join(" ");
void debug(`Starting using CodeQL CLI: ${this.codeqlPath} ${argsString}`);
const child = spawn(this.codeqlPath, args);
if (!child || !child.pid) {
throw new Error(
`Failed to start using command ${this.codeqlPath} ${argsString}.`,
);
}
let lastStdout: string | Buffer | undefined = undefined;
child.stdout.on("data", (data: string | Buffer) => {
lastStdout = data;
});
// Set up event listeners.
child.on("close", (code, signal) => {
if (code !== null) {
debug(`Child process exited with code ${code}`);
}
if (signal) {
debug(`Child process exited due to receipt of signal ${signal}`);
}
// If the process exited abnormally, log the last stdout message,
// It may be from the jvm.
if (code !== 0 && lastStdout !== undefined) {
debug(`Last stdout was "${lastStdout.toString()}"`);
}
});
return child;
}
private async runCommandImmediately(
args: string[],
): Promise<CodeqlCliOutput> {
const stderrBuffers: Buffer[] = [];
const parentProcess = process;
if (this.commandInProcess) {
throw new Error(
"runCommandImmediately called while command was in process",
);
}
this.commandInProcess = true;
try {
// Launch the process if it doesn't exist
if (!this.process) {
this.process = this.launchProcess();
}
const process = this.process;
// The array of fragments of stdout
const stdoutBuffers: Buffer[] = [];
void debug(`Running using CodeQL CLI: ${args.join(" ")}`);
try {
await new Promise<void>((resolve, reject) => {
// Follow standard Actions behavior and print any lines to stdout/stderr immediately
let parentStdout: Writable;
if (parentProcess.stdout instanceof Writable) {
parentStdout = parentProcess.stdout;
}
let parentStderr: Writable | undefined = undefined;
if (parentProcess.stderr instanceof Writable) {
parentStderr = parentProcess.stderr;
}
// Start listening to stdout
process.stdout.addListener("data", (newData: Buffer) => {
stdoutBuffers.push(newData);
if (
newData.length > 0 &&
newData.readUInt8(newData.length - 1) === 0
) {
if (newData.length > 1) {
parentStdout?.write(newData.subarray(0, newData.length - 1));
}
} else {
parentStdout?.write(newData);
}
// If the buffer ends in '0' then exit.
// We don't have to check the middle as no output will be written after the null until
// the next command starts
if (
newData.length > 0 &&
newData.readUInt8(newData.length - 1) === 0
) {
resolve();
}
});
// Listen to stderr
process.stderr.addListener("data", (newData: Buffer) => {
stderrBuffers.push(newData);
parentStderr?.write(newData);
});
// Listen for process exit.
process.addListener("close", (code) =>
reject(
new Error(
`The process ${this.codeqlPath} ${args.join(" ")} exited with code ${code}`,
),
),
);
// Write the command followed by a null terminator.
process.stdin.write(JSON.stringify(args), "utf8");
process.stdin.write(this.nullBuffer);
});
void debug("CLI command succeeded.");
const stdoutBuffer = Buffer.concat(stdoutBuffers);
return {
exitCode: 0,
stdout: stdoutBuffer.toString("utf8", 0, stdoutBuffer.length - 1),
stderr: Buffer.concat(stderrBuffers).toString("utf8"),
};
} catch (err) {
// Kill the process if it isn't already dead.
this.killProcessIfRunning();
if (stderrBuffers.length > 0) {
error(
`Failed to run ${args.join(" ")}:${EOL} ${Buffer.concat(stderrBuffers).toString("utf8")}`,
);
}
throw err;
} finally {
debug(Buffer.concat(stderrBuffers).toString("utf8"));
// Remove the listeners we set up.
process.stdout.removeAllListeners("data");
process.stderr.removeAllListeners("data");
process.removeAllListeners("close");
}
} finally {
this.commandInProcess = false;
// start running the next command immediately
this.runNext();
}
}
/**
* Run the next command in the queue
*/
private runNext(): void {
const callback = this.commandQueue.shift();
if (callback) {
callback();
}
}
private killProcessIfRunning(): void {
if (this.process) {
// Tell the Java CLI server process to shut down.
debug("Sending shutdown request");
try {
this.process.stdin.write(JSON.stringify(["shutdown"]), "utf8");
this.process.stdin.write(this.nullBuffer);
debug("Sent shutdown request");
} catch (e: unknown) {
// We are probably fine here, the process has already closed stdin.
debug(
`Shutdown request failed: process stdin may have already closed. The error was ${e}`,
);
debug("Stopping the process anyway.");
}
// Close the stdin and stdout streams.
// This is important on Windows where the child process may not die cleanly.
this.process.stdin.end();
this.process.kill();
this.process.stdout.destroy();
this.process.stderr.destroy();
this.process = undefined;
}
}
}