Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
80 changes: 60 additions & 20 deletions package.json
Original file line numberDiff line numberDiff line change
Expand Up@@ -192,67 +192,67 @@
"commands": [
{
"command": "coder.login",
"title": "Coder: Login"
"title": "Login",
"category": "Coder",
"icon": "$(sign-in)"
},
{
"command": "coder.logout",
"title": "Coder: Logout",
"when": "coder.authenticated",
"title": "Logout",
"category": "Coder",
"icon": "$(sign-out)"
},
{
"command": "coder.open",
"title": "Open Workspace",
"icon": "$(play)",
"category": "Coder"
"category": "Coder",
"icon": "$(play)"
},
{
"command": "coder.openFromSidebar",
"title": "Coder: Open Workspace",
"title": "Open Workspace",
"category": "Coder",
"icon": "$(play)"
},
{
"command": "coder.createWorkspace",
"title": "Create Workspace",
"category": "Coder",
"when": "coder.authenticated",
"icon": "$(add)"
},
{
"command": "coder.navigateToWorkspace",
"title": "Navigate to Workspace Page",
"when": "coder.authenticated",
"category": "Coder",
"icon": "$(link-external)"
},
{
"command": "coder.navigateToWorkspaceSettings",
"title": "Edit Workspace Settings",
"when": "coder.authenticated",
"category": "Coder",
"icon": "$(settings-gear)"
},
{
"command": "coder.workspace.update",
"title": "Coder: Update Workspace",
"when": "coder.workspace.updatable"
"title": "Update Workspace",
"category": "Coder"
},
{
"command": "coder.refreshWorkspaces",
"title": "Refresh Workspace",
"category": "Coder",
"icon": "$(refresh)",
"when": "coder.authenticated"
"icon": "$(refresh)"
},
{
"command": "coder.viewLogs",
"title": "Coder: View Logs",
"icon": "$(list-unordered)",
"when": "coder.authenticated"
"icon": "$(list-unordered)"
},
{
"command": "coder.openAppStatus",
"title": "Coder: Open App Status",
"icon": "$(robot)",
"when": "coder.authenticated"
"title": "Open App Status",
"category": "Coder",
"icon": "$(robot)"
},
{
"command": "coder.searchMyWorkspaces",
Expand All@@ -275,8 +275,44 @@
"menus":{
"commandPalette": [
{
"command": "coder.debug.listDeployments",
"when": "coder.devMode"
"command": "coder.login",
"when": "!coder.authenticated"
},
{
"command": "coder.logout",
"when": "coder.authenticated"
},
{
"command": "coder.createWorkspace",
"when": "coder.authenticated"
},
{
"command": "coder.navigateToWorkspace",
"when": "coder.workspace.connected"
},
{
"command": "coder.navigateToWorkspaceSettings",
"when": "coder.workspace.connected"
},
{
"command": "coder.workspace.update",
"when": "coder.workspace.updatable"
},
{
"command": "coder.refreshWorkspaces",
"when": "coder.authenticated"
},
{
"command": "coder.viewLogs",
"when": "true"
},
{
"command": "coder.openAppStatus",
"when": "false"
},
{
"command": "coder.open",
"when": "coder.authenticated"
},
{
"command": "coder.openFromSidebar",
Expand All@@ -289,6 +325,10 @@
{
"command": "coder.searchAllWorkspaces",
"when": "false"
},
{
"command": "coder.debug.listDeployments",
"when": "coder.devMode"
}
],
"view/title": [
Expand Down
2 changes: 1 addition & 1 deletion src/api/utils.ts
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
import fs from "fs/promises"
import fs from "node:fs/promises"
import{ProxyAgent } from "proxy-agent"
import{type WorkspaceConfiguration } from "vscode"

Expand Down
4 changes: 2 additions & 2 deletions src/commands.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -242,7 +242,7 @@ export class Commands{
*
* Otherwise, the currently connected workspace is used (if any).
*/
public async navigateToWorkspace(item: OpenableTreeItem){
public async navigateToWorkspace(item?: OpenableTreeItem){
if (item){
const baseUrl = this.requireExtensionBaseUrl();
const workspaceId = createWorkspaceIdentifier(item.workspace);
Expand All@@ -266,7 +266,7 @@ export class Commands{
*
* Otherwise, the currently connected workspace is used (if any).
*/
public async navigateToWorkspaceSettings(item: OpenableTreeItem){
public async navigateToWorkspaceSettings(item?: OpenableTreeItem){
if (item){
const baseUrl = this.requireExtensionBaseUrl();
const workspaceId = createWorkspaceIdentifier(item.workspace);
Expand Down
1 change: 1 addition & 0 deletions src/core/contextManager.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -4,6 +4,7 @@ const CONTEXT_DEFAULTS ={
"coder.authenticated": false,
"coder.isOwner": false,
"coder.loaded": false,
"coder.workspace.connected": false,
"coder.workspace.updatable": false,
} as const;

Expand Down
1 change: 1 addition & 0 deletions src/remote/remote.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -556,6 +556,7 @@ export class Remote{
throw ex;
}

this.contextManager.set("coder.workspace.connected", true);
this.logger.info("Remote setup complete");

// Returning the URL and token allows the plugin to authenticate its own
Expand Down
7 changes: 4 additions & 3 deletions src/remote/sshProcess.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -256,8 +256,9 @@ export class SshProcessMonitor implements vscode.Disposable{
const targetPid = this.currentPid;
while (!this.disposed && this.currentPid === targetPid){
try{
const logFiles = await fs.readdir(logDir);
logFiles.sort().reverse();
const logFiles = (await fs.readdir(logDir))
.sort((a, b) => a.localeCompare(b))
.reverse();
const logFileName = logFiles.find(
(file) =>
file === `${targetPid}.log` || file.endsWith(`-${targetPid}.log`),
Expand DownExpand Up@@ -420,7 +421,7 @@ async function findRemoteSshLogPath(
const dirs = await fs.readdir(logsParentDir);
const outputDirs = dirs
.filter((d) => d.startsWith("output_logging_"))
.sort()
.sort((a, b) => a.localeCompare(b))
.reverse();

if (outputDirs.length > 0){
Expand Down
45 changes: 44 additions & 1 deletion test/unit/remote/sshProcess.test.ts
Original file line numberDiff line numberDiff line change
Expand Up@@ -127,6 +127,27 @@ describe("SshProcessMonitor", () =>{
expect(find).toHaveBeenCalledWith("port", 33333);
});

it("sorts output_logging_ directories using localeCompare for consistent ordering", async () =>{
// localeCompare differs from default sort() for mixed case
vol.fromJSON({
"/logs/output_logging_a/1-Remote - SSH.log": "-> socksPort 11111 ->",
"/logs/output_logging_Z/1-Remote - SSH.log": "-> socksPort 22222 ->",
});

mockReaddirOrder("/logs", [
"output_logging_a",
"output_logging_Z",
"window1",
]);

const monitor = createMonitor({codeLogDir: "/logs/window1"});
await waitForEvent(monitor.onPidChange);

// With localeCompare: ["a", "Z"] -> reversed -> "Z" first (port 22222)
// With plain sort(): ["Z", "a"] -> reversed -> "a" first (port 11111)
expect(find).toHaveBeenCalledWith("port", 22222);
});

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({
Expand DownExpand Up@@ -301,6 +322,28 @@ describe("SshProcessMonitor", () =>{

expect(logPath).toBe("/proxy-logs/2024-01-03-999.log");
});

it("sorts log files using localeCompare for consistent cross-platform ordering", async () =>{
// localeCompare differs from default sort() for mixed case
vol.fromJSON({
"/logs/ms-vscode-remote.remote-ssh/1-Remote - SSH.log":
"-> socksPort 12345 ->",
"/proxy-logs/a-999.log": "",
"/proxy-logs/Z-999.log": "",
});

mockReaddirOrder("/proxy-logs", ["a-999.log", "Z-999.log"]);

const monitor = createMonitor({
codeLogDir: "/logs/window1",
proxyLogDir: "/proxy-logs",
});
const logPath = await waitForEvent(monitor.onLogFilePathChange);

// With localeCompare: ["a", "Z"] -> reversed -> "Z" first
// With plain sort(): ["Z", "a"] -> reversed -> "a" first (WRONG)
expect(logPath).toBe("/proxy-logs/Z-999.log");
});
});

describe("network status", () =>{
Expand DownExpand Up@@ -483,7 +526,7 @@ function mockReaddirOrder(dirPath: string, files: string[]): void{
if (path === dirPath){
return Promise.resolve(files);
}
return originalReaddir(path) as Promise<string[]>;
return originalReaddir(path);
};
vi.spyOn(fsPromises, "readdir").mockImplementation(
mockImpl as typeof fsPromises.readdir,
Expand Down