@@ -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 inVisual 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 directoryis 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
+ }