11import * as rpc from 'vscode-jsonrpc/node'
22import * as proc from 'node:child_process'
33import * as path from 'node:path'
4- import * as fs from 'node:fs/promises'
54import { type ScanOptions , type ScanResult } from './oxide'
65
6+ interface ServerHandle {
7+ helper : proc . ChildProcess
8+ connection : rpc . MessageConnection
9+ }
10+
11+ /**
12+ * The path to the Oxide helper process
13+ *
14+ * TODO:
15+ * - Can we find a way to not require a build first — i.e. point to
16+ * `oxide-helper.ts` and have things "hot reload" during tests?
17+ */
18+ const helperPath = process . env . TEST
19+ ? // This first path is relative to the source file so running tests in Vitest
20+ // result in the correct path — does still point to the built files.
21+ path . resolve ( path . dirname ( __filename ) , '../bin/oxide-helper.js' )
22+ : // The second path is relative to the built file. This is the same for the
23+ // language server *and* the extension since the file is named identically
24+ // in both builds.
25+ path . resolve ( path . dirname ( __filename ) , './oxide-helper.js' )
26+
727/**
828 * This helper starts a session in which we can use Oxide in *another process*
929 * to communicate content scanning results.
@@ -19,75 +39,64 @@ import { type ScanOptions, type ScanResult } from './oxide'
1939 * us sidestep the problem as the process will only be running as needed.
2040 */
2141export class OxideSession {
22- helper : proc . ChildProcess | null = null
23- connection : rpc . MessageConnection | null = null
42+ /**
43+ * An object that represents the connection to the server
44+ *
45+ * This ensures that either everything is initialized or nothing is
46+ */
47+ private server : Promise < ServerHandle > | null = null
2448
2549 public async scan ( options : ScanOptions ) : Promise < ScanResult > {
26- await this . startIfNeeded ( )
50+ let server = await this . startIfNeeded ( )
2751
28- return await this . connection . sendRequest ( 'scan' , options )
52+ return await server . connection . sendRequest ( 'scan' , options )
2953 }
3054
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- ]
55+ startIfNeeded ( ) : Promise < ServerHandle > {
56+ this . server ??= this . start ( )
4457
45- let module : string | null = null
46-
47- for ( let relativePath of modulePaths ) {
48- let filepath = path . resolve ( path . dirname ( __filename ) , relativePath )
49-
50- if (
51- await fs . access ( filepath ) . then (
52- ( ) => true ,
53- ( ) => false ,
54- )
55- ) {
56- module = filepath
57- break
58- }
59- }
58+ return this . server
59+ }
6060
61- if ( ! module ) throw new Error ( 'unable to load' )
61+ private async start ( ) : Promise < ServerHandle > {
62+ // 1. Start the new process
63+ let helper = proc . fork ( helperPath )
64+
65+ // 2. If the process fails to spawn we want to throw
66+ //
67+ // We do end up caching the failed promise but that should be
68+ // fine. It seems unlikely that, if this fails, trying again
69+ // would "fix" whatever problem there was and succeed.
70+ await new Promise ( ( resolve , reject ) => {
71+ helper . on ( 'spawn' , resolve )
72+ helper . on ( 'error' , reject )
73+ } )
6274
63- let helper = proc . fork ( module )
75+ // 3. Setup a channel to talk to the server
6476 let connection = rpc . createMessageConnection (
6577 new rpc . IPCMessageReader ( helper ) ,
6678 new rpc . IPCMessageWriter ( helper ) ,
6779 )
6880
69- helper . on ( 'disconnect' , ( ) => {
81+ // 4. If the process exits we can tear down everything
82+ helper . on ( 'close' , ( ) => {
7083 connection . dispose ( )
71- this . connection = null
72- this . helper = null
73- } )
74-
75- helper . on ( 'exit' , ( ) => {
76- connection . dispose ( )
77- this . connection = null
78- this . helper = null
84+ this . server = null
7985 } )
8086
87+ // 5. Start listening for messages
8188 connection . listen ( )
8289
83- this . helper = helper
84- this . connection = connection
90+ return { helper, connection }
8591 }
8692
8793 async stop ( ) {
88- if ( ! this . helper ) return
94+ if ( ! this . server ) return
95+
96+ let server = await this . server
8997
90- this . helper . disconnect ( )
91- this . helper . kill ( )
98+ // We terminate the server because, if for some reason it gets stuck,
99+ // we don't want it to stick around.
100+ server . helper . kill ( )
92101 }
93102}
0 commit comments