File tree Expand file tree Collapse file tree 17 files changed +204
-69
lines changed
control-plane/workspace-server Expand file tree Collapse file tree 17 files changed +204
-69
lines changed Original file line number Diff line number Diff line change @@ -5,6 +5,7 @@ import { Flag } from "../../flag/flag"
55import { Workspace } from "../../control-plane/workspace"
66import { Project } from "../../project/project"
77import { Installation } from "../../installation"
8+ import { Instance } from "../../project/instance"
89
910export const ServeCommand = cmd ( {
1011 command : "serve" ,
@@ -18,7 +19,13 @@ export const ServeCommand = cmd({
1819 const server = Server . listen ( opts )
1920 console . log ( `opencode server listening on http://${ server . hostname } :${ server . port } ` )
2021
21- await new Promise ( ( ) => { } )
22+ // Wait for termination signal instead of blocking forever
23+ await new Promise < void > ( ( resolve ) => {
24+ const shutdown = ( ) => resolve ( )
25+ process . on ( "SIGTERM" , shutdown )
26+ process . on ( "SIGINT" , shutdown )
27+ } )
28+ await Instance . disposeAll ( )
2229 await server . stop ( )
2330 } ,
2431} )
Original file line number Diff line number Diff line change @@ -7,10 +7,25 @@ export function WorkspaceServerRoutes() {
77 c . header ( "X-Accel-Buffering" , "no" )
88 c . header ( "X-Content-Type-Options" , "nosniff" )
99 return streamSSE ( c , async ( stream ) => {
10+ let done = false
11+ let resolveStream : ( ( ) => void ) | undefined
12+
13+ const cleanup = ( ) => {
14+ if ( done ) return
15+ done = true
16+ clearInterval ( heartbeat )
17+ GlobalBus . off ( "event" , handler )
18+ resolveStream ?.( )
19+ }
20+
1021 const send = async ( event : unknown ) => {
11- await stream . writeSSE ( {
12- data : JSON . stringify ( event ) ,
13- } )
22+ try {
23+ await stream . writeSSE ( {
24+ data : JSON . stringify ( event ) ,
25+ } )
26+ } catch {
27+ cleanup ( )
28+ }
1429 }
1530 const handler = async ( event : { directory ?: string ; payload : unknown } ) => {
1631 await send ( event . payload )
@@ -22,11 +37,8 @@ export function WorkspaceServerRoutes() {
2237 } , 10_000 )
2338
2439 await new Promise < void > ( ( resolve ) => {
25- stream . onAbort ( ( ) => {
26- clearInterval ( heartbeat )
27- GlobalBus . off ( "event" , handler )
28- resolve ( )
29- } )
40+ resolveStream = resolve
41+ stream . onAbort ( cleanup )
3042 } )
3143 } )
3244 } )
Original file line number Diff line number Diff line change @@ -101,9 +101,13 @@ export namespace Format {
101101 return result
102102 }
103103
104+ let unsubFormatted : ( ( ) => void ) | undefined
105+
104106 export function init ( ) {
105107 log . info ( "init" )
106- Bus . subscribe ( File . Event . Edited , async ( payload ) => {
108+ // Unsubscribe previous subscription to prevent stacking on re-init
109+ unsubFormatted ?.( )
110+ unsubFormatted = Bus . subscribe ( File . Event . Edited , async ( payload ) => {
107111 const file = payload . properties . file
108112 log . info ( "formatting" , { file } )
109113 const ext = path . extname ( file )
Original file line number Diff line number Diff line change @@ -208,6 +208,10 @@ try {
208208 }
209209 process . exitCode = 1
210210} finally {
211+ // Dispose all instances (LSP, MCP, PTY child processes) to prevent zombies.
212+ // Race with a 5-second timeout so we don't hang on unresponsive subprocesses.
213+ const { Instance } = await import ( "./project/instance" )
214+ await Promise . race ( [ Instance . disposeAll ( ) , new Promise ( ( r ) => setTimeout ( r , 5000 ) ) ] ) . catch ( ( ) => { } )
211215 // Some subprocesses don't react properly to SIGTERM and similar signals.
212216 // Most notably, some docker-container-based MCP servers don't handle such signals unless
213217 // run using `docker run --init`.
Original file line number Diff line number Diff line change @@ -156,6 +156,7 @@ export namespace LSPClient {
156156 } )
157157 }
158158
159+ const MAX_OPEN_FILES = 1000
159160 const files : {
160161 [ path : string ] : number
161162 } = { }
@@ -224,6 +225,12 @@ export namespace LSPClient {
224225 } ,
225226 } )
226227 files [ input . path ] = 0
228+ // Evict oldest file if we exceed the limit
229+ const keys = Object . keys ( files )
230+ if ( keys . length > MAX_OPEN_FILES ) {
231+ const oldest = keys [ 0 ]
232+ delete files [ oldest ]
233+ }
227234 return
228235 } ,
229236 } ,
@@ -263,6 +270,7 @@ export namespace LSPClient {
263270 l . info ( "shutting down" )
264271 diagnostics . clear ( )
265272 diagnosticOrder . length = 0
273+ for ( const key of Object . keys ( files ) ) delete files [ key ]
266274 connection . end ( )
267275 connection . dispose ( )
268276 input . server . process . kill ( )
Original file line number Diff line number Diff line change @@ -140,6 +140,9 @@ export namespace LSP {
140140 } ,
141141 async ( state ) => {
142142 await Promise . all ( state . clients . map ( ( client ) => client . shutdown ( ) ) )
143+ state . clients . length = 0
144+ state . broken . clear ( )
145+ state . spawning . clear ( )
143146 } ,
144147 )
145148
Original file line number Diff line number Diff line change @@ -414,7 +414,9 @@ export namespace MCP {
414414 duration : 8000 ,
415415 } ) . catch ( ( e ) => log . debug ( "failed to show toast" , { error : e } ) )
416416 } else {
417- // Store transport for later finishAuth call
417+ // Close any existing pending transport before storing the new one
418+ const existing = pendingOAuthTransports . get ( key )
419+ if ( existing ) existing . close ?.( ) . catch ( ( ) => { } )
418420 pendingOAuthTransports . set ( key , transport )
419421 status = { status : "needs_auth" as const }
420422 // Show toast for needs_auth
@@ -936,6 +938,8 @@ export namespace MCP {
936938 export async function removeAuth ( mcpName : string ) : Promise < void > {
937939 await McpAuth . remove ( mcpName )
938940 McpOAuthCallback . cancelPending ( mcpName )
941+ const transport = pendingOAuthTransports . get ( mcpName )
942+ if ( transport ) transport . close ?.( ) . catch ( ( ) => { } )
939943 pendingOAuthTransports . delete ( mcpName )
940944 await McpAuth . clearOAuthState ( mcpName )
941945 log . info ( "removed oauth credentials" , { mcpName } )
Original file line number Diff line number Diff line change @@ -279,6 +279,21 @@ export namespace PermissionNext {
279279 }
280280 }
281281
282+ export async function clearSession ( sessionID : string ) {
283+ const s = await state ( )
284+ for ( const [ id , pending ] of Object . entries ( s . pending ) ) {
285+ if ( pending . info . sessionID === sessionID ) {
286+ delete s . pending [ id ]
287+ Bus . publish ( Event . Replied , {
288+ sessionID : pending . info . sessionID ,
289+ requestID : pending . info . id ,
290+ reply : "reject" ,
291+ } )
292+ pending . reject ( new RejectedError ( ) )
293+ }
294+ }
295+ }
296+
282297 export async function list ( ) {
283298 const s = await state ( )
284299 return Object . values ( s . pending ) . map ( ( x ) => x . info )
Original file line number Diff line number Diff line change @@ -124,14 +124,18 @@ export namespace Plugin {
124124 return state ( ) . then ( ( x ) => x . hooks )
125125 }
126126
127+ let unsub : ( ( ) => void ) | undefined
128+
127129 export async function init ( ) {
128130 const hooks = await state ( ) . then ( ( x ) => x . hooks )
129131 const config = await Config . get ( )
130132 for ( const hook of hooks ) {
131133 // @ts -expect-error this is because we haven't moved plugin to sdk v2
132134 await hook . config ?.( config )
133135 }
134- Bus . subscribeAll ( async ( input ) => {
136+ // Unsubscribe previous wildcard subscriber to prevent stacking on re-init
137+ unsub ?.( )
138+ unsub = Bus . subscribeAll ( async ( input ) => {
135139 const hooks = await state ( ) . then ( ( x ) => x . hooks )
136140 for ( const hook of hooks ) {
137141 hook [ "event" ] ?.( {
Original file line number Diff line number Diff line change @@ -165,6 +165,20 @@ export namespace Question {
165165 }
166166 }
167167
168+ export async function clearSession ( sessionID : string ) {
169+ const s = await state ( )
170+ for ( const [ id , pending ] of Object . entries ( s . pending ) ) {
171+ if ( pending . info . sessionID === sessionID ) {
172+ delete s . pending [ id ]
173+ Bus . publish ( Event . Rejected , {
174+ sessionID : pending . info . sessionID ,
175+ requestID : pending . info . id ,
176+ } )
177+ pending . reject ( new RejectedError ( ) )
178+ }
179+ }
180+ }
181+
168182 export async function list ( ) {
169183 return state ( ) . then ( ( x ) => Object . values ( x . pending ) . map ( ( x ) => x . info ) )
170184 }
You can’t perform that action at this time.
0 commit comments