Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,6 @@
"activationEvents": [
"onResolveRemoteAuthority:ssh-remote",
"onCommand:coder.connect",
"onCommand:coder.open",
"onCommand:coder.login",
"onView:coderRemote",
"onUri"
],
"extensionDependencies": [
Expand Down Expand Up @@ -77,6 +74,11 @@
{
"command": "coder.open",
"title": "Coder: Open Workspace"
},
{
"command": "coder.workspace.update",
"title": "Coder: Update Workspace",
"when": "coder.workspace.updatable"
}
]
},
Expand Down
22 changes: 20 additions & 2 deletions src/commands.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
import axios from "axios"
import { getUser, getWorkspaces } from "coder/site/src/api/api"
import { getUser, getWorkspaces, updateWorkspaceVersion } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import * as vscode from "vscode"
import { Remote } from "./remote"
import { Storage } from "./storage"

export class Commands {
public constructor(private readonly storage: Storage) {}
public constructor(private readonly vscodeProposed: typeof vscode, private readonly storage: Storage) {}

public async login(...args: string[]): Promise<void> {
let url: string | undefined = args.length >= 1 ? args[0] : undefined
Expand Down Expand Up @@ -215,4 +215,22 @@ export class Commands {
reuseWindow: !newWindow,
})
}

public async updateWorkspace(): Promise<void> {
if (!this.storage.workspace) {
return
}
const action = await this.vscodeProposed.window.showInformationMessage(
"Update Workspace",
{
useCustom: true,
modal: true,
detail: `${this.storage.workspace.owner_name}/${this.storage.workspace.name} will be updated then this window will reload to watch the build logs and reconnect.`,
},
"Update",
)
if (action === "Update") {
await updateWorkspaceVersion(this.storage.workspace)
}
}
}
13 changes: 7 additions & 6 deletions src/extension.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,12 +50,6 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
},
})

const commands = new Commands(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))

// 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!
Expand All @@ -75,6 +69,13 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
false,
)

