-
Notifications
You must be signed in to change notification settings - Fork 21
/
Copy pathextension.ts
269 lines (243 loc) · 11.5 KB
/
extension.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
"use strict"
import axios, { isAxiosError } from "axios"
import { getErrorMessage } from "coder/site/src/api/errors"
import * as module from "module"
import * as vscode from "vscode"
import { makeCoderSdk, needToken } from "./api"
import { errToStr } from "./api-helper"
import { Commands } from "./commands"
import { CertificateError, getErrorDetail } from "./error"
import { getMemoryLogger } from "./memoryLogger"
import { Remote } from "./remote"
import { Storage } from "./storage"
import { toSafeHost } from "./util"
import { WorkspaceQuery, WorkspaceProvider } from "./workspacesProvider"
export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
// Initialize the memory logger right when the extension starts.
const logger = getMemoryLogger()
await logger.initLogFile(ctx.globalStorageUri.fsPath)
// Log initial memory usage and activation
logger.info("CODER extension activating")
logger.logMemoryUsage("EXTENSION_ACTIVATE")
// Register disposal of the logger when the extension deactivates
ctx.subscriptions.push({ dispose: () => logger.dispose() })
// Log extension mode
logger.info(`Extension mode: ${extensionModeToString(ctx.extensionMode)}`)
// The Remote SSH extension's proposed APIs are used to override the SSH host
// name in VS Code itself. It's visually unappealing having a lengthy name!
//
// This is janky, but that's alright since it provides such minimal
// functionality to the extension.
//
// Cursor and VSCode are covered by ms remote, and the only other is windsurf for now
// Means that vscodium is not supported by this for now
const remoteSSHExtension =
vscode.extensions.getExtension("codeium.windsurf-remote-openssh") ||
vscode.extensions.getExtension("ms-vscode-remote.remote-ssh")
if (!remoteSSHExtension) {
logger.error("Remote SSH extension not found, cannot activate Coder extension")
vscode.window.showErrorMessage("Remote SSH extension not found, cannot activate Coder extension")
throw new Error("Remote SSH extension not found")
}
logger.info(`Found Remote SSH extension: ${remoteSSHExtension.id}`)
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const vscodeProposed: typeof vscode = (module as any)._load(
"vscode",
{
filename: remoteSSHExtension?.extensionPath,
},
false,
)
const output = vscode.window.createOutputChannel("Coder")
const storage = new Storage(output, ctx.globalState, ctx.secrets, ctx.globalStorageUri, ctx.logUri)
// This client tracks the current login and will be used through the life of
// the plugin to poll workspaces for the current login, as well as being used
// in commands that operate on the current login.
const url = storage.getUrl()
const restClient = await makeCoderSdk(url || "", await storage.getSessionToken(), storage)
const myWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.Mine, restClient, storage, 5)
const allWorkspacesProvider = new WorkspaceProvider(WorkspaceQuery.All, restClient, storage)
// createTreeView, unlike registerTreeDataProvider, gives us the tree view API
// (so we can see when it is visible) but otherwise they have the same effect.
const myWsTree = vscode.window.createTreeView("myWorkspaces", { treeDataProvider: myWorkspacesProvider })
myWorkspacesProvider.setVisibility(myWsTree.visible)
myWsTree.onDidChangeVisibility((event) => {
myWorkspacesProvider.setVisibility(event.visible)
})
const allWsTree = vscode.window.createTreeView("allWorkspaces", { treeDataProvider: allWorkspacesProvider })
allWorkspacesProvider.setVisibility(allWsTree.visible)
allWsTree.onDidChangeVisibility((event) => {
allWorkspacesProvider.setVisibility(event.visible)
})
// Handle vscode:// URIs.
vscode.window.registerUriHandler({
handleUri: async (uri) => {
const params = new URLSearchParams(uri.query)
if (uri.path === "/open") {
const owner = params.get("owner")
const workspace = params.get("workspace")
const agent = params.get("agent")
const folder = params.get("folder")
const openRecent =
params.has("openRecent") && (!params.get("openRecent") || params.get("openRecent") === "true")
if (!owner) {
throw new Error("owner must be specified as a query parameter")
}
if (!workspace) {
throw new Error("workspace must be specified as a query parameter")
}
// We are not guaranteed that the URL we currently have is for the URL
// this workspace belongs to, or that we even have a URL at all (the
// queries will default to localhost) so ask for it if missing.
// Pre-populate in case we do have the right URL so the user can just
// hit enter and move on.
const url = await commands.maybeAskUrl(params.get("url"), storage.getUrl())
if (url) {
restClient.setHost(url)
await storage.setUrl(url)
} else {
throw new Error("url must be provided or specified as a query parameter")
}
// If the token is missing we will get a 401 later and the user will be
// prompted to sign in again, so we do not need to ensure it is set now.
// For non-token auth, we write a blank token since the `vscodessh`
// command currently always requires a token file. However, if there is
// a query parameter for non-token auth go ahead and use it anyway; all
// that really matters is the file is created.
const token = needToken() ? params.get("token") : (params.get("token") ?? "")
if (token) {
restClient.setSessionToken(token)
await storage.setSessionToken(token)
}
// Store on disk to be used by the cli.
await storage.configureCli(toSafeHost(url), url, token)
vscode.commands.executeCommand("coder.open", owner, workspace, agent, folder, openRecent)
} else {
throw new Error(`Unknown path ${uri.path}`)
}
},
})
// Register globally available commands. Many of these have visibility
// controlled by contexts, see `when` in the package.json.
const commands = new Commands(vscodeProposed, restClient, storage)
vscode.commands.registerCommand("coder.login", commands.login.bind(commands))
vscode.commands.registerCommand("coder.logout", commands.logout.bind(commands))
vscode.commands.registerCommand("coder.open", commands.open.bind(commands))
vscode.commands.registerCommand("coder.openFromSidebar", commands.openFromSidebar.bind(commands))
vscode.commands.registerCommand("coder.workspace.update", commands.updateWorkspace.bind(commands))
vscode.commands.registerCommand("coder.createWorkspace", commands.createWorkspace.bind(commands))
vscode.commands.registerCommand("coder.navigateToWorkspace", commands.navigateToWorkspace.bind(commands))
vscode.commands.registerCommand(
"coder.navigateToWorkspaceSettings",
commands.navigateToWorkspaceSettings.bind(commands),
)
vscode.commands.registerCommand("coder.refreshWorkspaces", () => {
myWorkspacesProvider.fetchAndRefresh()
allWorkspacesProvider.fetchAndRefresh()
})
vscode.commands.registerCommand("coder.viewLogs", commands.viewLogs.bind(commands))
// Since the "onResolveRemoteAuthority:ssh-remote" activation event exists
// in package.json we're able to perform actions before the authority is
// resolved by the remote SSH extension.
if (vscodeProposed.env.remoteAuthority) {
const remote = new Remote(vscodeProposed, storage, commands, ctx.extensionMode)
try {
const details = await remote.setup(vscodeProposed.env.remoteAuthority)
if (details) {
// Authenticate the plugin client which is used in the sidebar to display
// workspaces belonging to this deployment.
restClient.setHost(details.url)
restClient.setSessionToken(details.token)
}
} catch (ex) {
if (ex instanceof CertificateError) {
storage.writeToCoderOutputChannel(ex.x509Err || ex.message)
await ex.showModal("Failed to open workspace")
} else if (isAxiosError(ex)) {
const msg = getErrorMessage(ex, "None")
const detail = getErrorDetail(ex) || "None"
const urlString = axios.getUri(ex.config)
const method = ex.config?.method?.toUpperCase() || "request"
const status = ex.response?.status || "None"
const message = `API ${method} to '${urlString}' failed.\nStatus code: ${status}\nMessage: ${msg}\nDetail: ${detail}`
storage.writeToCoderOutputChannel(message)
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
detail: message,
modal: true,
useCustom: true,
})
} else {
const message = errToStr(ex, "No error message was provided")
storage.writeToCoderOutputChannel(message)
await vscodeProposed.window.showErrorMessage("Failed to open workspace", {
detail: message,
modal: true,
useCustom: true,
})
}
// Always close remote session when we fail to open a workspace.
await remote.closeRemote()
return
}
}
// See if the plugin client is authenticated.
const baseUrl = restClient.getAxiosInstance().defaults.baseURL
if (baseUrl) {
storage.writeToCoderOutputChannel(`Logged in to ${baseUrl}; checking credentials`)
restClient
.getAuthenticatedUser()
.then(async (user) => {
if (user && user.roles) {
storage.writeToCoderOutputChannel("Credentials are valid")
vscode.commands.executeCommand("setContext", "coder.authenticated", true)
if (user.roles.find((role) => role.name === "owner")) {
await vscode.commands.executeCommand("setContext", "coder.isOwner", true)
}
// Fetch and monitor workspaces, now that we know the client is good.
myWorkspacesProvider.fetchAndRefresh()
allWorkspacesProvider.fetchAndRefresh()
} else {
storage.writeToCoderOutputChannel(`No error, but got unexpected response: ${user}`)
}
})
.catch((error) => {
// This should be a failure to make the request, like the header command
// errored.
storage.writeToCoderOutputChannel(`Failed to check user authentication: ${error.message}`)
vscode.window.showErrorMessage(`Failed to check user authentication: ${error.message}`)
})
.finally(() => {
vscode.commands.executeCommand("setContext", "coder.loaded", true)
})
} else {
storage.writeToCoderOutputChannel("Not currently logged in")
vscode.commands.executeCommand("setContext", "coder.loaded", true)
// Handle autologin, if not already logged in.
const cfg = vscode.workspace.getConfiguration()
if (cfg.get("coder.autologin") === true) {
const defaultUrl = cfg.get("coder.defaultUrl") || process.env.CODER_URL
if (defaultUrl) {
vscode.commands.executeCommand("coder.login", defaultUrl, undefined, undefined, "true")
}
}
}
logger.info("Coder extension activation complete")
}
function extensionModeToString(mode: vscode.ExtensionMode): string {
switch (mode) {
case vscode.ExtensionMode.Development:
return "Development"
case vscode.ExtensionMode.Production:
return "Production"
case vscode.ExtensionMode.Test:
return "Test"
default:
return `Unknown (${mode})`
}
}
// Add deactivation handler to log memory usage on extension shutdown
export function deactivate(): void {
const logger = getMemoryLogger()
logger.info("Coder extension deactivating")
logger.logMemoryUsage("EXTENSION_DEACTIVATE")
}