Skip to content

Commit 3157a40

Browse files
author
Teffen
authored
Fix issues surrounding initial web server load. (#4509)
- Clean up watcher behaviors.
1 parent 5fe16be commit 3157a40

File tree

5 files changed

+282
-122
lines changed

5 files changed

+282
-122
lines changed

ci/dev/watch.ts

+171-104
Original file line numberDiff line numberDiff line change
@@ -1,139 +1,206 @@
1-
import * as cp from "child_process"
1+
import { spawn, fork, ChildProcess } from "child_process"
2+
import del from "del"
3+
import { promises as fs } from "fs"
24
import * as path from "path"
3-
import { onLine } from "../../src/node/util"
4-
5-
async function main(): Promise<void> {
6-
try {
7-
const watcher = new Watcher()
8-
await watcher.watch()
9-
} catch (error: any) {
10-
console.error(error.message)
11-
process.exit(1)
12-
}
5+
import { CompilationStats, onLine, OnLineCallback, VSCodeCompileStatus } from "../../src/node/util"
6+
7+
interface DevelopmentCompilers {
8+
[key: string]: ChildProcess | undefined
9+
vscode: ChildProcess
10+
vscodeWebExtensions: ChildProcess
11+
codeServer: ChildProcess
12+
plugins: ChildProcess | undefined
1313
}
1414

1515
class Watcher {
16-
private readonly rootPath = path.resolve(__dirname, "../..")
17-
private readonly vscodeSourcePath = path.join(this.rootPath, "vendor/modules/code-oss-dev")
16+
private rootPath = path.resolve(process.cwd())
17+
private readonly paths = {
18+
/** Path to uncompiled VS Code source. */
19+
vscodeDir: path.join(this.rootPath, "vendor", "modules", "code-oss-dev"),
20+
compilationStatsFile: path.join(this.rootPath, "out", "watcher.json"),
21+
pluginDir: process.env.PLUGIN_DIR,
22+
}
23+
24+
//#region Web Server
1825

19-
private static log(message: string, skipNewline = false): void {
20-
process.stdout.write(message)
21-
if (!skipNewline) {
22-
process.stdout.write("\n")
26+
/** Development web server. */
27+
private webServer: ChildProcess | undefined
28+
29+
private reloadWebServer = (): void => {
30+
if (this.webServer) {
31+
this.webServer.kill()
2332
}
33+
34+
// Pass CLI args, save for `node` and the initial script name.
35+
const args = process.argv.slice(2)
36+
this.webServer = fork(path.join(this.rootPath, "out/node/entry.js"), args)
37+
const { pid } = this.webServer
38+
39+
this.webServer.on("exit", () => console.log("[Code Server]", `Web process ${pid} exited`))
40+
41+
console.log("\n[Code Server]", `Spawned web server process ${pid}`)
2442
}
2543

26-
public async watch(): Promise<void> {
27-
let server: cp.ChildProcess | undefined
28-
const restartServer = (): void => {
29-
if (server) {
30-
server.kill()
31-
}
32-
const s = cp.fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2))
33-
console.log(`[server] spawned process ${s.pid}`)
34-
s.on("exit", () => console.log(`[server] process ${s.pid} exited`))
35-
server = s
36-
}
44+
//#endregion
3745

38-
const vscode = cp.spawn("yarn", ["watch"], { cwd: this.vscodeSourcePath })
46+
//#region Compilers
3947

40-
const vscodeWebExtensions = cp.spawn("yarn", ["watch-web"], { cwd: this.vscodeSourcePath })
48+
private readonly compilers: DevelopmentCompilers = {
49+
codeServer: spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath }),
50+
vscode: spawn("yarn", ["watch"], { cwd: this.paths.vscodeDir }),
51+
vscodeWebExtensions: spawn("yarn", ["watch-web"], { cwd: this.paths.vscodeDir }),
52+
plugins: this.paths.pluginDir ? spawn("yarn", ["build", "--watch"], { cwd: this.paths.pluginDir }) : undefined,
53+
}
4154

42-
const tsc = cp.spawn("tsc", ["--watch", "--pretty", "--preserveWatchOutput"], { cwd: this.rootPath })
43-
const plugin = process.env.PLUGIN_DIR
44-
? cp.spawn("yarn", ["build", "--watch"], { cwd: process.env.PLUGIN_DIR })
45-
: undefined
55+
private vscodeCompileStatus = VSCodeCompileStatus.Loading
4656

47-
const cleanup = (code?: number | null): void => {
48-
Watcher.log("killing vs code watcher")
49-
vscode.removeAllListeners()
50-
vscode.kill()
57+
public async initialize(): Promise<void> {
58+
for (const event of ["SIGINT", "SIGTERM"]) {
59+
process.on(event, () => this.dispose(0))
60+
}
5161

52-
Watcher.log("killing vs code web extension watcher")
53-
vscodeWebExtensions.removeAllListeners()
54-
vscodeWebExtensions.kill()
62+
if (!this.hasVerboseLogging) {
63+
console.log("\n[Watcher]", "Compiler logs will be minimal. Pass --log to show all output.")
64+
}
5565

56-
Watcher.log("killing tsc")
57-
tsc.removeAllListeners()
58-
tsc.kill()
66+
this.cleanFiles()
5967

60-
if (plugin) {
61-
Watcher.log("killing plugin")
62-
plugin.removeAllListeners()
63-
plugin.kill()
64-
}
68+
for (const [processName, devProcess] of Object.entries(this.compilers)) {
69+
if (!devProcess) continue
6570

66-
if (server) {
67-
Watcher.log("killing server")
68-
server.removeAllListeners()
69-
server.kill()
71+
devProcess.on("exit", (code) => {
72+
this.log(`[${processName}]`, "Terminated unexpectedly")
73+
this.dispose(code)
74+
})
75+
76+
if (devProcess.stderr) {
77+
devProcess.stderr.on("data", (d: string | Uint8Array) => process.stderr.write(d))
7078
}
79+
}
80+
81+
onLine(this.compilers.vscode, this.parseVSCodeLine)
82+
onLine(this.compilers.codeServer, this.parseCodeServerLine)
7183

72-
Watcher.log("killing watch")
73-
process.exit(code || 0)
84+
if (this.compilers.plugins) {
85+
onLine(this.compilers.plugins, this.parsePluginLine)
7486
}
87+
}
88+
89+
//#endregion
7590

76-
process.on("SIGINT", () => cleanup())
77-
process.on("SIGTERM", () => cleanup())
91+
//#region Line Parsers
7892

79-
vscode.on("exit", (code) => {
80-
Watcher.log("vs code watcher terminated unexpectedly")
81-
cleanup(code)
82-
})
93+
private parseVSCodeLine: OnLineCallback = (strippedLine, originalLine) => {
94+
if (!strippedLine.includes("watch-extensions") || this.hasVerboseLogging) {
95+
console.log("[VS Code]", originalLine)
96+
}
8397

84-
vscodeWebExtensions.on("exit", (code) => {
85-
Watcher.log("vs code extension watcher terminated unexpectedly")
86-
cleanup(code)
87-
})
98+
switch (this.vscodeCompileStatus) {
99+
case VSCodeCompileStatus.Loading:
100+
// Wait for watch-client since "Finished compilation" will appear multiple
101+
// times before the client starts building.
102+
if (strippedLine.includes("Starting 'watch-client'")) {
103+
console.log("[VS Code] 🚧 Compiling 🚧", "(This may take a moment!)")
104+
this.vscodeCompileStatus = VSCodeCompileStatus.Compiling
105+
}
106+
break
107+
case VSCodeCompileStatus.Compiling:
108+
if (strippedLine.includes("Finished compilation")) {
109+
console.log("[VS Code] ✨ Finished compiling! ✨", "(Refresh your web browser ♻️)")
110+
this.vscodeCompileStatus = VSCodeCompileStatus.Compiled
111+
112+
this.emitCompilationStats()
113+
this.reloadWebServer()
114+
}
115+
break
116+
case VSCodeCompileStatus.Compiled:
117+
console.log("[VS Code] 🔔 Finished recompiling! 🔔", "(Refresh your web browser ♻️)")
118+
this.emitCompilationStats()
119+
this.reloadWebServer()
120+
break
121+
}
122+
}
88123

89-
tsc.on("exit", (code) => {
90-
Watcher.log("tsc terminated unexpectedly")
91-
cleanup(code)
92-
})
124+
private parseCodeServerLine: OnLineCallback = (strippedLine, originalLine) => {
125+
if (!strippedLine.length) return
93126

94-
if (plugin) {
95-
plugin.on("exit", (code) => {
96-
Watcher.log("plugin terminated unexpectedly")
97-
cleanup(code)
98-
})
127+
console.log("[Compiler][Code Server]", originalLine)
128+
129+
if (strippedLine.includes("Watching for file changes")) {
130+
console.log("[Compiler][Code Server]", "Finished compiling!", "(Refresh your web browser ♻️)")
131+
132+
this.reloadWebServer()
99133
}
134+
}
135+
136+
private parsePluginLine: OnLineCallback = (strippedLine, originalLine) => {
137+
if (!strippedLine.length) return
100138

101-
vscodeWebExtensions.stderr.on("data", (d) => process.stderr.write(d))
102-
vscode.stderr.on("data", (d) => process.stderr.write(d))
103-
tsc.stderr.on("data", (d) => process.stderr.write(d))
139+
console.log("[Compiler][Plugin]", originalLine)
104140

105-
if (plugin) {
106-
plugin.stderr.on("data", (d) => process.stderr.write(d))
141+
if (strippedLine.includes("Watching for file changes...")) {
142+
this.reloadWebServer()
107143
}
144+
}
108145

109-
onLine(vscode, (line, original) => {
110-
console.log("[vscode]", original)
111-
if (line.includes("Finished compilation")) {
112-
restartServer()
113-
}
114-
})
146+
//#endregion
115147

116-
onLine(tsc, (line, original) => {
117-
// tsc outputs blank lines; skip them.
118-
if (line !== "") {
119-
console.log("[tsc]", original)
120-
}
121-
if (line.includes("Watching for file changes")) {
122-
restartServer()
123-
}
124-
})
148+
//#region Utilities
125149

126-
if (plugin) {
127-
onLine(plugin, (line, original) => {
128-
// tsc outputs blank lines; skip them.
129-
if (line !== "") {
130-
console.log("[plugin]", original)
131-
}
132-
if (line.includes("Watching for file changes")) {
133-
restartServer()
134-
}
135-
})
150+
/**
151+
* Cleans files from previous builds.
152+
*/
153+
private cleanFiles(): Promise<string[]> {
154+
console.log("[Watcher]", "Cleaning files from previous builds...")
155+
156+
return del([
157+
"out/**/*",
158+
// Included because the cache can sometimes enter bad state when debugging compiled files.
159+
".cache/**/*",
160+
])
161+
}
162+
163+
/**
164+
* Emits a file containing compilation data.
165+
* This is especially useful when Express needs to determine if VS Code is still compiling.
166+
*/
167+
private emitCompilationStats(): Promise<void> {
168+
const stats: CompilationStats = {
169+
status: this.vscodeCompileStatus,
170+
lastCompiledAt: new Date(),
171+
}
172+
173+
this.log("Writing watcher stats...")
174+
return fs.writeFile(this.paths.compilationStatsFile, JSON.stringify(stats, null, 2))
175+
}
176+
177+
private log(...entries: string[]) {
178+
process.stdout.write(entries.join(" "))
179+
}
180+
181+
private dispose(code: number | null): void {
182+
for (const [processName, devProcess] of Object.entries(this.compilers)) {
183+
this.log(`[${processName}]`, "Killing...\n")
184+
devProcess?.removeAllListeners()
185+
devProcess?.kill()
136186
}
187+
process.exit(typeof code === "number" ? code : 0)
188+
}
189+
190+
private get hasVerboseLogging() {
191+
return process.argv.includes("--log")
192+
}
193+
194+
//#endregion
195+
}
196+
197+
async function main(): Promise<void> {
198+
try {
199+
const watcher = new Watcher()
200+
await watcher.initialize()
201+
} catch (error: any) {
202+
console.error(error.message)
203+
process.exit(1)
137204
}
138205
}
139206

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,7 @@
5252
"@typescript-eslint/parser": "^5.0.0",
5353
"audit-ci": "^5.0.0",
5454
"codecov": "^3.8.3",
55+
"del": "^6.0.0",
5556
"doctoc": "^2.0.0",
5657
"eslint": "^7.7.0",
5758
"eslint-config-prettier": "^8.1.0",

src/node/routes/vscode.ts

+14-6
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import { WebsocketRequest } from "../../../typings/pluginapi"
44
import { logError } from "../../common/util"
55
import { isDevMode } from "../constants"
66
import { ensureAuthenticated, authenticated, redirect } from "../http"
7-
import { loadAMDModule } from "../util"
7+
import { loadAMDModule, readCompilationStats } from "../util"
88
import { Router as WsRouter } from "../wsRouter"
99
import { errorHandler } from "./errors"
1010

@@ -40,7 +40,6 @@ export class CodeServerRouteWrapper {
4040
if (error instanceof Error && ["EntryNotFound", "FileNotFound", "HttpError"].includes(error.message)) {
4141
next()
4242
}
43-
4443
errorHandler(error, req, res, next)
4544
}
4645

@@ -62,9 +61,21 @@ export class CodeServerRouteWrapper {
6261
*/
6362
private ensureCodeServerLoaded: express.Handler = async (req, _res, next) => {
6463
if (this._codeServerMain) {
64+
// Already loaded...
6565
return next()
6666
}
6767

68+
if (isDevMode) {
69+
// Is the development mode file watcher still busy?
70+
const compileStats = await readCompilationStats()
71+
72+
if (!compileStats || !compileStats.lastCompiledAt) {
73+
return next(new Error("VS Code may still be compiling..."))
74+
}
75+
}
76+
77+
// Create the server...
78+
6879
const { args } = req
6980

7081
/**
@@ -84,10 +95,7 @@ export class CodeServerRouteWrapper {
8495
})
8596
} catch (createServerError) {
8697
logError(logger, "CodeServerRouteWrapper", createServerError)
87-
88-
const loggedError = isDevMode ? new Error("VS Code may still be compiling...") : createServerError
89-
90-
return next(loggedError)
98+
return next(createServerError)
9199
}
92100

93101
return next()

0 commit comments

Comments
 (0)