const commands = new Commands(vscodeProposed, 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.workspace.update", commands.updateWorkspace.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.
Expand Down
72 changes: 57 additions & 15 deletions src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -72,9 +72,8 @@ export class Remote {
}

// Find the workspace from the URI scheme provided!
let workspace: Workspace
try {
workspace = await getWorkspaceByOwnerAndName(parts[0], parts[1])
this.storage.workspace = await getWorkspaceByOwnerAndName(parts[0], parts[1])
} catch (error) {
if (!axios.isAxiosError(error)) {
throw error
Expand Down Expand Up @@ -120,10 +119,10 @@ export class Remote {

const disposables: vscode.Disposable[] = []
// Register before connection so the label still displays!
disposables.push(this.registerLabelFormatter(`${workspace.owner_name}/${workspace.name}`))
disposables.push(this.registerLabelFormatter(`${this.storage.workspace.owner_name}/${this.storage.workspace.name}`))

let buildComplete: undefined | (() => void)
if (workspace.latest_build.status === "stopped") {
if (this.storage.workspace.latest_build.status === "stopped") {
this.vscodeProposed.window.withProgress(
{
location: vscode.ProgressLocation.Notification,
Expand All @@ -135,15 +134,18 @@ export class Remote {
buildComplete = r
}),
)
workspace = {
...workspace,
latest_build: await startWorkspace(workspace.id),
this.storage.workspace = {
...this.storage.workspace,
latest_build: await startWorkspace(this.storage.workspace.id),
}
}

// If a build is running we should stream the logs to the user so they can
// watch what's going on!
if (workspace.latest_build.status === "pending" || workspace.latest_build.status === "starting") {
if (
this.storage.workspace.latest_build.status === "pending" ||
this.storage.workspace.latest_build.status === "starting"
) {
const writeEmitter = new vscode.EventEmitter<string>()
// We use a terminal instead of an output channel because it feels more
// familiar to a user!
Expand All @@ -160,11 +162,11 @@ export class Remote {
} as Partial<vscode.Pseudoterminal> as any,
})
// This fetches the initial bunch of logs.
const logs = await getWorkspaceBuildLogs(workspace.latest_build.id, new Date())
const logs = await getWorkspaceBuildLogs(this.storage.workspace.latest_build.id, new Date())
logs.forEach((log) => writeEmitter.fire(log.output + "\r\n"))
terminal.show(true)
// This follows the logs for new activity!
let path = `/api/v2/workspacebuilds/${workspace.latest_build.id}/logs?follow=true`
let path = `/api/v2/workspacebuilds/${this.storage.workspace.latest_build.id}/logs?follow=true`
if (logs.length) {
path += `&after=${logs[logs.length - 1].id}`
}
Expand Down Expand Up @@ -198,15 +200,15 @@ export class Remote {
})
})
writeEmitter.fire("Build complete")
workspace = await getWorkspace(workspace.id)
terminal.hide()
this.storage.workspace = await getWorkspace(this.storage.workspace.id)
terminal.dispose()

if (buildComplete) {
buildComplete()
}
}

const agents = workspace.latest_build.resources.reduce((acc, resource) => {
const agents = this.storage.workspace.latest_build.resources.reduce((acc, resource) => {
return acc.concat(resource.agents || [])
}, [] as WorkspaceAgent[])

Expand Down Expand Up @@ -250,7 +252,7 @@ export class Remote {
await fs.writeFile(this.storage.getUserSettingsPath(), jsonc.applyEdits(settingsContent, edits))

const workspaceUpdate = new vscode.EventEmitter<Workspace>()
const watchURL = new URL(`${this.storage.getURL()}/api/v2/workspaces/${workspace.id}/watch`)
const watchURL = new URL(`${this.storage.getURL()}/api/v2/workspaces/${this.storage.workspace.id}/watch`)
const eventSource = new EventSource(watchURL.toString(), {
headers: {
"Coder-Session-Token": await this.storage.getSessionToken(),
Expand All @@ -262,11 +264,44 @@ export class Remote {
eventSource.addEventListener("error", () => {
// TODO: Add debug output that we got an error here!
})

const workspaceUpdatedStatus = vscode.window.createStatusBarItem(vscode.StatusBarAlignment.Left, 999)
disposables.push(workspaceUpdatedStatus)

const refreshWorkspaceUpdatedStatus = (newWorkspace: Workspace) => {
// If the newly gotten workspace was updated, then we show a notification
// to the user that they should update.
if (!this.storage.workspace?.outdated && newWorkspace.outdated) {
vscode.window
.showInformationMessage("A new version of your workspace is available.", "Update")
.then((action) => {
if (action === "Update") {
vscode.commands.executeCommand("coder.workspace.update", newWorkspace)
}
})
}
if (!newWorkspace.outdated) {
vscode.commands.executeCommand("setContext", "coder.workspace.updatable", false)
workspaceUpdatedStatus.hide()
return
}
workspaceUpdatedStatus.name = "Coder Workspace Update"
workspaceUpdatedStatus.text = "$(fold-up) Update Workspace"
workspaceUpdatedStatus.command = "coder.workspace.update"
// Important for hiding the "Update Workspace" command.
vscode.commands.executeCommand("setContext", "coder.workspace.updatable", true)
workspaceUpdatedStatus.show()
}
// Show an initial status!
refreshWorkspaceUpdatedStatus(this.storage.workspace)

eventSource.addEventListener("data", (event: MessageEvent<string>) => {
const workspace = JSON.parse(event.data) as Workspace
if (!workspace) {
return
}
refreshWorkspaceUpdatedStatus(workspace)
this.storage.workspace = workspace
workspaceUpdate.fire(workspace)
if (workspace.latest_build.status === "stopping" || workspace.latest_build.status === "stopped") {
const action = this.vscodeProposed.window.showInformationMessage(
Expand All @@ -283,6 +318,13 @@ export class Remote {
}
this.reloadWindow()
}
// If a new build is initialized for a workspace, we automatically
// reload the window. Then the build log will appear, and startup
// will continue as expected.
if (workspace.latest_build.status === "starting") {
this.reloadWindow()
return
}
})

if (agent.status === "connecting") {
Expand Down Expand Up @@ -352,7 +394,7 @@ export class Remote {
})

// Register the label formatter again because SSH overrides it!
let label = `${workspace.owner_name}/${workspace.name}`
let label = `${this.storage.workspace.owner_name}/${this.storage.workspace.name}`
if (agents.length > 1) {
label += `/${agent.name}`
}
Expand Down
5 changes: 4 additions & 1 deletion src/storage.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import axios from "axios"
import { execFile } from "child_process"
import { getBuildInfo } from "coder/site/src/api/api"
import { Workspace } from "coder/site/src/api/typesGenerated"
import * as crypto from "crypto"
import { createWriteStream, createReadStream } from "fs"
import { createReadStream, createWriteStream } from "fs"
import fs from "fs/promises"
import { ensureDir } from "fs-extra"
import { IncomingMessage } from "http"
Expand All @@ -12,6 +13,8 @@ import prettyBytes from "pretty-bytes"
import * as vscode from "vscode"

export class Storage {
public workspace?: Workspace

constructor(
private readonly output: vscode.OutputChannel,
private readonly memento: vscode.Memento,
Expand Down