From 1890fcc4c75d3ffd594dc89fb65844f2502be8f4 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Wed, 10 Dec 2025 16:00:20 +0300 Subject: [PATCH 1/2] Add log file picker when viewing logs without active connection --- CHANGELOG.md | 4 ++++ src/commands.ts | 46 +++++++++++++++++++++++++++++++++++++--- src/remote/sshProcess.ts | 35 ++++++++++++++++-------------- 3 files changed, 66 insertions(+), 19 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index bfbc903a..bb1e5b34 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +### Added + +- Log file picker when viewing logs without an active workspace connection. + ## [v1.11.5](https://github.com/coder/vscode-coder/releases/tag/v1.11.5) 2025-12-10 ### Added diff --git a/src/commands.ts b/src/commands.ts index 9bb2ed54..c04317a1 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -5,6 +5,8 @@ import { type Workspace, type WorkspaceAgent, } from "coder/site/src/api/typesGenerated"; +import * as fs from "node:fs/promises"; +import * as path from "node:path"; import * as vscode from "vscode"; import { createWorkspaceIdentifier, extractAgents } from "./api/api-helper"; @@ -225,7 +227,43 @@ export class Commands { * View the logs for the currently connected workspace. */ public async viewLogs(): Promise { - if (!this.workspaceLogPath) { + if (this.workspaceLogPath) { + // Return the connected deployment's log file. + return this.openFile(this.workspaceLogPath); + } + + const logDir = vscode.workspace + .getConfiguration() + .get("coder.proxyLogDirectory"); + if (logDir) { + try { + const files = await fs.readdir(logDir); + // Sort explicitly since fs.readdir order is platform-dependent + const logFiles = files + .filter((f) => f.endsWith(".log")) + .sort() + .reverse(); + + if (logFiles.length === 0) { + vscode.window.showInformationMessage( + "No log files found in the configured log directory.", + ); + return; + } + + const selected = await vscode.window.showQuickPick(logFiles, { + title: "Select a log file to view", + }); + + if (selected) { + await this.openFile(path.join(logDir, selected)); + } + } catch (error) { + vscode.window.showErrorMessage( + `Failed to read log directory: ${error instanceof Error ? error.message : String(error)}`, + ); + } + } else { vscode.window .showInformationMessage( "No logs available. Make sure to set coder.proxyLogDirectory to get logs.", @@ -239,9 +277,11 @@ export class Commands { ); } }); - return; } - const uri = vscode.Uri.file(this.workspaceLogPath); + } + + private async openFile(path: string): Promise { + const uri = vscode.Uri.file(path); const doc = await vscode.workspace.openTextDocument(uri); await vscode.window.showTextDocument(doc); } diff --git a/src/remote/sshProcess.ts b/src/remote/sshProcess.ts index e86cf154..248e071f 100644 --- a/src/remote/sshProcess.ts +++ b/src/remote/sshProcess.ts @@ -257,7 +257,7 @@ export class SshProcessMonitor implements vscode.Disposable { while (!this.disposed && this.currentPid === targetPid) { try { const logFiles = await fs.readdir(logDir); - logFiles.reverse(); + logFiles.sort().reverse(); const logFileName = logFiles.find( (file) => file === `${targetPid}.log` || file.endsWith(`-${targetPid}.log`), @@ -404,15 +404,11 @@ async function findRemoteSshLogPath( // Try extension-specific folder (for VS Code clones like Cursor, Windsurf) try { const extensionLogDir = path.join(logsParentDir, extensionId); - // Node returns these directories sorted already! - const files = await fs.readdir(extensionLogDir); - files.reverse(); - - const remoteSsh = files.find((file) => file.includes("Remote - SSH")); - if (remoteSsh) { - return path.join(extensionLogDir, remoteSsh); + const remoteSshLog = await findSshLogInDir(extensionLogDir); + if (remoteSshLog) { + return remoteSshLog; } - // Folder exists but no Remote SSH log yet + logger.debug( `Extension log folder exists but no Remote SSH log found: ${extensionLogDir}`, ); @@ -421,18 +417,19 @@ async function findRemoteSshLogPath( } try { - // Node returns these directories sorted already! const dirs = await fs.readdir(logsParentDir); - dirs.reverse(); - const outputDirs = dirs.filter((d) => d.startsWith("output_logging_")); + const outputDirs = dirs + .filter((d) => d.startsWith("output_logging_")) + .sort() + .reverse(); if (outputDirs.length > 0) { const outputPath = path.join(logsParentDir, outputDirs[0]); - const files = await fs.readdir(outputPath); - const remoteSSHLog = files.find((f) => f.includes("Remote - SSH")); - if (remoteSSHLog) { - return path.join(outputPath, remoteSSHLog); + const remoteSshLog = await findSshLogInDir(outputPath); + if (remoteSshLog) { + return remoteSshLog; } + logger.debug( `Output logging folder exists but no Remote SSH log found: ${outputPath}`, ); @@ -445,3 +442,9 @@ async function findRemoteSshLogPath( return undefined; } + +async function findSshLogInDir(dirPath: string): Promise { + const files = await fs.readdir(dirPath); + const remoteSshLog = files.find((f) => f.includes("Remote - SSH")); + return remoteSshLog ? path.join(dirPath, remoteSshLog) : undefined; +} From dc4be1cda4eba05035a17f05eca6b8a2a7637ac5 Mon Sep 17 00:00:00 2001 From: Ehab Younes Date: Mon, 15 Dec 2025 13:05:20 +0300 Subject: [PATCH 2/2] Handle review comments --- src/commands.ts | 7 +-- test/unit/remote/sshProcess.test.ts | 85 ++++++++++++++++++++++++++++- 2 files changed, 87 insertions(+), 5 deletions(-) diff --git a/src/commands.ts b/src/commands.ts index c04317a1..554be055 100644 --- a/src/commands.ts +++ b/src/commands.ts @@ -280,10 +280,9 @@ export class Commands { } } - private async openFile(path: string): Promise { - const uri = vscode.Uri.file(path); - const doc = await vscode.workspace.openTextDocument(uri); - await vscode.window.showTextDocument(doc); + private async openFile(filePath: string): Promise { + const uri = vscode.Uri.file(filePath); + await vscode.window.showTextDocument(uri); } /** diff --git a/test/unit/remote/sshProcess.test.ts b/test/unit/remote/sshProcess.test.ts index 1ec0e048..5e30f533 100644 --- a/test/unit/remote/sshProcess.test.ts +++ b/test/unit/remote/sshProcess.test.ts @@ -1,5 +1,6 @@ import find from "find-process"; import { vol } from "memfs"; +import * as fsPromises from "node:fs/promises"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { @@ -23,7 +24,7 @@ describe("SshProcessMonitor", () => { let statusBar: MockStatusBar; beforeEach(() => { - vi.clearAllMocks(); + vi.restoreAllMocks(); vol.reset(); activeMonitors = []; statusBar = new MockStatusBar(); @@ -101,6 +102,45 @@ describe("SshProcessMonitor", () => { expect(pid).toBe(777); }); + it("uses newest output_logging_ directory when multiple exist", async () => { + // Reverse alphabetical order means highest number/newest first + vol.fromJSON({ + "/logs/output_logging_20240101/1-Remote - SSH.log": + "-> socksPort 11111 ->", + "/logs/output_logging_20240102/1-Remote - SSH.log": + "-> socksPort 22222 ->", + "/logs/output_logging_20240103/1-Remote - SSH.log": + "-> socksPort 33333 ->", + }); + + // Mock readdir to return directories in unsorted order (simulating Windows fs) + mockReaddirOrder("/logs", [ + "output_logging_20240103", + "output_logging_20240101", + "output_logging_20240102", + "window1", + ]); + + const monitor = createMonitor({ codeLogDir: "/logs/window1" }); + await waitForEvent(monitor.onPidChange); + + expect(find).toHaveBeenCalledWith("port", 33333); + }); + + it("falls back to output_logging_ when extension folder has no SSH log", async () => { + // Extension folder exists but doesn't have Remote SSH log + vol.fromJSON({ + "/logs/ms-vscode-remote.remote-ssh/some-other-log.log": "", + "/logs/output_logging_20240101/1-Remote - SSH.log": + "-> socksPort 55555 ->", + }); + + const monitor = createMonitor({ codeLogDir: "/logs/window1" }); + await waitForEvent(monitor.onPidChange); + + expect(find).toHaveBeenCalledWith("port", 55555); + }); + it("reconnects when network info becomes stale", async () => { vol.fromJSON({ "/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log": @@ -236,6 +276,31 @@ describe("SshProcessMonitor", () => { expect(monitor.getLogFilePath()).toBeUndefined(); }); + + it("checks log files in reverse alphabetical order", async () => { + vol.fromJSON({ + "/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log": + "-> socksPort 12345 ->", + "/proxy-logs/2024-01-01-999.log": "", + "/proxy-logs/2024-01-02-999.log": "", + "/proxy-logs/2024-01-03-999.log": "", + }); + + // Mock readdir to return files in unsorted order (simulating Windows fs) + mockReaddirOrder("/proxy-logs", [ + "2024-01-03-999.log", + "2024-01-01-999.log", + "2024-01-02-999.log", + ]); + + const monitor = createMonitor({ + codeLogDir: "/logs/window1", + proxyLogDir: "/proxy-logs", + }); + const logPath = await waitForEvent(monitor.onLogFilePathChange); + + expect(logPath).toBe("/proxy-logs/2024-01-03-999.log"); + }); }); describe("network status", () => { @@ -407,6 +472,24 @@ describe("SshProcessMonitor", () => { } }); +/** + * Helper to mock readdir returning files in a specific unsorted order. + * This is needed because memfs returns files in sorted order, which masks + * bugs in sorting logic. + */ +function mockReaddirOrder(dirPath: string, files: string[]): void { + const originalReaddir = fsPromises.readdir; + const mockImpl = (path: fs.PathLike): Promise => { + if (path === dirPath) { + return Promise.resolve(files); + } + return originalReaddir(path) as Promise; + }; + vi.spyOn(fsPromises, "readdir").mockImplementation( + mockImpl as typeof fsPromises.readdir, + ); +} + /** Wait for a VS Code event to fire once */ function waitForEvent( event: (listener: (e: T) => void) => { dispose(): void },