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