Skip to content

Commit 0a987b8

Browse files
committed
feat(wsl): improve WSL path handling and output decoding
- Refactored `toWslUncPath` to remove the prefix parameter for consistency. - Added `looksLikeUtf16Le` and `decodeWslOutput` functions to enhance output decoding from WSL commands. - Updated `listWslDistros` to utilize a command array for better command execution handling. - Simplified home path resolution in `handleFindWslClaudeRoots` to avoid duplicates and improve clarity.
1 parent 5c563f4 commit 0a987b8

1 file changed

Lines changed: 123 additions & 62 deletions

File tree

src/main/ipc/config.ts

Lines changed: 123 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -744,9 +744,9 @@ function normalizeWslHomePath(home: string): string | null {
744744
return normalized;
745745
}
746746

747-
function toWslUncPath(prefix: '\\\\wsl.localhost' | '\\\\wsl$', distro: string, posixPath: string): string {
747+
function toWslUncPath(distro: string, posixPath: string): string {
748748
const uncSuffix = posixPath.replace(/\//g, '\\');
749-
return `${prefix}\\${distro}${uncSuffix}`;
749+
return `\\\\wsl.localhost\\${distro}${uncSuffix}`;
750750
}
751751

752752
function getWslExecutableCandidates(): string[] {
@@ -762,16 +762,54 @@ function getWslExecutableCandidates(): string[] {
762762
return Array.from(candidates);
763763
}
764764

765+
function looksLikeUtf16Le(buffer: Buffer): boolean {
766+
const sampleSize = Math.min(buffer.length, 512);
767+
if (sampleSize < 2) {
768+
return false;
769+
}
770+
771+
let pairs = 0;
772+
let nullsAtOddIndex = 0;
773+
for (let i = 0; i + 1 < sampleSize; i += 2) {
774+
pairs += 1;
775+
if (buffer[i + 1] === 0) {
776+
nullsAtOddIndex += 1;
777+
}
778+
}
779+
780+
return pairs > 0 && nullsAtOddIndex / pairs >= 0.3;
781+
}
782+
783+
function decodeWslOutput(output: string | Buffer | undefined): string {
784+
if (typeof output === 'string') {
785+
return output.replace(/\0/g, '');
786+
}
787+
if (!output || output.length === 0) {
788+
return '';
789+
}
790+
791+
const hasUtf16LeBom = output.length >= 2 && output[0] === 0xff && output[1] === 0xfe;
792+
const decoded = hasUtf16LeBom || looksLikeUtf16Le(output)
793+
? output.toString('utf16le')
794+
: output.toString('utf8');
795+
return decoded.replace(/\0/g, '');
796+
}
797+
765798
async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string; stderr: string }> {
766799
const candidates = getWslExecutableCandidates();
767800
let lastError: unknown = null;
768801

769802
for (const executable of candidates) {
770803
try {
771-
const result = await execFileAsync(executable, args, { timeout });
804+
const result = await execFileAsync(executable, args, {
805+
timeout,
806+
windowsHide: true,
807+
maxBuffer: 1024 * 1024,
808+
encoding: 'buffer',
809+
});
772810
return {
773-
stdout: result.stdout ?? '',
774-
stderr: result.stderr ?? '',
811+
stdout: decodeWslOutput(result.stdout),
812+
stderr: decodeWslOutput(result.stderr),
775813
};
776814
} catch (error) {
777815
lastError = error;
@@ -781,24 +819,59 @@ async function runWsl(args: string[], timeout = 5000): Promise<{ stdout: string;
781819
throw lastError instanceof Error ? lastError : new Error('Unable to execute wsl.exe');
782820
}
783821

822+
function parseWslDistros(stdout: string): string[] {
823+
const distros: string[] = [];
824+
const seen = new Set<string>();
825+
const lines = stdout.split(/\r?\n/);
826+
827+
for (const rawLine of lines) {
828+
let line = rawLine.replace(/\0/g, '').trim();
829+
if (!line) {
830+
continue;
831+
}
832+
833+
line = line.replace(/^\*\s*/, '').trim();
834+
line = stripDefaultSuffix(line);
835+
836+
const lower = line.toLowerCase();
837+
if (
838+
lower.startsWith('windows subsystem for linux') ||
839+
lower.includes('default version') ||
840+
lower.startsWith('the following is a list')
841+
) {
842+
continue;
843+
}
844+
845+
const key = line.toLowerCase();
846+
if (!seen.has(key)) {
847+
seen.add(key);
848+
distros.push(line);
849+
}
850+
}
851+
852+
return distros;
853+
}
854+
784855
async function listWslDistros(): Promise<string[]> {
785-
try {
786-
const { stdout } = await runWsl(['-l', '-q'], 4000);
787-
return stdout
788-
.split(/\r?\n/)
789-
.map((line) => line.trim())
790-
.filter((line) => line.length > 0);
791-
} catch {
792-
// Fallback for environments where -q behavior is inconsistent.
793-
const { stdout } = await runWsl(['-l'], 4000);
794-
return stdout
795-
.split(/\r?\n/)
796-
.map((line) => line.trim())
797-
.filter((line) => line.length > 0)
798-
.filter((line) => !line.toLowerCase().startsWith('windows subsystem for linux'))
799-
.filter((line) => !line.toLowerCase().includes('default version'))
800-
.map(stripDefaultSuffix);
856+
const commands: string[][] = [
857+
['--list', '--quiet'],
858+
['-l', '-q'],
859+
['-l'],
860+
];
861+
862+
for (const command of commands) {
863+
try {
864+
const { stdout } = await runWsl(command, 4000);
865+
const parsed = parseWslDistros(stdout);
866+
if (parsed.length > 0) {
867+
return parsed;
868+
}
869+
} catch {
870+
// Try the next command variant.
871+
}
801872
}
873+
874+
return [];
802875
}
803876

804877
function stripDefaultSuffix(input: string): string {
@@ -841,50 +914,38 @@ async function handleFindWslClaudeRoots(
841914
const candidates: WslClaudeRootCandidate[] = [];
842915
const seen = new Set<string>();
843916
for (const distro of distros) {
844-
const homePath = await resolveWslHome(distro);
845-
const homeCandidates = new Set<string>();
846-
if (homePath) {
847-
homeCandidates.add(homePath);
848-
}
849-
if (process.env.USERNAME) {
850-
homeCandidates.add(`/home/${process.env.USERNAME}`);
917+
const resolvedHomePath = await resolveWslHome(distro);
918+
const fallbackHomePath = process.env.USERNAME ? `/home/${process.env.USERNAME}` : null;
919+
const normalizedHome =
920+
normalizeWslHomePath(resolvedHomePath ?? '') ??
921+
(fallbackHomePath ? normalizeWslHomePath(fallbackHomePath) : null);
922+
923+
if (!normalizedHome) {
924+
continue;
851925
}
852-
homeCandidates.add('/root');
853926

854-
for (const homeCandidate of homeCandidates) {
855-
const normalizedHome = normalizeWslHomePath(homeCandidate);
856-
if (!normalizedHome) {
857-
continue;
858-
}
859-
const claudePosixPath = path.posix.join(normalizedHome, '.claude');
860-
861-
const uncPaths = [
862-
toWslUncPath('\\\\wsl.localhost', distro, claudePosixPath),
863-
toWslUncPath('\\\\wsl$', distro, claudePosixPath),
864-
];
865-
866-
for (const claudeUncPath of uncPaths) {
867-
if (seen.has(claudeUncPath.toLowerCase())) {
868-
continue;
869-
}
870-
seen.add(claudeUncPath.toLowerCase());
871-
872-
const projectsPath = path.join(claudeUncPath, 'projects');
873-
const hasProjectsDir = (() => {
874-
try {
875-
return fs.existsSync(projectsPath) && fs.statSync(projectsPath).isDirectory();
876-
} catch {
877-
return false;
878-
}
879-
})();
880-
881-
candidates.push({
882-
distro,
883-
path: claudeUncPath,
884-
hasProjectsDir,
885-
});
886-
}
927+
const claudePosixPath = path.posix.join(normalizedHome, '.claude');
928+
const claudeUncPath = toWslUncPath(distro, claudePosixPath);
929+
const key = claudeUncPath.toLowerCase();
930+
if (seen.has(key)) {
931+
continue;
887932
}
933+
seen.add(key);
934+
935+
const projectsPath = path.join(claudeUncPath, 'projects');
936+
const hasProjectsDir = (() => {
937+
try {
938+
return fs.existsSync(projectsPath) && fs.statSync(projectsPath).isDirectory();
939+
} catch {
940+
return false;
941+
}
942+
})();
943+
944+
candidates.push({
945+
distro,
946+
path: claudeUncPath,
947+
hasProjectsDir,
948+
});
888949
}
889950

890951
return { success: true, data: candidates };

0 commit comments

Comments
 (0)