Skip to content

Commit dc8e4d1

Browse files
committed
Fix race condition when launching Oxide helper process
1 parent 5c79a60 commit dc8e4d1

File tree

1 file changed

+69
-41
lines changed

1 file changed

+69
-41
lines changed

packages/tailwindcss-language-server/src/oxide-session.ts

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ import * as path from 'node:path'
44
import * as fs from 'node:fs/promises'
55
import { type ScanOptions, type ScanResult } from './oxide'
66

7+
interface ServerHandle {
8+
helper: proc.ChildProcess
9+
connection: rpc.MessageConnection
10+
}
11+
712
/**
813
* This helper starts a session in which we can use Oxide in *another process*
914
* to communicate content scanning results.
@@ -19,75 +24,98 @@ import { type ScanOptions, type ScanResult } from './oxide'
1924
* us sidestep the problem as the process will only be running as needed.
2025
*/
2126
export class OxideSession {
22-
helper: proc.ChildProcess | null = null
23-
connection: rpc.MessageConnection | null = null
27+
/**
28+
* A cached helper path
29+
*
30+
* We store this so the lookup happens at most once
31+
*/
32+
private static helperPath: Promise<string> | null = null
33+
34+
/**
35+
* An object that represents the connection to the server
36+
*
37+
* This ensures that either everything is initialized or nothing is
38+
*/
39+
private server: Promise<ServerHandle> | null = null
2440

2541
public async scan(options: ScanOptions): Promise<ScanResult> {
26-
await this.startIfNeeded()
42+
let server = await this.startIfNeeded()
2743

28-
return await this.connection.sendRequest('scan', options)
44+
return await server.connection.sendRequest('scan', options)
2945
}
3046

31-
async startIfNeeded(): Promise<void> {
32-
if (this.connection) return
33-
34-
// TODO: Can we find a way to not require a build first?
35-
// let module = path.resolve(path.dirname(__filename), './oxide-helper.ts')
36-
37-
let modulePaths = [
38-
// Separate Language Server package
39-
'../bin/oxide-helper.js',
40-
41-
// Bundled with the VSCode extension
42-
'../dist/oxide-helper.js',
43-
]
47+
async startIfNeeded(): Promise<ServerHandle> {
48+
console.log('startIfNeeded?')
4449

45-
let module: string | null = null
50+
this.server ??= this.start()
4651

47-
for (let relativePath of modulePaths) {
48-
let filepath = path.resolve(path.dirname(__filename), relativePath)
52+
return this.server
53+
}
4954

50-
if (
51-
await fs.access(filepath).then(
52-
() => true,
53-
() => false,
54-
)
55-
) {
56-
module = filepath
57-
break
58-
}
59-
}
55+
private async start(): Promise<ServerHandle> {
56+
console.log('start server?')
6057

61-
if (!module) throw new Error('unable to load')
58+
let helperPath = await OxideSession.locateHelper()
6259

63-
let helper = proc.fork(module)
60+
let helper = proc.fork(helperPath)
6461
let connection = rpc.createMessageConnection(
6562
new rpc.IPCMessageReader(helper),
6663
new rpc.IPCMessageWriter(helper),
6764
)
6865

6966
helper.on('disconnect', () => {
67+
console.log('Helper disconnected')
7068
connection.dispose()
71-
this.connection = null
72-
this.helper = null
69+
this.server = null
7370
})
7471

7572
helper.on('exit', () => {
73+
console.log('Helper exited')
7674
connection.dispose()
77-
this.connection = null
78-
this.helper = null
75+
this.server = null
7976
})
8077

8178
connection.listen()
8279

83-
this.helper = helper
84-
this.connection = connection
80+
return { helper, connection }
8581
}
8682

8783
async stop() {
88-
if (!this.helper) return
84+
if (!this.server) return
85+
86+
let handle = await this.server
87+
handle.helper.disconnect()
88+
handle.helper.kill()
89+
}
90+
91+
private static async locateHelper(): Promise<string> {
92+
console.log('locate helper?')
93+
94+
// TODO: Can we find a way to not require a build first?
95+
// let module = path.resolve(path.dirname(__filename), './oxide-helper.ts')
96+
97+
let modulePaths = [
98+
// Separate Language Server package
99+
'../bin/oxide-helper.js',
100+
101+
// Bundled with the VSCode extension
102+
'../dist/oxide-helper.js',
103+
]
104+
105+
OxideSession.helperPath ??= (async () => {
106+
for (let relativePath of modulePaths) {
107+
let filepath = path.resolve(path.dirname(__filename), relativePath)
108+
let exists = await fs.access(filepath).then(
109+
() => true,
110+
() => false,
111+
)
112+
113+
if (exists) return filepath
114+
}
115+
116+
throw new Error('unable to load')
117+
})()
89118

90-
this.helper.disconnect()
91-
this.helper.kill()
119+
return OxideSession.helperPath
92120
}
93121
}

0 commit comments

Comments
 (0)