@@ -21,10 +21,54 @@ const sessionId = v4();
21
21
const WorkspaceLogs = React . lazy ( ( ) => import ( '../components/WorkspaceLogs' ) ) ;
22
22
23
23
export interface StartWorkspaceProps {
24
- workspaceId : string ;
24
+ workspaceId : string ,
25
+ parameters ?: StartWorkspaceParameters ,
26
+ }
27
+
28
+ export interface StartWorkspaceParameters {
29
+ trigger ?: StartWorkspaceTrigger ,
30
+ }
31
+
32
+ const StartWorkspaceTriggers = {
33
+ /**
34
+ * A workspace cluster's ws-proxy redirected the client to "/start/#<wsId>" because it could not find that workspace
35
+ */
36
+ "redirect_from_ws_cluster" : undefined ,
37
+
38
+ /**
39
+ * Supervisor redirect the client to "/start/#<wsId>" because it could
40
+ */
41
+ "redirect_from_supervisor" : undefined ,
42
+ } ;
43
+ export type StartWorkspaceTrigger = keyof ( typeof StartWorkspaceTriggers ) ;
44
+
45
+ export namespace StartWorkspaceParameters {
46
+ export function parse ( search : string ) : StartWorkspaceParameters | undefined {
47
+ try {
48
+ const result : StartWorkspaceParameters = { } ;
49
+ const params = new URLSearchParams ( search ) ;
50
+ for ( const [ k , v ] of params . entries ( ) ) {
51
+ switch ( k ) {
52
+ case "trigger" :
53
+ if ( v in StartWorkspaceTriggers ) {
54
+ result . trigger = v as StartWorkspaceTrigger ;
55
+ }
56
+ break ;
57
+ }
58
+ }
59
+ return result ;
60
+ } catch ( err ) {
61
+ console . error ( "/start: error parsing search params" , err ) ;
62
+ return undefined ;
63
+ }
64
+ }
25
65
}
26
66
27
67
export interface StartWorkspaceState {
68
+ /**
69
+ * This is set to the istanceId we started (think we started on).
70
+ * We only receive updates for this particular instance, or none if not set.
71
+ */
28
72
startedInstanceId ?: string ;
29
73
workspaceInstance ?: WorkspaceInstance ;
30
74
workspace ?: Workspace ;
@@ -34,8 +78,13 @@ export interface StartWorkspaceState {
34
78
link : string
35
79
label : string
36
80
clientID ?: string
37
- }
38
- ideOptions ?: IDEOptions
81
+ } ;
82
+ ideOptions ?: IDEOptions ;
83
+ showRestartModal ?: boolean ;
84
+ /**
85
+ * This flag is used to break the autostart-cycle explained in https://github.com/gitpod-io/gitpod/issues/8043
86
+ */
87
+ dontAutostart ?: boolean ;
39
88
}
40
89
41
90
export default class StartWorkspace extends React . Component < StartWorkspaceProps , StartWorkspaceState > {
@@ -75,8 +124,34 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
75
124
this . setState ( { error } ) ;
76
125
}
77
126
127
+ // we're coming back from a workspace cluster/IDE URL where we expected a workspace to be running but it wasn't because either:
128
+ // - this is a (very) old tab and the workspace already timed out
129
+ // - due to a start error our workspace terminated very quickly between:
130
+ // a) us being redirected to that IDEUrl (based on the first ws-manager update) and
131
+ // b) our requests being validated by ws-proxy
132
+ // we break this potentially DDOS cycle by showing a modal "Restart workspace?" instead of auto-restarting said workspace.
133
+ const params = this . props . parameters ;
134
+ if ( params ?. trigger === "redirect_from_ws_cluster"
135
+ || params ?. trigger === "redirect_from_supervisor" ) {
136
+ this . setState ( { dontAutostart : true } ) ;
137
+ this . fetchWorkspaceInfo ( undefined ) ;
138
+ return ;
139
+ }
140
+
141
+ // IDE case:
142
+ // - we assume the workspace has already been started for us
143
+ // - we don't know it's instanceId
144
+ // we could also rely on "startWorkspace" to return us the "running" instance, but that opens the door for
145
+ // self-DDOSing as we found in the past (cmp. https://github.com/gitpod-io/gitpod/issues/8043)
146
+ if ( this . runsInIFrame ( ) ) {
147
+ // fetch current state display (incl. potential errors)
148
+ this . setState ( { dontAutostart : true } ) ;
149
+ this . fetchWorkspaceInfo ( undefined ) ;
150
+ return ;
151
+ }
152
+
153
+ // dashboard case (no previous errors): start workspace as quickly as possible
78
154
this . startWorkspace ( ) ;
79
- getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) )
80
155
}
81
156
82
157
componentWillUnmount ( ) {
@@ -134,10 +209,12 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
134
209
this . redirectTo ( result . workspaceURL ) ;
135
210
return ;
136
211
}
137
- this . setState ( { startedInstanceId : result . instanceID } ) ;
138
- // Explicitly query state to guarantee we get at least one update
212
+ // Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
139
213
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
140
- this . fetchWorkspaceInfo ( ) ;
214
+ this . fetchWorkspaceInfo ( result . instanceID ) ;
215
+
216
+ // query IDE options so we can show them if necessary once the workspace is running
217
+ getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) ) ;
141
218
} catch ( error ) {
142
219
console . error ( error ) ;
143
220
if ( typeof error === 'string' ) {
@@ -180,15 +257,28 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
180
257
}
181
258
}
182
259
183
- async fetchWorkspaceInfo ( ) {
260
+ /**
261
+ * Fetches initial WorkspaceInfo from the server. If there is a WorkspaceInstance for workspaceId, we feed it
262
+ * into "onInstanceUpdate" and start accepting further updates.
263
+ *
264
+ * @param startedInstanceId The instanceId we want to listen on
265
+ */
266
+ async fetchWorkspaceInfo ( startedInstanceId : string | undefined ) {
267
+ // this ensures we're receiving updates for this instance
268
+ if ( startedInstanceId ) {
269
+ this . setState ( { startedInstanceId } ) ;
270
+ }
271
+
184
272
const { workspaceId } = this . props ;
185
273
try {
186
274
const info = await getGitpodService ( ) . server . getWorkspace ( workspaceId ) ;
187
275
if ( info . latestInstance ) {
188
- this . setState ( {
189
- workspace : info . workspace
190
- } ) ;
191
- this . onInstanceUpdate ( info . latestInstance ) ;
276
+ const instance = info . latestInstance ;
277
+ this . setState ( ( s ) => ( {
278
+ workspace : info . workspace ,
279
+ startedInstanceId : s . startedInstanceId || instance . id , // note: here's a potential mismatch between startedInstanceId and instance.id. TODO(gpl) How to handle this?
280
+ } ) ) ;
281
+ this . onInstanceUpdate ( instance ) ;
192
282
}
193
283
} catch ( error ) {
194
284
console . error ( error ) ;
@@ -197,7 +287,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
197
287
}
198
288
199
289
notifyDidOpenConnection ( ) {
200
- this . fetchWorkspaceInfo ( ) ;
290
+ this . fetchWorkspaceInfo ( undefined ) ;
201
291
}
202
292
203
293
async onInstanceUpdate ( workspaceInstance : WorkspaceInstance ) {
@@ -210,7 +300,8 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
210
300
211
301
// Redirect to workspaceURL if we are not yet running in an iframe.
212
302
// It happens this late if we were waiting for a docker build.
213
- if ( ! this . runsInIFrame ( ) && workspaceInstance . ideUrl ) {
303
+ const dontRedirect = this . state ?. dontAutostart ;
304
+ if ( ! this . runsInIFrame ( ) && workspaceInstance . ideUrl && ! dontRedirect ) {
214
305
this . redirectTo ( workspaceInstance . ideUrl ) ;
215
306
return ;
216
307
}
0 commit comments