Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
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
2 changes: 1 addition & 1 deletion src/api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ export function needToken(): boolean {
/**
* Create a new agent based off the current settings.
*/
async function createHttpAgent(): Promise<ProxyAgent> {
export async function createHttpAgent(): Promise<ProxyAgent> {
const cfg = vscode.workspace.getConfiguration()
const insecure = Boolean(cfg.get("coder.insecure"))
const certFile = expandPath(String(cfg.get("coder.tlsCertFile") ?? "").trim())
Expand Down
93 changes: 93 additions & 0 deletions src/inbox.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Api } from "coder/site/src/api/api"
import { ProxyAgent } from "proxy-agent"
import * as vscode from "vscode"
import { WebSocket } from "ws"
import { errToStr } from "./api-helper"
import { type Storage } from "./storage"

type InboxMessage = {
unread_count: number
notification: {
id: string
user_id: string
template_id: string
targets: string[]
title: string
content: string
actions: Record<string, string>
read_at: string
created_at: string
}
}

// These are the template IDs of our notifications.
// Maybe in the future we should avoid hardcoding
// these in both coderd and here.
const TEMPLATE_WORKSPACE_OUT_OF_MEMORY = "a9d027b4-ac49-4fb1-9f6d-45af15f64e7a"
const TEMPLATE_WORKSPACE_OUT_OF_DISK = "f047f6a3-5713-40f7-85aa-0394cce9fa3a"

export class Inbox implements vscode.Disposable {
private readonly storage: Storage
private disposed = false
private socket: WebSocket

constructor(httpAgent: ProxyAgent, restClient: Api, storage: Storage) {
this.storage = storage

const baseUrlRaw = restClient.getAxiosInstance().defaults.baseURL
if (!baseUrlRaw) {
throw new Error("No base URL set on REST client")
}

const baseUrl = new URL(baseUrlRaw)
const socketProto = baseUrl.protocol === "https:" ? "wss:" : "ws:"
const socketUrlRaw = `${socketProto}//${baseUrl.host}/api/v2/notifications/watch`

const coderSessionTokenHeader = "Coder-Session-Token"
this.socket = new WebSocket(new URL(socketUrlRaw), {
followRedirects: true,
agent: httpAgent,
headers: {
[coderSessionTokenHeader]: restClient.getAxiosInstance().defaults.headers.common[coderSessionTokenHeader] as
| string
| undefined,
},
})

this.socket.on("open", () => {
this.storage.writeToCoderOutputChannel("Listening to Coder Inbox")
})

this.socket.on("error", (error) => {
this.notifyError(error)
})

this.socket.on("message", (data) => {
try {
const inboxMessage = JSON.parse(data.toString()) as InboxMessage

if (
inboxMessage.notification.template_id === TEMPLATE_WORKSPACE_OUT_OF_DISK ||
inboxMessage.notification.template_id === TEMPLATE_WORKSPACE_OUT_OF_MEMORY
) {
vscode.window.showWarningMessage(inboxMessage.notification.title)
}
} catch (error) {
this.notifyError(error)
}
})
}

dispose() {
if (!this.disposed) {
this.storage.writeToCoderOutputChannel("No longer listening to Coder Inbox")
this.socket.close()
this.disposed = true
}
}

private notifyError(error: unknown) {
const message = errToStr(error, "Got empty error while monitoring Coder Inbox")
this.storage.writeToCoderOutputChannel(message)
}
}
8 changes: 7 additions & 1 deletion src/remote.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,13 @@ import * as path from "path"
import prettyBytes from "pretty-bytes"
import * as semver from "semver"
import * as vscode from "vscode"
import { makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
import { createHttpAgent, makeCoderSdk, needToken, startWorkspaceIfStoppedOrFailed, waitForBuild } from "./api"
import { extractAgents } from "./api-helper"
import * as cli from "./cliManager"
import { Commands } from "./commands"
import { featureSetForVersion, FeatureSet } from "./featureSet"
import { getHeaderCommand } from "./headers"
import { Inbox } from "./inbox"
import { SSHConfig, SSHValues, mergeSSHConfigValues } from "./sshConfig"
import { computeSSHProperties, sshSupportsSetEnv } from "./sshSupport"
import { Storage } from "./storage"
Expand Down Expand Up @@ -403,6 +404,11 @@ export class Remote {
disposables.push(monitor)
disposables.push(monitor.onChange.event((w) => (this.commands.workspace = w)))

// Watch coder inbox for messages
const httpAgent = await createHttpAgent()
const inbox = new Inbox(httpAgent, workspaceRestClient, this.storage)
disposables.push(inbox)

// Wait for the agent to connect.
if (agent.status === "connecting") {
this.storage.writeToCoderOutputChannel(`Waiting for ${workspaceName}/${agent.name}...`)
Expand Down