From 508dccc0b9e48c40c098a9fb0b4acb1ab11f114f Mon Sep 17 00:00:00 2001 From: Andrea Bergamasco Date: Fri, 11 Sep 2026 22:24:29 +0200 Subject: [PATCH] fix(local): resolve guest cwd against the shell's fsBase before validating (#11625) Co-authored-by: Andrea Bergamasco Co-authored-by: Eugene --- tabby-electron/src/shells/wsl.ts | 14 ++++++++++--- tabby-local/src/api.ts | 6 ++++++ tabby-local/src/profiles.ts | 2 ++ tabby-local/src/services/terminal.service.ts | 3 ++- tabby-local/src/session.ts | 3 ++- tabby-local/src/wslPath.ts | 22 ++++++++++++++++++++ 6 files changed, 45 insertions(+), 5 deletions(-) create mode 100644 tabby-local/src/wslPath.ts diff --git a/tabby-electron/src/shells/wsl.ts b/tabby-electron/src/shells/wsl.ts index fd3271009..e97b7f52f 100644 --- a/tabby-electron/src/shells/wsl.ts +++ b/tabby-electron/src/shells/wsl.ts @@ -49,6 +49,13 @@ export class WSLShellProvider extends ShellProvider { return [] } + // Windows path backing the distro's rootfs, so a POSIX cwd from the guest can be resolved. + // Flags bit 3 marks a WSL2 distro (files served over \\wsl$); WSL1 lives under BasePath. + const fsBaseForDistro = (key: any): string | undefined => + (key.Flags?.value || 0) & 8 + ? `\\\\wsl$\\${key.DistributionName.value}` + : key.BasePath ? key.BasePath.value + '\\rootfs' : undefined + const bashPath = `${process.env.windir}\\system32\\bash.exe` const wslPath = `${process.env.windir}\\system32\\wsl.exe` @@ -59,6 +66,7 @@ export class WSLShellProvider extends ShellProvider { if (lxss?.DefaultDistribution) { const defaultDistKey = wnr.getRegistryKey(wnr.HK.CU, lxssPath + '\\' + String(lxss.DefaultDistribution.value)) if (defaultDistKey?.DistributionName) { + const name = defaultDistKey.DistributionName.value const shell: Shell = { id: 'wsl', name: 'WSL / Default distro', @@ -68,8 +76,9 @@ export class WSLShellProvider extends ShellProvider { COLORTERM: 'truecolor', }, shellType: 'unix', + fsBase: fsBaseForDistro(defaultDistKey), // eslint-disable-next-line @typescript-eslint/no-unnecessary-condition - icon: wslIconMap[defaultDistKey.DistributionName.value] ?? wslIconMap.Linux, + icon: wslIconMap[name] ?? wslIconMap.Linux, } shells.push(shell) } @@ -97,9 +106,8 @@ export class WSLShellProvider extends ShellProvider { if (!childKey.DistributionName || !childKey.BasePath) { continue } - const wslVersion = (childKey.Flags?.value || 0) & 8 ? 2 : 1 const name = childKey.DistributionName.value - const fsBase = wslVersion === 2 ? `\\\\wsl$\\${name}` : childKey.BasePath.value as string + '\\rootfs' + const fsBase = fsBaseForDistro(childKey) const slug = slugify(name, { remove: /[:.]/g }) const shell: Shell = { id: `wsl-${slug}`, diff --git a/tabby-local/src/api.ts b/tabby-local/src/api.ts index c23ded766..3144dd7bb 100644 --- a/tabby-local/src/api.ts +++ b/tabby-local/src/api.ts @@ -46,6 +46,12 @@ export interface SessionOptions { shellType: ShellType | null pauseAfterExit: boolean runAsAdministrator: boolean + + /** + * Base path to which cwd is relative, e.g. `\\wsl$\Ubuntu` for WSL shells. + * Used to translate a POSIX cwd reported by the guest shell into a real Windows path. + */ + fsBase: string | null } export interface LocalProfile extends BaseTerminalProfile { diff --git a/tabby-local/src/profiles.ts b/tabby-local/src/profiles.ts index 6599f0ea3..9afaf2226 100644 --- a/tabby-local/src/profiles.ts +++ b/tabby-local/src/profiles.ts @@ -25,6 +25,7 @@ export class LocalProfilesService extends ProfileProvider { shellType: null, pauseAfterExit: false, runAsAdministrator: false, + fsBase: null, }, } @@ -84,6 +85,7 @@ export class LocalProfilesService extends ProfileProvider { env: shell.env, cwd: shell.cwd ?? null, shellType: shell.shellType ?? null, + fsBase: shell.fsBase ?? null, } } diff --git a/tabby-local/src/services/terminal.service.ts b/tabby-local/src/services/terminal.service.ts index 0b8f9603b..3bf6efa39 100644 --- a/tabby-local/src/services/terminal.service.ts +++ b/tabby-local/src/services/terminal.service.ts @@ -2,6 +2,7 @@ import { Injectable } from '@angular/core' import { Logger, LogService, ConfigService, ProfilesService, PartialProfile } from 'tabby-core' import { TerminalTabComponent } from '../components/terminalTab.component' import { LocalProfile } from '../api' +import { resolveGuestCWD } from '../wslPath' import { isDirectorySync } from '../util' @Injectable({ providedIn: 'root' }) @@ -37,7 +38,7 @@ export class TerminalService { const fullProfile = this.profilesService.getConfigProxyForProfile(profile) - cwd = cwd ?? fullProfile.options.cwd + cwd = resolveGuestCWD(cwd ?? fullProfile.options.cwd, fullProfile.options.fsBase) if (cwd && !isDirectorySync(cwd)) { console.warn('Ignoring invalid CWD:', cwd) diff --git a/tabby-local/src/session.ts b/tabby-local/src/session.ts index 46d02cf6a..881f697e0 100644 --- a/tabby-local/src/session.ts +++ b/tabby-local/src/session.ts @@ -4,6 +4,7 @@ import { HostAppService, ConfigService, WIN_BUILD_CONPTY_SUPPORTED, isWindowsBui import { BaseSession } from 'tabby-terminal' import { SessionOptions, ChildProcess, PTYInterface, PTYProxy } from './api' import { getEnvironment, substituteEnv } from './environment' +import { resolveGuestCWD } from './wslPath' import { isDirectory, isDirectorySync } from './util' const windowsDirectoryRegex = /([a-zA-Z]:[^\:\[\]\?\"\<\>\|]+)/mi @@ -87,7 +88,7 @@ export class Session extends BaseSession { } // eslint-disable-next-line @typescript-eslint/prefer-nullish-coalescing - let cwd = options.cwd || process.env.HOME + let cwd = resolveGuestCWD(options.cwd, options.fsBase) || process.env.HOME if (!isDirectorySync(cwd)) { console.warn('Ignoring invalid CWD:', cwd) diff --git a/tabby-local/src/wslPath.ts b/tabby-local/src/wslPath.ts new file mode 100644 index 000000000..ededebc20 --- /dev/null +++ b/tabby-local/src/wslPath.ts @@ -0,0 +1,22 @@ +import { strictEqual } from 'assert' + +/** Rewrite a POSIX guest cwd (e.g. WSL) to a Windows path via fsBase; unchanged otherwise. */ +export function resolveGuestCWD (cwd: string | null | undefined, fsBase: string | null | undefined): string | null | undefined { + if (cwd?.startsWith('/') && fsBase) { + return fsBase + cwd.replace(/\//g, '\\') + } + return cwd +} + +function selfCheck (): void { + strictEqual(resolveGuestCWD('/home/x', '\\\\wsl$\\Ubuntu'), '\\\\wsl$\\Ubuntu\\home\\x') + strictEqual(resolveGuestCWD('C:\\Users\\x', '\\\\wsl$\\Ubuntu'), 'C:\\Users\\x') + strictEqual(resolveGuestCWD('/home/x', null), '/home/x') + strictEqual(resolveGuestCWD(null, '\\\\wsl$\\Ubuntu'), null) + strictEqual(resolveGuestCWD(undefined, null), undefined) +} + +// run: npx ts-node -O '{"module":"commonjs"}' tabby-local/src/wslPath.ts +if (String(process.argv[1]).includes('wslPath')) { + selfCheck() +}