@@ -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,33 @@ 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
+ parameters ?: StartWorkspaceParameters ,
27
+ }
28
+
29
+ export interface StartWorkspaceParameters {
30
+ /**
31
+ * The last start of this workspace experienced in error (either in ws-proxy or supervisor).
32
+ */
33
+ error ?: boolean ,
34
+ }
35
+
36
+ export namespace StartWorkspaceParameters {
37
+ export function parse ( search : string ) : StartWorkspaceParameters | undefined {
38
+ try {
39
+ return queryString . parse ( search , { parseBooleans : true } ) ;
40
+ } catch ( err ) {
41
+ console . error ( "/start: error parsing search params" , err ) ;
42
+ return undefined ;
43
+ }
44
+ }
25
45
}
26
46
27
47
export interface StartWorkspaceState {
48
+ /**
49
+ * This is set to the istanceId we started (think we started on).
50
+ * We only receive updates for this particular instance, or none if not set.
51
+ */
28
52
startedInstanceId ?: string ;
29
53
workspaceInstance ?: WorkspaceInstance ;
30
54
workspace ?: Workspace ;
@@ -34,8 +58,12 @@ export interface StartWorkspaceState {
34
58
link : string
35
59
label : string
36
60
clientID ?: string
37
- }
38
- ideOptions ?: IDEOptions
61
+ } ;
62
+ ideOptions ?: IDEOptions ;
63
+ /**
64
+ * This flag is used to break the autostart-cycle explained in https://github.com/gitpod-io/gitpod/issues/8043
65
+ */
66
+ dontAutostart ?: boolean ;
39
67
}
40
68
41
69
export default class StartWorkspace extends React . Component < StartWorkspaceProps , StartWorkspaceState > {
@@ -75,8 +103,35 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
75
103
this . setState ( { error } ) ;
76
104
}
77
105
106
+ // we're coming back from a workspace cluster/IDE URL where we expected a workspace to be running but it wasn't because either:
107
+ // - this is a (very) old tab and the workspace already timed out
108
+ // - due to a start error our workspace terminated very quickly between:
109
+ // a) us being redirected to that IDEUrl (based on the first ws-manager update) and
110
+ // b) our requests being validated by ws-proxy
111
+ // we break this potentially DDOS cycle by showing a modal "Restart workspace?" instead of auto-restarting said workspace.
112
+ const params = this . props . parameters ;
113
+ if ( ! ! params ?. error ) {
114
+ this . setState ( { dontAutostart : true } ) ;
115
+ this . fetchWorkspaceInfo ( undefined ) ;
116
+ return ;
117
+ }
118
+
119
+ // IDE case:
120
+ // - we assume the workspace has already been started for us
121
+ // - we don't know it's instanceId
122
+ // we could also rely on "startWorkspace" to return us the "running" instance, but that opens the door for
123
+ // self-DDOSing as we found in the past (cmp. https://github.com/gitpod-io/gitpod/issues/8043)
124
+ if ( this . runsInIFrame ( ) ) {
125
+ // fetch current state display (incl. potential errors)
126
+ this . setState ( { dontAutostart : true } ) ;
127
+ this . fetchWorkspaceInfo ( undefined ) ;
128
+ return ;
129
+ }
130
+
131
+ // dashboard case (no previous errors): start workspace as quickly as possible
78
132
this . startWorkspace ( ) ;
79
- getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) )
133
+ // query IDE options so we can show them if necessary once the workspace is running
134
+ getGitpodService ( ) . server . getIDEOptions ( ) . then ( ideOptions => this . setState ( { ideOptions } ) ) ;
80
135
}
81
136
82
137
componentWillUnmount ( ) {
@@ -134,10 +189,9 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
134
189
this . redirectTo ( result . workspaceURL ) ;
135
190
return ;
136
191
}
137
- this . setState ( { startedInstanceId : result . instanceID } ) ;
138
- // Explicitly query state to guarantee we get at least one update
192
+ // Start listening too instance updates - and explicitly query state once to guarantee we get at least one update
139
193
// (needed for already started workspaces, and not hanging in 'Starting ...' for too long)
140
- this . fetchWorkspaceInfo ( ) ;
194
+ this . fetchWorkspaceInfo ( result . instanceID ) ;
141
195
} catch ( error ) {
142
196
console . error ( error ) ;
143
197
if ( typeof error === 'string' ) {
@@ -180,15 +234,28 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
180
234
}
181
235
}
182
236
183
- async fetchWorkspaceInfo ( ) {
237
+ /**
238
+ * Fetches initial WorkspaceInfo from the server. If there is a WorkspaceInstance for workspaceId, we feed it
239
+ * into "onInstanceUpdate" and start accepting further updates.
240
+ *
241
+ * @param startedInstanceId The instanceId we want to listen on
242
+ */
243
+ async fetchWorkspaceInfo ( startedInstanceId : string | undefined ) {
244
+ // this ensures we're receiving updates for this instance
245
+ if ( startedInstanceId ) {
246
+ this . setState ( { startedInstanceId } ) ;
247
+ }
248
+
184
249
const { workspaceId } = this . props ;
185
250
try {
186
251
const info = await getGitpodService ( ) . server . getWorkspace ( workspaceId ) ;
187
252
if ( info . latestInstance ) {
188
- this . setState ( {
189
- workspace : info . workspace
190
- } ) ;
191
- this . onInstanceUpdate ( info . latestInstance ) ;
253
+ const instance = info . latestInstance ;
254
+ this . setState ( ( s ) => ( {
255
+ workspace : info . workspace ,
256
+ startedInstanceId : s . startedInstanceId || instance . id , // note: here's a potential mismatch between startedInstanceId and instance.id. TODO(gpl) How to handle this?
257
+ } ) ) ;
258
+ this . onInstanceUpdate ( instance ) ;
192
259
}
193
260
} catch ( error ) {
194
261
console . error ( error ) ;
@@ -197,7 +264,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
197
264
}
198
265
199
266
notifyDidOpenConnection ( ) {
200
- this . fetchWorkspaceInfo ( ) ;
267
+ this . fetchWorkspaceInfo ( undefined ) ;
201
268
}
202
269
203
270
async onInstanceUpdate ( workspaceInstance : WorkspaceInstance ) {
@@ -210,7 +277,7 @@ export default class StartWorkspace extends React.Component<StartWorkspaceProps,
210
277
211
278
// Redirect to workspaceURL if we are not yet running in an iframe.
212
279
// It happens this late if we were waiting for a docker build.
213
- if ( ! this . runsInIFrame ( ) && workspaceInstance . ideUrl ) {
280
+ if ( ! this . runsInIFrame ( ) && workspaceInstance . ideUrl && ! this . state ?. dontAutostart ) {
214
281
this . redirectTo ( workspaceInstance . ideUrl ) ;
215
282
return ;
216
283
}
0 commit comments