@@ -43,7 +43,7 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
43
43
cmd := & clibase.Cmd {
44
44
Annotations : workspaceCommand ,
45
45
Use : "vscode <workspace> [<directory in workspace>]" ,
46
- Short : "Open a workspace in Visual Studio Code" ,
46
+ Short : fmt . Sprintf ( "Open a workspace in %s" , vscodeDesktopName ) ,
47
47
Middleware : clibase .Chain (
48
48
clibase .RequireRangeArgs (1 , 2 ),
49
49
r .InitClient (client ),
@@ -73,18 +73,12 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
73
73
insideThisWorkspace := insideAWorkspace && inWorkspaceName == workspaceName
74
74
75
75
if ! insideThisWorkspace {
76
- // We could optionally add a flag to skip wait, like with SSH.
77
- wait := false
78
- for _ , script := range workspaceAgent .Scripts {
79
- if script .StartBlocksLogin {
80
- wait = true
81
- break
82
- }
83
- }
76
+ // Wait for the agent to connect, we don't care about readiness
77
+ // otherwise (e.g. wait).
84
78
err = cliui .Agent (ctx , inv .Stderr , workspaceAgent .ID , cliui.AgentOptions {
85
79
Fetch : client .WorkspaceAgent ,
86
- FetchLogs : client . WorkspaceAgentLogsAfter ,
87
- Wait : wait ,
80
+ FetchLogs : nil ,
81
+ Wait : false ,
88
82
})
89
83
if err != nil {
90
84
if xerrors .Is (err , context .Canceled ) {
@@ -93,55 +87,33 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
93
87
return xerrors .Errorf ("agent: %w" , err )
94
88
}
95
89
96
- // If the ExpandedDirectory was initially missing, it could mean
97
- // that the agent hadn't reported it in yet. Retry once.
98
- if workspaceAgent .ExpandedDirectory == "" {
99
- autostart = false // Don't retry autostart.
100
- workspace , workspaceAgent , err = getWorkspaceAndAgent (ctx , inv , client , autostart , codersdk .Me , workspaceName )
90
+ // The agent will report it's expanded directory before leaving
91
+ // the created state, so we need to wait for that to happen.
92
+ // However, if no directory is set, the expanded directory will
93
+ // not be set either.
94
+ if workspaceAgent .Directory != "" {
95
+ workspace , workspaceAgent , err = waitForAgentCond (ctx , client , workspace , workspaceAgent , func (a codersdk.WorkspaceAgent ) bool {
96
+ return workspaceAgent .LifecycleState != codersdk .WorkspaceAgentLifecycleCreated
97
+ })
101
98
if err != nil {
102
- return xerrors .Errorf ("get workspace and agent retry : %w" , err )
99
+ return xerrors .Errorf ("wait for agent: %w" , err )
103
100
}
104
101
}
105
102
}
106
103
107
- directory := workspaceAgent . ExpandedDirectory // Empty unless agent directory is set.
104
+ var directory string
108
105
if len (inv .Args ) > 1 {
109
- d := inv .Args [1 ]
110
-
111
- switch {
112
- case insideThisWorkspace :
113
- // TODO(mafredri): Return error if directory doesn't exist?
114
- directory , err = filepath .Abs (d )
115
- if err != nil {
116
- return xerrors .Errorf ("expand directory: %w" , err )
117
- }
118
-
119
- case d == "~" || strings .HasPrefix (d , "~/" ):
120
- return xerrors .Errorf ("path %q requires expansion and is not supported, use an absolute path instead" , d )
121
-
122
- case workspaceAgent .OperatingSystem == "windows" :
123
- switch {
124
- case directory != "" && ! isWindowsAbsPath (d ):
125
- directory = windowsJoinPath (directory , d )
126
- case isWindowsAbsPath (d ):
127
- directory = d
128
- default :
129
- return xerrors .Errorf ("path %q not supported, use an absolute path instead" , d )
130
- }
131
-
132
- // Note that we use `path` instead of `filepath` since we want Unix behavior.
133
- case directory != "" && ! path .IsAbs (d ):
134
- directory = path .Join (directory , d )
135
- case path .IsAbs (d ):
136
- directory = d
137
- default :
138
- return xerrors .Errorf ("path %q not supported, use an absolute path instead" , d )
139
- }
106
+ directory = inv .Args [1 ]
140
107
}
141
-
142
- u , err := url .Parse ("vscode://coder.coder-remote/open" )
108
+ directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
143
109
if err != nil {
144
- return xerrors .Errorf ("parse vscode URI: %w" , err )
110
+ return xerrors .Errorf ("resolve agent path: %w" , err )
111
+ }
112
+
113
+ u := & url.URL {
114
+ Scheme : "vscode" ,
115
+ Host : "coder.coder-remote" ,
116
+ Path : "/open" ,
145
117
}
146
118
147
119
qp := url.Values {}
@@ -190,6 +162,16 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
190
162
}
191
163
if err != nil {
192
164
if ! generateToken {
165
+ // This is not an important step, so we don't want
166
+ // to block the user here.
167
+ token := qp .Get ("token" )
168
+ wait := doAsync (func () {
169
+ // Best effort, we don't care if this fails.
170
+ apiKeyID := strings .SplitN (token , "-" , 2 )[0 ]
171
+ _ = client .DeleteAPIKey (ctx , codersdk .Me , apiKeyID )
172
+ })
173
+ defer wait ()
174
+
193
175
qp .Del ("token" )
194
176
u .RawQuery = qp .Encode ()
195
177
}
@@ -226,19 +208,50 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
226
208
return cmd
227
209
}
228
210
211
+ // waitForAgentCond uses the watch workspace API to update the agent information
212
+ // until the condition is met.
213
+ func waitForAgentCond (ctx context.Context , client * codersdk.Client , workspace codersdk.Workspace , workspaceAgent codersdk.WorkspaceAgent , cond func (codersdk.WorkspaceAgent ) bool ) (codersdk.Workspace , codersdk.WorkspaceAgent , error ) {
214
+ ctx , cancel := context .WithCancel (ctx )
215
+ defer cancel ()
216
+
217
+ if cond (workspaceAgent ) {
218
+ return workspace , workspaceAgent , nil
219
+ }
220
+
221
+ wc , err := client .WatchWorkspace (ctx , workspace .ID )
222
+ if err != nil {
223
+ return workspace , workspaceAgent , xerrors .Errorf ("watch workspace: %w" , err )
224
+ }
225
+
226
+ for workspace = range wc {
227
+ workspaceAgent , err = getWorkspaceAgent (workspace , workspaceAgent .Name )
228
+ if err != nil {
229
+ return workspace , workspaceAgent , xerrors .Errorf ("get workspace agent: %w" , err )
230
+ }
231
+ if cond (workspaceAgent ) {
232
+ return workspace , workspaceAgent , nil
233
+ }
234
+ }
235
+
236
+ return workspace , workspaceAgent , xerrors .New ("watch workspace: unexpected closed channel" )
237
+ }
238
+
229
239
// isWindowsAbsPath checks if the path is an absolute path on Windows. On Unix
230
240
// systems the check is very simplistic and does not cover edge cases.
231
- //
232
- //nolint:revive // Shadow path variable for readability.
233
- func isWindowsAbsPath (path string ) bool {
241
+ func isWindowsAbsPath (p string ) bool {
234
242
if runtime .GOOS == "windows" {
235
- return filepath .IsAbs (path )
243
+ return filepath .IsAbs (p )
236
244
}
237
245
238
246
switch {
239
- case len (path ) >= 2 && path [1 ] == ':' :
247
+ case len (p ) < 2 :
248
+ return false
249
+ case p [1 ] == ':' :
240
250
// Path starts with a drive letter.
241
- return len (path ) == 2 || (len (path ) >= 4 && path [2 ] == '\\' && path [3 ] == '\\' )
251
+ return len (p ) == 2 || (len (p ) >= 3 && p [2 ] == '\\' )
252
+ case p [0 ] == '\\' && p [1 ] == '\\' :
253
+ // Path starts with \\.
254
+ return true
242
255
default :
243
256
return false
244
257
}
@@ -262,7 +275,62 @@ func windowsJoinPath(elem ...string) string {
262
275
s = e
263
276
continue
264
277
}
265
- s += "\\ " + strings .TrimSuffix (s , "\\ " )
278
+ s += "\\ " + strings .TrimSuffix (e , "\\ " )
266
279
}
267
280
return s
268
281
}
282
+
283
+ // resolveAgentAbsPath resolves the absolute path to a file or directory in the
284
+ // workspace. If the path is relative, it will be resolved relative to the
285
+ // workspace's expanded directory. If the path is absolute, it will be returned
286
+ // as-is. If the path is relative and the workspace directory is not expanded,
287
+ // an error will be returned.
288
+ //
289
+ // If the path is being resolved within the workspace, the path will be resolved
290
+ // relative to the current working directory.
291
+ func resolveAgentAbsPath (workingDirectory , relOrAbsPath , agentOS string , local bool ) (string , error ) {
292
+ if relOrAbsPath == "" {
293
+ return workingDirectory , nil
294
+ }
295
+
296
+ switch {
297
+ case relOrAbsPath == "~" || strings .HasPrefix (relOrAbsPath , "~/" ):
298
+ return "" , xerrors .Errorf ("path %q requires expansion and is not supported, use an absolute path instead" , relOrAbsPath )
299
+
300
+ case local :
301
+ p , err := filepath .Abs (relOrAbsPath )
302
+ if err != nil {
303
+ return "" , xerrors .Errorf ("expand path: %w" , err )
304
+ }
305
+ return p , nil
306
+
307
+ case agentOS == "windows" :
308
+ switch {
309
+ case workingDirectory != "" && ! isWindowsAbsPath (relOrAbsPath ):
310
+ return windowsJoinPath (workingDirectory , relOrAbsPath ), nil
311
+ case isWindowsAbsPath (relOrAbsPath ):
312
+ return relOrAbsPath , nil
313
+ default :
314
+ return "" , xerrors .Errorf ("path %q not supported, use an absolute path instead" , relOrAbsPath )
315
+ }
316
+
317
+ // Note that we use `path` instead of `filepath` since we want Unix behavior.
318
+ case workingDirectory != "" && ! path .IsAbs (relOrAbsPath ):
319
+ return path .Join (workingDirectory , relOrAbsPath ), nil
320
+ case path .IsAbs (relOrAbsPath ):
321
+ return relOrAbsPath , nil
322
+ default :
323
+ return "" , xerrors .Errorf ("path %q not supported, use an absolute path instead" , relOrAbsPath )
324
+ }
325
+ }
326
+
327
+ func doAsync (f func ()) (wait func ()) {
328
+ done := make (chan struct {})
329
+ go func () {
330
+ defer close (done )
331
+ f ()
332
+ }()
333
+ return func () {
334
+ <- done
335
+ }
336
+ }
0 commit comments