From ba1717af5b31fa58b3393024a43da2c1072e7718 Mon Sep 17 00:00:00 2001
From: Kyle Carberry <kyle@carberry.com>
Date: Sat, 4 Nov 2023 13:59:24 -0500
Subject: [PATCH 1/2] Add log file

---
 package.json  |  2 +-
 src/remote.ts | 32 ++++++++++++++++----------------
 2 files changed, 17 insertions(+), 17 deletions(-)

diff --git a/package.json b/package.json
index 48f9dbd8..16e5e5db 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
 {
   "name": "coder-remote",
   "publisher": "coder",
-  "displayName": "Coder Remote",
+  "displayName": "Coder",
   "description": "Open any workspace with a single click.",
   "repository": "https://github.com/coder/vscode-coder",
   "version": "0.1.25",
diff --git a/src/remote.ts b/src/remote.ts
index 3e072dd9..8b52773c 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -76,6 +76,9 @@ export class Remote {
       await this.closeRemote()
       return
     }
+    const hasCoderLogs = (parsedVersion?.compare("2.3.3") || 0) >= 0 || parsedVersion?.prerelease[0] === "devel"
+
+    console.log("hasCoderLogs", hasCoderLogs)
 
     // Find the workspace from the URI scheme provided!
     try {
@@ -484,24 +487,21 @@ export class Remote {
     // Now override with the user's config.
     const userConfigSSH = vscode.workspace.getConfiguration("coder").get<string[]>("sshConfig") || []
     // Parse the user's config into a Record<string, string>.
-    const userConfig = userConfigSSH.reduce(
-      (acc, line) => {
-        let i = line.indexOf("=")
+    const userConfig = userConfigSSH.reduce((acc, line) => {
+      let i = line.indexOf("=")
+      if (i === -1) {
+        i = line.indexOf(" ")
         if (i === -1) {
-          i = line.indexOf(" ")
-          if (i === -1) {
-            // This line is malformed. The setting is incorrect, and does not match
-            // the pattern regex in the settings schema.
-            return acc
-          }
+          // This line is malformed. The setting is incorrect, and does not match
+          // the pattern regex in the settings schema.
+          return acc
         }
-        const key = line.slice(0, i)
-        const value = line.slice(i + 1)
-        acc[key] = value
-        return acc
-      },
-      {} as Record<string, string>,
-    )
+      }
+      const key = line.slice(0, i)
+      const value = line.slice(i + 1)
+      acc[key] = value
+      return acc
+    }, {} as Record<string, string>)
     const sshConfigOverrides = mergeSSHConfigValues(deploymentSSHConfig, userConfig)
 
     let sshConfigFile = vscode.workspace.getConfiguration().get<string>("remote.SSH.configFile")

From 5ad161b7a764d0d25619e7e6f5a4f81e750a72d3 Mon Sep 17 00:00:00 2001
From: Kyle Carberry <kyle@carberry.com>
Date: Mon, 13 Nov 2023 12:42:52 -0600
Subject: [PATCH 2/2] feat: add "Show Logs" command to help with network
 debugging

---
 package.json     |  6 ++++++
 src/commands.ts  | 11 +++++++++++
 src/extension.ts |  1 +
 src/remote.ts    | 47 ++++++++++++++++++++++++++++-------------------
 src/storage.ts   |  7 +++++++
 5 files changed, 53 insertions(+), 19 deletions(-)

diff --git a/package.json b/package.json
index 16e5e5db..8ffdb57c 100644
--- a/package.json
+++ b/package.json
@@ -168,6 +168,12 @@
         "title": "Coder: Refresh Workspace",
         "icon": "$(refresh)",
         "when": "coder.authenticated"
+      },
+      {
+        "command": "coder.viewLogs",
+        "title": "Coder: View Logs",
+        "icon": "$(list-unordered)",
+        "when": "coder.authenticated"
       }
     ],
     "menus": {
diff --git a/src/commands.ts b/src/commands.ts
index 774768c5..c099a34f 100644
--- a/src/commands.ts
+++ b/src/commands.ts
@@ -115,6 +115,17 @@ export class Commands {
     }
   }
 
+  // viewLogs opens the workspace logs.
+  public async viewLogs(): Promise<void> {
+    if (!this.storage.workspaceLogPath) {
+      vscode.window.showInformationMessage("No logs available.", this.storage.workspaceLogPath || "<unset>")
+      return
+    }
+    const uri = vscode.Uri.file(this.storage.workspaceLogPath)
+    const doc = await vscode.workspace.openTextDocument(uri)
+    await vscode.window.showTextDocument(doc)
+  }
+
   public async logout(): Promise<void> {
     await this.storage.setURL(undefined)
     await this.storage.setSessionToken(undefined)
diff --git a/src/extension.ts b/src/extension.ts
index 34188082..1bb82b84 100644
--- a/src/extension.ts
+++ b/src/extension.ts
@@ -157,6 +157,7 @@ export async function activate(ctx: vscode.ExtensionContext): Promise<void> {
     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
diff --git a/src/remote.ts b/src/remote.ts
index 8b52773c..fb0a4e7b 100644
--- a/src/remote.ts
+++ b/src/remote.ts
@@ -76,10 +76,11 @@ export class Remote {
       await this.closeRemote()
       return
     }
+    // CLI versions before 2.3.3 don't support the --log-dir flag!
+    // If this check didn't exist, VS Code connections would fail on
+    // older versions because of an unknown CLI argument.
     const hasCoderLogs = (parsedVersion?.compare("2.3.3") || 0) >= 0 || parsedVersion?.prerelease[0] === "devel"
 
-    console.log("hasCoderLogs", hasCoderLogs)
-
     // Find the workspace from the URI scheme provided!
     try {
       this.storage.workspace = await getWorkspaceByOwnerAndName(parts[0], parts[1])
@@ -429,7 +430,7 @@ export class Remote {
     //
     // If we didn't write to the SSH config file, connecting would fail with
     // "Host not found".
-    await this.updateSSHConfig(authorityParts[1])
+    await this.updateSSHConfig(authorityParts[1], hasCoderLogs)
 
     this.findSSHProcessID().then((pid) => {
       if (!pid) {
@@ -437,6 +438,7 @@ export class Remote {
         return
       }
       disposables.push(this.showNetworkUpdates(pid))
+      this.storage.workspaceLogPath = path.join(this.storage.getLogPath(), `${pid}.log`)
     })
 
     // Register the label formatter again because SSH overrides it!
@@ -459,7 +461,7 @@ export class Remote {
 
   // updateSSHConfig updates the SSH configuration with a wildcard that handles
   // all Coder entries.
-  private async updateSSHConfig(hostName: string) {
+  private async updateSSHConfig(hostName: string, hasCoderLogs = false) {
     let deploymentSSHConfig = defaultSSHConfigResponse
     try {
       const deploymentConfig = await getDeploymentSSHConfig()
@@ -487,21 +489,24 @@ export class Remote {
     // Now override with the user's config.
     const userConfigSSH = vscode.workspace.getConfiguration("coder").get<string[]>("sshConfig") || []
     // Parse the user's config into a Record<string, string>.
-    const userConfig = userConfigSSH.reduce((acc, line) => {
-      let i = line.indexOf("=")
-      if (i === -1) {
-        i = line.indexOf(" ")
+    const userConfig = userConfigSSH.reduce(
+      (acc, line) => {
+        let i = line.indexOf("=")
         if (i === -1) {
-          // This line is malformed. The setting is incorrect, and does not match
-          // the pattern regex in the settings schema.
-          return acc
+          i = line.indexOf(" ")
+          if (i === -1) {
+            // This line is malformed. The setting is incorrect, and does not match
+            // the pattern regex in the settings schema.
+            return acc
+          }
         }
-      }
-      const key = line.slice(0, i)
-      const value = line.slice(i + 1)
-      acc[key] = value
-      return acc
-    }, {} as Record<string, string>)
+        const key = line.slice(0, i)
+        const value = line.slice(i + 1)
+        acc[key] = value
+        return acc
+      },
+      {} as Record<string, string>,
+    )
     const sshConfigOverrides = mergeSSHConfigValues(deploymentSSHConfig, userConfig)
 
     let sshConfigFile = vscode.workspace.getConfiguration().get<string>("remote.SSH.configFile")
@@ -542,12 +547,16 @@ export class Remote {
     if (typeof headerCommand === "string" && headerCommand.trim().length > 0) {
       headerArg = ` --header-command ${escape(headerCommand)}`
     }
-
+    let logArg = ""
+    if (hasCoderLogs) {
+      await fs.mkdir(this.storage.getLogPath(), { recursive: true })
+      logArg = ` --log-dir ${escape(this.storage.getLogPath())}`
+    }
     const sshValues: SSHValues = {
       Host: `${Remote.Prefix}*`,
       ProxyCommand: `${escape(binaryPath)}${headerArg} vscodessh --network-info-dir ${escape(
         this.storage.getNetworkInfoPath(),
-      )} --session-token-file ${escape(this.storage.getSessionTokenPath())} --url-file ${escape(
+      )}${logArg} --session-token-file ${escape(this.storage.getSessionTokenPath())} --url-file ${escape(
         this.storage.getURLPath(),
       )} %h`,
       ConnectTimeout: "0",
diff --git a/src/storage.ts b/src/storage.ts
index 8506464f..fa173cdd 100644
--- a/src/storage.ts
+++ b/src/storage.ts
@@ -15,6 +15,7 @@ import { getHeaderCommand, getHeaders } from "./headers"
 
 export class Storage {
   public workspace?: Workspace
+  public workspaceLogPath?: string
 
   constructor(
     private readonly output: vscode.OutputChannel,
@@ -279,6 +280,12 @@ export class Storage {
     return path.join(this.globalStorageUri.fsPath, "net")
   }
 
+  // getLogPath returns the path where log data from the Coder
+  // agent is stored.
+  public getLogPath(): string {
+    return path.join(this.globalStorageUri.fsPath, "log")
+  }
+
   public getUserSettingsPath(): string {
     return path.join(this.globalStorageUri.fsPath, "..", "..", "..", "User", "settings.json")
   }