@@ -8,6 +8,7 @@ import { ContextURL, DisposableCollection, GitpodServer, RateLimiterError, Start
8
8
import { IDEOptions } from "@gitpod/gitpod-protocol/lib/ide-protocol" ;
9
9
import { ErrorCodes } from "@gitpod/gitpod-protocol/lib/messaging/error" ;
10
10
import EventEmitter from "events" ;
11
+ import * as queryString from "query-string" ;
11
12
import React , { Suspense , useEffect } from "react" ;
12
13
import { v4 } from 'uuid' ;
13
14
import Arrow from "../components/Arrow" ;
@@ -21,10 +22,54 @@ const sessionId = v4();
21
22
const WorkspaceLogs = React . lazy ( ( ) => import ( '../components/WorkspaceLogs' ) ) ;
22
23
23
24
export interface StartWorkspaceProps {
24
- workspaceId : string ;
25
+ workspaceId : string ,
26
+ runsInIFrame : boolean ,
27
+ /**
28
+ * This flag is used to break the autostart-cycle explained in https://github.com/gitpod-io/gitpod/issues/8043
29
+ */
30
+ dontAutostart : boolean ,
31
+ }
32
+
33
+ export function parseProps ( workspaceId : string , search ?: string ) : StartWorkspaceProps {
34
+ const params = parseParameters ( search ) ;
35
+ const runsInIFrame = window . top !== window . self ;
36
+ return {
37
+ workspaceId,
38
+ runsInIFrame : window . top !== window . self ,
39
+ // Either:
40
+ // - not_found: we were sent back from a workspace cluster/IDE URL where we expected a workspace to be running but it wasn't because either:
41
+ // - this is a (very) old tab and the workspace already timed out
42
+ // - due to a start error our workspace terminated very quickly between:
43
+ // a) us being redirected to that IDEUrl (based on the first ws-manager update) and
44
+ // b) our requests being validated by ws-proxy
45
+ // - runsInIFrame (IDE case):
46
+ // - we assume the workspace has already been started for us
47
+ // - we don't know it's instanceId
48
+ dontAutostart : params . notFound || runsInIFrame ,
49
+ }
50
+ }
51
+
52
+ function parseParameters ( search ?: string ) : { notFound ?: boolean } {
53
+ try {
54
+ if ( search === undefined ) {
55
+ return { } ;
56
+ }
57
+ const params = queryString . parse ( search , { parseBooleans : true } ) ;
58
+ const notFound = ! ! ( params && params [ "not_found" ] ) ;
59
+ return {
60
+ notFound,
61
+ } ;
62
+ } catch ( err ) {
63
+ console . error ( "/start: error parsing search params" , err ) ;
64
+ return { } ;
65
+ }
25
66
}
26
67
27
68
export interface StartWorkspaceState {
69
+ /**
70
+ * This is set to the istanceId we started (think we started on).
71
+ * We only receive updates for this particular instance, or none if not set.
72
+ */
28
73
startedInstanceId ?: string ;
29
74
workspaceInstance ?: WorkspaceInstance ;
30
75
workspace ?: Workspace ;
@@ -34,8 +79,8 @@ export interface StartWorkspaceState {
34
79
link : string
35
80
label : string
36
81
clientID ?: string
37
- }
38
- ideOptions ?: IDEOptions
82
+ } ;
83
+ ideOptions ?: IDEOptions ;
39
84
}
40
85
41
86
export default class StartWorkspace extends React . Component < StartWorkspaceProps , StartWorkspaceState > {
@@ -47,7 +92,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
47
92
48
93
private readonly toDispose = new DisposableCollection ( ) ;
49
94
componentWillMount ( ) {
50
- if ( this . runsInIFrame ( ) ) {
95
+ if ( this . props . runsInIFrame ) {
51
96
window . parent . postMessage ( { type : '$setSessionId' , sessionId } , '*' ) ;
52
97
const setStateEventListener = ( event : MessageEvent ) => {
53
98
if ( event . data . type === 'setState' && 'state' in event . data && typeof event . data [ 'state' ] === 'object' ) {
@@ -75,8 +120,15 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
75
120
this . setState ( { error } ) ;
76
121
}
77
122
123
+ if ( this . props . dontAutostart ) {
124
+ this . fetchWorkspaceInfo ( undefined ) ;
125
+ return ;
126
+ }
127
+
128
+ // dashboard case (no previous errors): start workspace as quickly as possible
78
129
this . startWorkspace ( ) ;
79
- getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) )
130
+ // query IDE options so we can show them if necessary once the workspace is running
131
+ getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) ) ;
80
132
}
81
133
82
134
componentWillUnmount ( ) {
@@ -130,14 +182,13 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
130
182
}
131
183
console . log ( "/start: started workspace instance: " + result . instanceID ) ;
132
184
// redirect to workspaceURL if we are not yet running in an iframe
133
- if ( ! this . runsInIFrame ( ) && result . workspaceURL ) {
185
+ if ( ! this . props . runsInIFrame && result . workspaceURL ) {
134
186
this . redirectTo ( result . workspaceURL ) ;
135
187
return ;
136
188
}
137
- this . setState ( { startedInstanceId : result . instanceID } ) ;
138
- // Explicitly query state to guarantee we get at least one update
189
+ // Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
139
190
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
140
- this . fetchWorkspaceInfo ( ) ;
191
+ this . fetchWorkspaceInfo ( result . instanceID ) ;
141
192
} catch ( error ) {
142
193
console . error ( error ) ;
143
194
if ( typeof error === 'string' ) {
@@ -180,15 +231,28 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
180
231
}
181
232
}
182
233
183
- async fetchWorkspaceInfo ( ) {
234
+ /**
235
+ * Fetches initial WorkspaceInfo from the server. If there is a WorkspaceInstance for workspaceId, we feed it
236
+ * into "onInstanceUpdate" and start accepting further updates.
237
+ *
238
+ * @param startedInstanceId The instanceId we want to listen on
239
+ */
240
+ async fetchWorkspaceInfo ( startedInstanceId : string | undefined ) {
241
+ // this ensures we're receiving updates for this instance
242
+ if ( startedInstanceId ) {
243
+ this . setState ( { startedInstanceId } ) ;
244
+ }
245
+
184
246
const { workspaceId } = this . props ;
185
247
try {
186
248
const info = await getGitpodService ( ) . server . getWorkspace ( workspaceId ) ;
187
249
if ( info . latestInstance ) {
188
- this . setState ( {
189
- workspace : info . workspace
190
- } ) ;
191
- this . onInstanceUpdate ( info . latestInstance ) ;
250
+ const instance = info . latestInstance ;
251
+ this . setState ( ( s ) => ( {
252
+ workspace : info . workspace ,
253
+ startedInstanceId : s . startedInstanceId || instance . id , // note: here's a potential mismatch between startedInstanceId and instance.id. TODO(gpl) How to handle this?
254
+ } ) ) ;
255
+ this . onInstanceUpdate ( instance ) ;
192
256
}
193
257
} catch ( error ) {
194
258
console . error ( error ) ;
@@ -197,20 +261,35 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
197
261
}
198
262
199
263
notifyDidOpenConnection ( ) {
200
- this . fetchWorkspaceInfo ( ) ;
264
+ this . fetchWorkspaceInfo ( undefined ) ;
201
265
}
202
266
203
267
async onInstanceUpdate ( workspaceInstance : WorkspaceInstance ) {
204
- const startedInstanceId = this . state ?. startedInstanceId ;
205
- if ( workspaceInstance . workspaceId !== this . props . workspaceId || startedInstanceId !== workspaceInstance . id ) {
268
+ if ( workspaceInstance . workspaceId !== this . props . workspaceId ) {
206
269
return ;
207
270
}
208
271
272
+ // Here we filter out updates to instances we haven't started to avoid issues with updates coming in out-of-order
273
+ // (e.g., multiple "stopped" events from the older instance, where we already started a fresh one after the first)
274
+ // Only exception is when we do the switch from the "old" to the "new" one.
275
+ const startedInstanceId = this . state ?. startedInstanceId ;
276
+ if ( startedInstanceId !== workspaceInstance . id ) {
277
+ // do we want to switch to "new" instance we just received an update for?
278
+ const switchToNewInstance = this . state . workspaceInstance ?. status . phase === "stopped" && workspaceInstance . status . phase !== "stopped" ;
279
+ if ( ! switchToNewInstance ) {
280
+ return ;
281
+ }
282
+ this . setState ( {
283
+ startedInstanceId : workspaceInstance . id ,
284
+ workspaceInstance,
285
+ } ) ;
286
+ }
287
+
209
288
await this . ensureWorkspaceAuth ( workspaceInstance . id ) ;
210
289
211
290
// Redirect to workspaceURL if we are not yet running in an iframe.
212
291
// It happens this late if we were waiting for a docker build.
213
- if ( ! this . runsInIFrame ( ) && workspaceInstance . ideUrl ) {
292
+ if ( ! this . props . runsInIFrame && workspaceInstance . ideUrl && ! this . props . dontAutostart ) {
214
293
this . redirectTo ( workspaceInstance . ideUrl ) ;
215
294
return ;
216
295
}
@@ -255,22 +334,18 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
255
334
}
256
335
257
336
redirectTo ( url : string ) {
258
- if ( this . runsInIFrame ( ) ) {
337
+ if ( this . props . runsInIFrame ) {
259
338
window . parent . postMessage ( { type : 'relocate' , url } , '*' ) ;
260
339
} else {
261
340
window . location . href = url ;
262
341
}
263
342
}
264
343
265
- runsInIFrame ( ) {
266
- return window . top !== window . self ;
267
- }
268
-
269
344
render ( ) {
270
345
const { error } = this . state ;
271
346
const isHeadless = this . state . workspace ?. type !== 'regular' ;
272
347
const isPrebuilt = WithPrebuild . is ( this . state . workspace ?. context ) ;
273
- let phase = StartPhase . Preparing ;
348
+ let phase : StartPhase | undefined = StartPhase . Preparing ;
274
349
let title = undefined ;
275
350
let statusMessage = ! ! error ? undefined : < p className = "text-base text-gray-400" > Preparing workspace …</ p > ;
276
351
const contextURL = ContextURL . getNormalizedURL ( this . state . workspace ) ?. toString ( ) ;
@@ -317,7 +392,29 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
317
392
}
318
393
if ( ! this . state . desktopIde ) {
319
394
phase = StartPhase . Running ;
320
- statusMessage = < p className = "text-base text-gray-400" > Opening Workspace …</ p > ;
395
+
396
+ if ( this . props . dontAutostart ) {
397
+ // hide the progress bar, as we're already running
398
+ phase = undefined ;
399
+ title = 'Running' ;
400
+
401
+ // in case we dontAutostart the IDE we have to provide controls to do so
402
+ statusMessage = < div >
403
+ < div className = "flex space-x-3 items-center text-left rounded-xl m-auto px-4 h-16 w-72 mt-4 mb-2 bg-gray-100 dark:bg-gray-800" >
404
+ < div className = "rounded-full w-3 h-3 text-sm bg-green-500" > </ div >
405
+ < div >
406
+ < p className = "text-gray-700 dark:text-gray-200 font-semibold w-56 truncate" > { this . state . workspaceInstance . workspaceId } </ p >
407
+ < a target = "_parent" href = { contextURL } > < p className = "w-56 truncate hover:text-blue-600 dark:hover:text-blue-400" > { contextURL } </ p > </ a >
408
+ </ div >
409
+ </ div >
410
+ < div className = "mt-10 justify-center flex space-x-2" >
411
+ < a target = "_parent" href = { gitpodHostUrl . asDashboard ( ) . toString ( ) } > < button className = "secondary" > Go to Dashboard</ button > </ a >
412
+ < a target = "_parent" href = { gitpodHostUrl . asStart ( this . props . workspaceId ) . toString ( ) /** move over 'start' here to fetch fresh credentials in case this is an older tab */ } > < button > Open Workspace</ button > </ a >
413
+ </ div >
414
+ </ div > ;
415
+ } else {
416
+ statusMessage = < p className = "text-base text-gray-400" > Opening Workspace …</ p > ;
417
+ }
321
418
} else {
322
419
phase = StartPhase . IdeReady ;
323
420
const openLink = this . state . desktopIde . link ;
0 commit comments