mirror of
https://github.com/binaricat/Netcatty.git
synced 2026-10-09 15:58:34 +00:00
Validate Mosh client is executable in Settings UI
Address Codex P2 on PR #849 commit b6c384af. UI's debounced validator
called validatePath which only reported exists / isFile / isDirectory,
so a regular file without the POSIX execute bit (e.g. a stray
/etc/hosts-style path) was marked as valid in Settings — but
startMoshSession's isExecutableFile check then rejected the same path
at connect time, deferring the error until the user actually tried to
use Mosh.
- validatePath now returns `isExecutable: boolean`, mirroring
isExecutableFile semantics (POSIX: stat.mode & 0o111; Win32: any
regular file is treated as executable since NTFS lacks POSIX bits).
Existing callers (localShell, localStartDir) ignore the new field.
- global.d.ts ValidatePath return type extended.
- SettingsTerminalTab Mosh validator surfaces a `notExecutable`
message when the file exists but lacks exec permissions, keeping
the UI in lockstep with main-process gating.
- en / zh-CN strings for the new state.
Verified: /bin/sh -> isExecutable:true, /etc/hosts -> false, /etc ->
false (directory). UI now warns immediately on the regression case.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -383,6 +383,7 @@ const en: Messages = {
|
||||
'settings.terminal.mosh.client.placeholder': 'Auto-detect',
|
||||
'settings.terminal.mosh.client.notFound': 'File not found at that path.',
|
||||
'settings.terminal.mosh.client.isDirectory': 'Path points to a directory, not an executable.',
|
||||
'settings.terminal.mosh.client.notExecutable': 'File exists but is not executable. Run `chmod +x` on it or pick another binary.',
|
||||
'settings.terminal.mosh.detect': 'Detect',
|
||||
'settings.terminal.mosh.browse': 'Browse…',
|
||||
'settings.terminal.mosh.detected': 'Detected at',
|
||||
|
||||
@@ -1467,6 +1467,7 @@ const zhCN: Messages = {
|
||||
'settings.terminal.mosh.client.placeholder': '自动探测',
|
||||
'settings.terminal.mosh.client.notFound': '该路径下未找到文件。',
|
||||
'settings.terminal.mosh.client.isDirectory': '该路径指向目录而非可执行文件。',
|
||||
'settings.terminal.mosh.client.notExecutable': '文件存在但不可执行。请对其执行 `chmod +x`,或选择其它二进制文件。',
|
||||
'settings.terminal.mosh.detect': '探测',
|
||||
'settings.terminal.mosh.browse': '浏览…',
|
||||
'settings.terminal.mosh.detected': '已找到',
|
||||
|
||||
@@ -478,7 +478,11 @@ export default function SettingsTerminalTab(props: {
|
||||
}
|
||||
const timeoutId = setTimeout(() => {
|
||||
bridge.validatePath(moshPath, "file").then((result) => {
|
||||
if (result.exists && result.isFile) {
|
||||
if (result.exists && result.isFile && !result.isExecutable) {
|
||||
// Stays consistent with startMoshSession's isExecutableFile check —
|
||||
// a regular file without the execute bit can't actually launch.
|
||||
setMoshValidation({ valid: false, message: t("settings.terminal.mosh.client.notExecutable") });
|
||||
} else if (result.exists && result.isFile) {
|
||||
setMoshValidation({ valid: true });
|
||||
} else if (result.exists && result.isDirectory) {
|
||||
setMoshValidation({ valid: false, message: t("settings.terminal.mosh.client.isDirectory") });
|
||||
|
||||
@@ -1287,29 +1287,42 @@ function getDefaultShell() {
|
||||
* Validate a path - check if it exists and whether it's a file or directory
|
||||
* @param {object} event - IPC event
|
||||
* @param {object} payload - Contains { path: string, type?: 'file' | 'directory' | 'any' }
|
||||
* @returns {{ exists: boolean, isFile: boolean, isDirectory: boolean }}
|
||||
* @returns {{ exists: boolean, isFile: boolean, isDirectory: boolean, isExecutable: boolean }}
|
||||
*
|
||||
* `isExecutable` mirrors isExecutableFile(): POSIX requires the file mode
|
||||
* to have an execute bit; Win32 treats any regular file as executable
|
||||
* (NTFS lacks POSIX bits — extension/PATHEXT decides at spawn time).
|
||||
* Existing callers ignore the new field; consumers that need exec
|
||||
* semantics (e.g. Mosh client path) read it explicitly.
|
||||
*/
|
||||
function statIsExecutable(stat) {
|
||||
if (!stat || !stat.isFile()) return false;
|
||||
if (process.platform === "win32") return true;
|
||||
return (stat.mode & 0o111) !== 0;
|
||||
}
|
||||
|
||||
function validatePath(event, payload) {
|
||||
const targetPath = payload?.path;
|
||||
const type = payload?.type || 'any';
|
||||
if (!targetPath) {
|
||||
return { exists: false, isFile: false, isDirectory: false };
|
||||
return { exists: false, isFile: false, isDirectory: false, isExecutable: false };
|
||||
}
|
||||
|
||||
|
||||
try {
|
||||
// Resolve path (handle ~, etc.)
|
||||
let resolvedPath = expandHomePath(targetPath);
|
||||
resolvedPath = path.resolve(resolvedPath);
|
||||
|
||||
|
||||
if (fs.existsSync(resolvedPath)) {
|
||||
const stat = fs.statSync(resolvedPath);
|
||||
return {
|
||||
exists: true,
|
||||
isFile: stat.isFile(),
|
||||
isDirectory: stat.isDirectory(),
|
||||
isExecutable: statIsExecutable(stat),
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// If type is 'file' and path doesn't exist, try to resolve via PATH (for executables like cmd.exe, powershell.exe)
|
||||
if (type === 'file') {
|
||||
const resolvedExecutable = findExecutable(targetPath);
|
||||
@@ -1320,6 +1333,7 @@ function validatePath(event, payload) {
|
||||
exists: true,
|
||||
isFile: stat.isFile(),
|
||||
isDirectory: stat.isDirectory(),
|
||||
isExecutable: statIsExecutable(stat),
|
||||
};
|
||||
}
|
||||
// Also try with .exe extension on Windows if not already present
|
||||
@@ -1331,15 +1345,16 @@ function validatePath(event, payload) {
|
||||
exists: true,
|
||||
isFile: stat.isFile(),
|
||||
isDirectory: stat.isDirectory(),
|
||||
isExecutable: statIsExecutable(stat),
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { exists: false, isFile: false, isDirectory: false };
|
||||
|
||||
return { exists: false, isFile: false, isDirectory: false, isExecutable: false };
|
||||
} catch (err) {
|
||||
console.warn(`[Terminal] Error validating path "${targetPath}":`, err.message);
|
||||
return { exists: false, isFile: false, isDirectory: false };
|
||||
return { exists: false, isFile: false, isDirectory: false, isExecutable: false };
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Vendored
+1
-1
@@ -216,7 +216,7 @@ declare global {
|
||||
}>>;
|
||||
getDefaultShell?(): Promise<string>;
|
||||
discoverShells?(): Promise<DiscoveredShell[]>;
|
||||
validatePath?(path: string, type?: 'file' | 'directory' | 'any'): Promise<{ exists: boolean; isFile: boolean; isDirectory: boolean }>;
|
||||
validatePath?(path: string, type?: 'file' | 'directory' | 'any'): Promise<{ exists: boolean; isFile: boolean; isDirectory: boolean; isExecutable: boolean }>;
|
||||
generateKeyPair?(options: {
|
||||
type: 'RSA' | 'ECDSA' | 'ED25519';
|
||||
bits?: number;
|
||||
|
||||
Reference in New Issue
Block a user