Skip to content

Commit 58f1c54

Browse files
committed
Fix issues surrounding initial web server load.
- Clean up watcher behaviors.
1 parent 211badf commit 58f1c54

File tree

5 files changed

+280
-131
lines changed

5 files changed

+280
-131
lines changed

ci/dev/watch.ts

+169-113
Original file line numberDiff line numberDiff line change
@@ -1,148 +1,204 @@
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+
}
1823

19-
private static log(message: string, skipNewline = false): void {
20-
process.stdout.write(message)
21-
if (!skipNewline) {
22-
process.stdout.write("\n")
24+
//#region Web Server
25+
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+
this.webServer = fork(path.join(this.rootPath, "out/node/entry.js"), process.argv.slice(2))
35+
const { pid } = this.webServer
36+
37+
this.webServer.on("exit", () => console.log("[Code Server]", `Web process ${pid} exited`))
38+
39+
console.log("\n[Code Server]", `Spawned web server process ${pid}`)
2440
}
2541

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-
}
42+
//#endregion
3743

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

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

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
53+
private vscodeCompileStatus = VSCodeCompileStatus.Loading
4654

47-
const cleanup = (code?: number | null): void => {
48-
Watcher.log("killing vs code watcher")
49-
vscode.removeAllListeners()
50-
vscode.kill()
55+
public async initialize(): Promise<void> {
56+
for (const event of ["SIGINT", "SIGTERM"]) {
57+
process.on(event, () => this.dispose(0))
58+
}
59+
60+
if (!this.hasVerboseLogging) {
61+
console.log("\n[Watcher]", "Compiler logs will be minimal. Pass --log to show all output.")
62+
}
5163

52-
Watcher.log("killing vs code web extension watcher")
53-
vscodeWebExtensions.removeAllListeners()
54-
vscodeWebExtensions.kill()
64+
this.cleanFiles()
5565

56-
Watcher.log("killing tsc")
57-
tsc.removeAllListeners()
58-
tsc.kill()
66+
for (const [processName, devProcess] of Object.entries(this.compilers)) {
67+
if (!devProcess) continue
5968

60-
if (plugin) {
61-
Watcher.log("killing plugin")
62-
plugin.removeAllListeners()
63-
plugin.kill()
64-
}
69+
devProcess.on("exit", (code) => {
70+
this.log(`[${processName}]`, "Terminated unexpectedly")
71+
this.dispose(code)
72+
})
6573

66-
if (server) {
67-
Watcher.log("killing server")
68-
server.removeAllListeners()
69-
server.kill()
74+
if (devProcess.stderr) {
75+
devProcess.stderr.on("data", (d: string | Uint8Array) => process.stderr.write(d))
7076
}
77+
}
7178

72-
Watcher.log("killing watch")
73-
process.exit(code || 0)
79+
onLine(this.compilers.vscode, this.parseVSCodeLine)
80+
onLine(this.compilers.codeServer, this.parseCodeServerLine)
81+
82+
if (this.compilers.plugins) {
83+
onLine(this.compilers.plugins, this.parsePluginLine)
7484
}
85+
}
7586

76-
process.on("SIGINT", () => cleanup())
77-
process.on("SIGTERM", () => cleanup())
87+
//#endregion
7888

79-
vscode.on("exit", (code) => {
80-
Watcher.log("vs code watcher terminated unexpectedly")
81-
cleanup(code)
82-
})
89+
//#region Line Parsers
8390

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

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

94-
if (plugin) {
95-
plugin.on("exit", (code) => {
96-
Watcher.log("plugin terminated unexpectedly")
97-
cleanup(code)
98-
})
125+
console.log("[Compiler][Code Server]", originalLine)
126+
127+
if (strippedLine.includes("Watching for file changes")) {
128+
console.log("[Compiler][Code Server]", "Finished compiling!", "(Refresh your web browser ♻️)")
129+
130+
this.reloadWebServer()
99131
}
132+
}
100133

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))
134+
private parsePluginLine: OnLineCallback = (strippedLine, originalLine) => {
135+
if (!strippedLine.length) return
104136

105-
if (plugin) {
106-
plugin.stderr.on("data", (d) => process.stderr.write(d))
137+
console.log("[Compiler][Plugin]", originalLine)
138+
139+
if (strippedLine.includes("Watching for file changes...")) {
140+
this.reloadWebServer()
107141
}
142+
}
108143

109-
let startingVscode = false
110-
let startedVscode = false
111-
onLine(vscode, (line, original) => {
112-
console.log("[vscode]", original)
113-
// Wait for watch-client since "Finished compilation" will appear multiple
114-
// times before the client starts building.
115-
if (!startingVscode && line.includes("Starting watch-client")) {
116-
startingVscode = true
117-
} else if (startingVscode && line.includes("Finished compilation")) {
118-
if (startedVscode) {
119-
restartServer()
120-
}
121-
startedVscode = true
122-
}
123-
})
144+
//#endregion
124145

125-
onLine(tsc, (line, original) => {
126-
// tsc outputs blank lines; skip them.
127-
if (line !== "") {
128-
console.log("[tsc]", original)
129-
}
130-
if (line.includes("Watching for file changes")) {
131-
restartServer()
132-
}
133-
})
146+
//#region Utilities
134147

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

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)