@@ -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,30 @@ 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 )
101
- if err != nil {
102
- return xerrors . Errorf ( "get workspace and agent retry: %w" , err )
103
- }
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
+ })
104
98
}
105
99
}
106
100
107
- directory := workspaceAgent . ExpandedDirectory // Empty unless agent directory is set.
101
+ var directory string
108
102
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
- }
103
+ directory = inv .Args [1 ]
140
104
}
141
-
142
- u , err := url .Parse ("vscode://coder.coder-remote/open" )
105
+ directory , err = resolveAgentAbsPath (workspaceAgent .ExpandedDirectory , directory , workspaceAgent .OperatingSystem , insideThisWorkspace )
143
106
if err != nil {
144
- return xerrors .Errorf ("parse vscode URI: %w" , err )
107
+ return xerrors .Errorf ("resolve agent path: %w" , err )
108
+ }
109
+
110
+ u := & url.URL {
111
+ Scheme : "vscode" ,
112
+ Host : "coder.coder-remote" ,
113
+ Path : "/open" ,
145
114
}
146
115
147
116
qp := url.Values {}
@@ -190,6 +159,16 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
190
159
}
191
160
if err != nil {
192
161
if ! generateToken {
162
+ // This is not an important step, so we don't want
163
+ // to block the user here.
164
+ token := qp .Get ("token" )
165
+ wait := doAsync (func () {
166
+ // Best effort, we don't care if this fails.
167
+ apiKeyID := strings .SplitN (token , "-" , 2 )[0 ]
168
+ _ = client .DeleteAPIKey (ctx , codersdk .Me , apiKeyID )
169
+ })
170
+ defer wait ()
171
+
193
172
qp .Del ("token" )
194
173
u .RawQuery = qp .Encode ()
195
174
}
@@ -226,19 +205,50 @@ func (r *RootCmd) openVSCode() *clibase.Cmd {
226
205
return cmd
227
206
}
228
207
208
+ // waitForAgentCond uses the watch workspace API to update the agent information
209
+ // until the condition is met.
210
+ func waitForAgentCond (ctx context.Context , client * codersdk.Client , workspace codersdk.Workspace , workspaceAgent codersdk.WorkspaceAgent , cond func (codersdk.WorkspaceAgent ) bool ) (codersdk.Workspace , codersdk.WorkspaceAgent , error ) {
211
+ ctx , cancel := context .WithCancel (ctx )
212
+ defer cancel ()
213
+
214
+ if cond (workspaceAgent ) {
215
+ return workspace , workspaceAgent , nil
216
+ }
217
+
218
+ wc , err := client .WatchWorkspace (ctx , workspace .ID )
219
+ if err != nil {
220
+ return workspace , workspaceAgent , xerrors .Errorf ("watch workspace: %w" , err )
221
+ }
222
+
223
+ for workspace = range wc {
224
+ workspaceAgent , err = getWorkspaceAgent (workspace , workspaceAgent .Name )
225
+ if err != nil {
226
+ return workspace , workspaceAgent , xerrors .Errorf ("get workspace agent: %w" , err )
227
+ }
228
+ if cond (workspaceAgent ) {
229
+ return workspace , workspaceAgent , nil
230
+ }
231
+ }
232
+
233
+ return workspace , workspaceAgent , xerrors .New ("watch workspace: unexpected closed channel" )
234
+ }
235
+
229
236
// isWindowsAbsPath checks if the path is an absolute path on Windows. On Unix
230
237
// 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 {
238
+ func isWindowsAbsPath (p string ) bool {
234
239
if runtime .GOOS == "windows" {
235
- return filepath .IsAbs (path )
240
+ return filepath .IsAbs (p )
236
241
}
237
242
238
243
switch {
239
- case len (path ) >= 2 && path [1 ] == ':' :
244
+ case len (p ) < 2 :
245
+ return false
246
+ case p [1 ] == ':' :
240
247
// Path starts with a drive letter.
241
- return len (path ) == 2 || (len (path ) >= 4 && path [2 ] == '\\' && path [3 ] == '\\' )
248
+ return len (p ) == 2 || (len (p ) >= 3 && p [2 ] == '\\' )
249
+ case p [0 ] == '\\' && p [1 ] == '\\' :
250
+ // Path starts with \\.
251
+ return true
242
252
default :
243
253
return false
244
254
}
@@ -262,7 +272,59 @@ func windowsJoinPath(elem ...string) string {
262
272
s = e
263
273
continue
264
274
}
265
- s += "\\ " + strings .TrimSuffix (s , "\\ " )
275
+ s += "\\ " + strings .TrimSuffix (e , "\\ " )
266
276
}
267
277
return s
268
278
}
279
+
280
+ // resolveAgentAbsPath resolves the absolute path to a file or directory in the
281
+ // workspace. If the path is relative, it will be resolved relative to the
282
+ // workspace's expanded directory. If the path is absolute, it will be returned
283
+ // as-is. If the path is relative and the workspace directory is not expanded,
284
+ // an error will be returned.
285
+ //
286
+ // If the path is being resolved within the workspace, the path will be resolved
287
+ // relative to the current working directory.
288
+ func resolveAgentAbsPath (workingDirectory , relOrAbsPath , agentOS string , local bool ) (string , error ) {
289
+ if relOrAbsPath == "" {
290
+ return workingDirectory , nil
291
+ }
292
+
293
+ switch {
294
+ case local :
295
+ p , err := filepath .Abs (relOrAbsPath )
296
+ if err != nil {
297
+ return "" , xerrors .Errorf ("expand path: %w" , err )
298
+ }
299
+ return p , nil
300
+
301
+ case agentOS == "windows" :
302
+ switch {
303
+ case workingDirectory != "" && ! isWindowsAbsPath (relOrAbsPath ):
304
+ return windowsJoinPath (workingDirectory , relOrAbsPath ), nil
305
+ case isWindowsAbsPath (relOrAbsPath ):
306
+ return relOrAbsPath , nil
307
+ default :
308
+ return "" , xerrors .Errorf ("path %q not supported, use an absolute path instead" , relOrAbsPath )
309
+ }
310
+
311
+ // Note that we use `path` instead of `filepath` since we want Unix behavior.
312
+ case workingDirectory != "" && ! path .IsAbs (relOrAbsPath ):
313
+ return path .Join (workingDirectory , relOrAbsPath ), nil
314
+ case path .IsAbs (relOrAbsPath ):
315
+ return relOrAbsPath , nil
316
+ default :
317
+ return "" , xerrors .Errorf ("path %q not supported, use an absolute path instead" , relOrAbsPath )
318
+ }
319
+ }
320
+
321
+ func doAsync (f func ()) (wait func ()) {
322
+ done := make (chan struct {})
323
+ go func () {
324
+ defer close (done )
325
+ f ()
326
+ }()
327
+ return func () {
328
+ <- done
329
+ }
330
+ }
0 commit comments