@@ -15,6 +15,7 @@ import * as Effect from "effect/Effect";
1515import * as HashSet from "effect/HashSet" ;
1616import * as Option from "effect/Option" ;
1717import * as Ref from "effect/Ref" ;
18+ import * as Semaphore from "effect/Semaphore" ;
1819import * as FileSystem from "effect/FileSystem" ;
1920import * as Layer from "effect/Layer" ;
2021import * as Path from "effect/Path" ;
@@ -59,6 +60,15 @@ export class ServerSelfUpdate extends Context.Service<
5960 requestId : string ,
6061 onHandoffAccepted ?: ( ) => Effect . Effect < void > ,
6162 ) => Effect . Effect < never , ServerSelfUpdateError > ;
63+ /** How this server can update itself, or null when it cannot. */
64+ readonly capability : ServerSelfUpdateCapability | null ;
65+ /** Downloads and preflights a boot-service runtime without restarting. */
66+ readonly stage : (
67+ targetVersion : string ,
68+ ) => Effect . Effect <
69+ { readonly orchestrationProtocol : number | undefined } ,
70+ ServerSelfUpdateError
71+ > ;
6272 }
6373> ( ) ( "t3/cloud/selfUpdate/ServerSelfUpdate" ) { }
6474
@@ -137,6 +147,7 @@ export const withRunningThreadContinuation = Effect.fn(
137147 } ;
138148
139149 return ServerSelfUpdate . of ( {
150+ ...input . selfUpdate ,
140151 update,
141152 commitDesktopUpdate : ( requestId ) =>
142153 Effect . gen ( function * ( ) {
@@ -185,6 +196,8 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
185196 yield * Config . String ( CLI_RELEASE_BASE_URL_ENV ) . pipe ( Config . option ) ,
186197 ) ;
187198 const inFlight = yield * Ref . make ( false ) ;
199+ // A background stage and a manual update may target the same version.
200+ const stageLock = yield * Semaphore . make ( 1 ) ;
188201
189202 const capability : ServerSelfUpdateCapability | null =
190203 serverConfig . mode === "desktop" ? "desktop-managed" : launcher . managed ? "boot-service" : null ;
@@ -193,6 +206,109 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
193206 ? new ServerSelfUpdateError ( { reason } )
194207 : new ServerSelfUpdateError ( { reason, cause } ) ;
195208
209+ /** Downloads and preflights a runtime without handing off, so a background
210+ update can stage first and restart later. */
211+ const stageRuntime = Effect . fn ( "cloud.server_self_update.stage" ) ( function * (
212+ targetVersion : string ,
213+ ) {
214+ return yield * stageLock . withPermits ( 1 ) ( stageRuntimeUnlocked ( targetVersion ) ) ;
215+ } ) ;
216+ const stageRuntimeUnlocked = Effect . fn ( "cloud.server_self_update.stageUnlocked" ) ( function * (
217+ targetVersion : string ,
218+ ) {
219+ let orchestrationProtocol : number | undefined ;
220+ const paths = yield * ensurePinnedRuntimeInstalled ( {
221+ baseDir : serverConfig . baseDir ,
222+ version : targetVersion ,
223+ fs,
224+ path,
225+ runner,
226+ httpClient,
227+ platform,
228+ arch,
229+ releaseBaseUrl,
230+ validate : ( runtime ) =>
231+ runner
232+ . run ( {
233+ command : pinnedRuntimeCommand ( runtime ) . command ,
234+ args : [
235+ ...pinnedRuntimeCommand ( runtime ) . args ,
236+ "__service-preflight" ,
237+ "--database-path" ,
238+ serverConfig . dbPath ,
239+ "--launcher-protocol" ,
240+ String ( SERVICE_LAUNCHER_PROTOCOL ) ,
241+ ] ,
242+ timeout : PREFLIGHT_TIMEOUT ,
243+ } )
244+ . pipe (
245+ Effect . mapError (
246+ ( cause ) =>
247+ new PinnedRuntimeInstallError ( {
248+ step : "running the staged service preflight" ,
249+ cause,
250+ } ) ,
251+ ) ,
252+ Effect . flatMap (
253+ (
254+ result ,
255+ ) : Effect . Effect <
256+ void ,
257+ PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError
258+ > => {
259+ if ( result . code !== 0 ) {
260+ return Effect . fail (
261+ new PinnedRuntimeInstallError ( {
262+ step : "running the staged service preflight" ,
263+ exitCode : Number ( result . code ) ,
264+ stdoutLength : result . stdout . length ,
265+ stderrLength : result . stderr . length ,
266+ } ) ,
267+ ) ;
268+ }
269+ let parsed : unknown ;
270+ try {
271+ parsed = JSON . parse ( result . stdout . trim ( ) ) ;
272+ } catch ( cause ) {
273+ return Effect . fail (
274+ new PinnedRuntimeInstallError ( {
275+ step : "decoding the staged service preflight" ,
276+ cause,
277+ } ) ,
278+ ) ;
279+ }
280+ const preflight = decodeServicePreflightResult ( parsed ) ;
281+ if ( preflight === undefined || preflight . version !== targetVersion ) {
282+ return Effect . fail (
283+ new PinnedRuntimeInstallError ( {
284+ step : "verifying the staged service preflight" ,
285+ } ) ,
286+ ) ;
287+ }
288+ if ( preflight . status === "ready" ) {
289+ orchestrationProtocol = preflight . orchestrationProtocol ;
290+ }
291+ return preflight . status === "ready"
292+ ? Effect . void
293+ : Effect . fail (
294+ new PinnedRuntimePreflightBlockedError ( {
295+ version : targetVersion ,
296+ reason : preflight . reason ,
297+ } ) ,
298+ ) ;
299+ } ,
300+ ) ,
301+ ) ,
302+ } ) . pipe (
303+ Effect . mapError ( ( error ) =>
304+ error . _tag === "PinnedRuntimePreflightBlockedError"
305+ ? failWith ( error . reason , error )
306+ : failWith ( `Could not prepare t3@${ targetVersion } .` , error ) ,
307+ ) ,
308+ ) ;
309+ return { paths, orchestrationProtocol } ;
310+ } ) ;
311+
196312 const update : ServerSelfUpdate [ "Service" ] [ "update" ] = Effect . fn (
197313 "cloud.server_self_update.update" ,
198314 ) ( function * ( input , reportProgress = ( ) => Effect . void , onHandoffAccepted = ( ) => Effect . void ) {
@@ -223,92 +339,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
223339
224340 return yield * Effect . gen ( function * ( ) {
225341 yield * reportProgress ( "downloading" ) ;
226- const paths = yield * ensurePinnedRuntimeInstalled ( {
227- baseDir : serverConfig . baseDir ,
228- version : targetVersion ,
229- fs,
230- path,
231- runner,
232- httpClient,
233- platform,
234- arch,
235- releaseBaseUrl,
236- validate : ( runtime ) =>
237- runner
238- . run ( {
239- command : pinnedRuntimeCommand ( runtime ) . command ,
240- args : [
241- ...pinnedRuntimeCommand ( runtime ) . args ,
242- "__service-preflight" ,
243- "--database-path" ,
244- serverConfig . dbPath ,
245- "--launcher-protocol" ,
246- String ( SERVICE_LAUNCHER_PROTOCOL ) ,
247- ] ,
248- timeout : PREFLIGHT_TIMEOUT ,
249- } )
250- . pipe (
251- Effect . mapError (
252- ( cause ) =>
253- new PinnedRuntimeInstallError ( {
254- step : "running the staged service preflight" ,
255- cause,
256- } ) ,
257- ) ,
258- Effect . flatMap (
259- (
260- result ,
261- ) : Effect . Effect <
262- void ,
263- PinnedRuntimeInstallError | PinnedRuntimePreflightBlockedError
264- > => {
265- if ( result . code !== 0 ) {
266- return Effect . fail (
267- new PinnedRuntimeInstallError ( {
268- step : "running the staged service preflight" ,
269- exitCode : Number ( result . code ) ,
270- stdoutLength : result . stdout . length ,
271- stderrLength : result . stderr . length ,
272- } ) ,
273- ) ;
274- }
275- let parsed : unknown ;
276- try {
277- parsed = JSON . parse ( result . stdout . trim ( ) ) ;
278- } catch ( cause ) {
279- return Effect . fail (
280- new PinnedRuntimeInstallError ( {
281- step : "decoding the staged service preflight" ,
282- cause,
283- } ) ,
284- ) ;
285- }
286- const preflight = decodeServicePreflightResult ( parsed ) ;
287- if ( preflight === undefined || preflight . version !== targetVersion ) {
288- return Effect . fail (
289- new PinnedRuntimeInstallError ( {
290- step : "verifying the staged service preflight" ,
291- } ) ,
292- ) ;
293- }
294- return preflight . status === "ready"
295- ? Effect . void
296- : Effect . fail (
297- new PinnedRuntimePreflightBlockedError ( {
298- version : targetVersion ,
299- reason : preflight . reason ,
300- } ) ,
301- ) ;
302- } ,
303- ) ,
304- ) ,
305- } ) . pipe (
306- Effect . mapError ( ( error ) =>
307- error . _tag === "PinnedRuntimePreflightBlockedError"
308- ? failWith ( error . reason , error )
309- : failWith ( `Could not prepare t3@${ targetVersion } .` , error ) ,
310- ) ,
311- ) ;
342+ const { paths } = yield * stageRuntime ( targetVersion ) ;
312343
313344 yield * reportProgress ( "installing" ) ;
314345 const updateId = yield * Effect . uninterruptible (
@@ -338,6 +369,13 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
338369 update,
339370 commitDesktopUpdate : ( requestId , onHandoffAccepted ) =>
340371 desktopAppUpdate . commit ( requestId , onHandoffAccepted ) ,
372+ capability,
373+ stage : ( targetVersion ) =>
374+ capability === "boot-service"
375+ ? stageRuntime ( targetVersion ) . pipe (
376+ Effect . map ( ( { orchestrationProtocol } ) => ( { orchestrationProtocol } ) ) ,
377+ )
378+ : Effect . fail ( failWith ( "Only background service installs can stage an update." ) ) ,
341379 } ) ;
342380} ) ;
343381
0 commit comments