-
-
Notifications
You must be signed in to change notification settings - Fork 154
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Fix handling special chars in script paths
This commit improves the handling of paths with spaces or special characters during script execution in the desktop application. Key improvements: - Paths are now quoted for macOS/Linux, addressing issues with whitespace or single quotes. - Windows paths are enclosed in double quotes to handle special characters. Other supporting changes: - Add more documentation for terminal execution commands. - Refactor terminal script file execution into a dedicated file for improved separation of concerns. - Refactor naming of `RuntimeEnvironment` to align with naming conventions (no interface with I prefix) and for clarity. - Refactor `TemporaryFileCodeRunner` to simplify it by removing the `os` parameter and handling OS-specific logic within the filename generator instead. - Refactor `fileName` to `filename` for consistency.
- Loading branch information
1 parent
fac72ed
commit 40f5eb8
Showing
27 changed files
with
573 additions
and
316 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,6 @@ | ||
import { OperatingSystem } from '@/domain/OperatingSystem'; | ||
|
||
export interface CodeRunner { | ||
runCode( | ||
code: string, | ||
tempScriptFolderName: string, | ||
os: OperatingSystem, | ||
): Promise<void>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
3 changes: 3 additions & 0 deletions
3
src/infrastructure/CodeRunner/Execution/ScriptFileExecutor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface ScriptFileExecutor { | ||
executeScriptFile(filePath: string): Promise<void>; | ||
} |
117 changes: 117 additions & 0 deletions
117
src/infrastructure/CodeRunner/Execution/VisibleTerminalScriptFileExecutor.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,117 @@ | ||
import { OperatingSystem } from '@/domain/OperatingSystem'; | ||
import { CommandOps, SystemOperations } from '@/infrastructure/CodeRunner/SystemOperations/SystemOperations'; | ||
import { Logger } from '@/application/Common/Log/Logger'; | ||
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger'; | ||
import { RuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/RuntimeEnvironment'; | ||
import { HostRuntimeEnvironment } from '@/infrastructure/RuntimeEnvironment/HostRuntimeEnvironment'; | ||
import { createNodeSystemOperations } from '@/infrastructure/CodeRunner/SystemOperations/NodeSystemOperations'; | ||
import { ScriptFileExecutor } from './ScriptFileExecutor'; | ||
|
||
export class VisibleTerminalScriptExecutor implements ScriptFileExecutor { | ||
constructor( | ||
private readonly system: SystemOperations = createNodeSystemOperations(), | ||
private readonly logger: Logger = ElectronLogger, | ||
private readonly environment: RuntimeEnvironment = HostRuntimeEnvironment.CurrentEnvironment, | ||
) { } | ||
|
||
public async executeScriptFile(filePath: string): Promise<void> { | ||
const { os } = this.environment; | ||
if (os === undefined) { | ||
throw new Error('Unknown operating system'); | ||
} | ||
await this.setFileExecutablePermissions(filePath); | ||
await this.runFileWithRunner(filePath, os); | ||
} | ||
|
||
private async setFileExecutablePermissions(filePath: string): Promise<void> { | ||
this.logger.info(`Setting execution permissions for file at ${filePath}`); | ||
await this.system.fileSystem.setFilePermissions(filePath, '755'); | ||
this.logger.info(`Execution permissions set successfully for ${filePath}`); | ||
} | ||
|
||
private async runFileWithRunner(filePath: string, os: OperatingSystem): Promise<void> { | ||
this.logger.info(`Executing script file: ${filePath} on ${OperatingSystem[os]}.`); | ||
const runner = TerminalRunners[os]; | ||
if (!runner) { | ||
throw new Error(`Unsupported operating system: ${OperatingSystem[os]}`); | ||
} | ||
const context: TerminalExecutionContext = { | ||
scriptFilePath: filePath, | ||
commandOps: this.system.command, | ||
logger: this.logger, | ||
}; | ||
await runner(context); | ||
this.logger.info('Command script file successfully.'); | ||
} | ||
} | ||
|
||
interface TerminalExecutionContext { | ||
readonly scriptFilePath: string; | ||
readonly commandOps: CommandOps; | ||
readonly logger: Logger; | ||
} | ||
|
||
type TerminalRunner = (context: TerminalExecutionContext) => Promise<void>; | ||
|
||
const TerminalRunners: Partial<Record<OperatingSystem, TerminalRunner>> = { | ||
[OperatingSystem.Windows]: async (context) => { | ||
/* | ||
Options: | ||
"path": | ||
✅ Launches the script within `cmd.exe`. | ||
✅ Uses user-friendly GUI sudo prompt. | ||
*/ | ||
const command = cmdShellPathArgumentEscape(context.scriptFilePath); | ||
await runCommand(command, context); | ||
}, | ||
[OperatingSystem.Linux]: async (context) => { | ||
const command = `x-terminal-emulator -e ${posixShellPathArgumentEscape(context.scriptFilePath)}`; | ||
/* | ||
Options: | ||
`x-terminal-emulator -e`: | ||
✅ Launches the script within the default terminal emulator. | ||
❌ Requires terminal-based (not GUI) sudo prompt, which may not be very user friendly. | ||
*/ | ||
await runCommand(command, context); | ||
}, | ||
[OperatingSystem.macOS]: async (context) => { | ||
const command = `open -a Terminal.app ${posixShellPathArgumentEscape(context.scriptFilePath)}`; | ||
/* | ||
Options: | ||
`open -a Terminal.app`: | ||
✅ Launches the script within Terminal app, that exists natively in all modern macOS | ||
versions. | ||
❌ Requires terminal-based (not GUI) sudo prompt, which may not be very user friendly. | ||
❌ Terminal app requires many privileges to execute the script, this would prompt user | ||
to grant privileges to the Terminal app. | ||
`osascript -e "do shell script \\"${scriptPath}\\" with administrator privileges"`: | ||
✅ Uses user-friendly GUI sudo prompt. | ||
❌ Executes the script in the background, which does not provide the user with immediate | ||
visual feedback or allow interaction with the script as it runs. | ||
*/ | ||
await runCommand(command, context); | ||
}, | ||
} as const; | ||
|
||
async function runCommand(command: string, context: TerminalExecutionContext): Promise<void> { | ||
context.logger.info(`Executing command:\n${command}`); | ||
await context.commandOps.exec(command); | ||
context.logger.info('Executed command successfully.'); | ||
} | ||
|
||
function posixShellPathArgumentEscape(pathArgument: string): string { | ||
// - Wraps the path in single quotes, which is a standard practice in POSIX shells | ||
// (like bash and zsh) found on macOS/Linux to ensure that characters like spaces, '*', and | ||
// '?' are treated as literals, not as special characters. | ||
// - Escapes any single quotes within the path itself. This allows paths containing single | ||
// quotes to be correctly interpreted in POSIX-compliant systems, such as Linux and macOS. | ||
return `'${pathArgument.replaceAll('\'', '\'\\\'\'')}'`; | ||
} | ||
|
||
function cmdShellPathArgumentEscape(pathArgument: string): string { | ||
// - Encloses the path in double quotes, which is necessary for Windows command line (cmd.exe) | ||
// to correctly handle paths containing spaces. | ||
// - Paths in Windows cannot include double quotes `"` themselves, so these are not escaped. | ||
return `"${pathArgument}"`; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
export interface FilenameGenerator { | ||
generateFilename(): string; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
51 changes: 51 additions & 0 deletions
51
src/infrastructure/RuntimeEnvironment/HostRuntimeEnvironment.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,51 @@ | ||
import { OperatingSystem } from '@/domain/OperatingSystem'; | ||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables'; | ||
import { IEnvironmentVariables } from '@/infrastructure/EnvironmentVariables/IEnvironmentVariables'; | ||
import { EnvironmentVariablesFactory } from '@/infrastructure/EnvironmentVariables/EnvironmentVariablesFactory'; | ||
import { ConditionBasedOsDetector } from './BrowserOs/ConditionBasedOsDetector'; | ||
import { BrowserEnvironment, BrowserOsDetector } from './BrowserOs/BrowserOsDetector'; | ||
import { RuntimeEnvironment } from './RuntimeEnvironment'; | ||
import { isTouchEnabledDevice } from './TouchSupportDetection'; | ||
|
||
export class HostRuntimeEnvironment implements RuntimeEnvironment { | ||
public static readonly CurrentEnvironment | ||
: RuntimeEnvironment = new HostRuntimeEnvironment(window); | ||
|
||
public readonly isDesktop: boolean; | ||
|
||
public readonly os: OperatingSystem | undefined; | ||
|
||
public readonly isNonProduction: boolean; | ||
|
||
protected constructor( | ||
window: Partial<Window>, | ||
environmentVariables: IEnvironmentVariables = EnvironmentVariablesFactory.Current.instance, | ||
browserOsDetector: BrowserOsDetector = new ConditionBasedOsDetector(), | ||
touchDetector = isTouchEnabledDevice, | ||
) { | ||
if (!window) { throw new Error('missing window'); } // do not trust strictNullChecks for global objects | ||
this.isNonProduction = environmentVariables.isNonProduction; | ||
this.isDesktop = isDesktop(window); | ||
if (this.isDesktop) { | ||
this.os = window?.os; | ||
} else { | ||
this.os = undefined; | ||
const userAgent = getUserAgent(window); | ||
if (userAgent) { | ||
const browserEnvironment: BrowserEnvironment = { | ||
userAgent, | ||
isTouchSupported: touchDetector(), | ||
}; | ||
this.os = browserOsDetector.detect(browserEnvironment); | ||
} | ||
} | ||
} | ||
} | ||
|
||
function getUserAgent(window: Partial<Window>): string | undefined { | ||
return window?.navigator?.userAgent; | ||
} | ||
|
||
function isDesktop(window: Partial<WindowVariables>): boolean { | ||
return window?.isDesktop === true; | ||
} |
This file was deleted.
Oops, something went wrong.
Oops, something went wrong.