Fix win execution with whitespace in username #351
This commit addresses the issue where scripts fail to execute on Windows environments with usernames containing spaces. The problem stemmed from PowerShell and cmd shell's handling of spaces in quoted arguments. The solution involves encoding PowerShell commands before execution, which mitigates the quoting issues previously causing script failures. This approach is now integrated into the execution flow, ensuring that commands are correctly handled irrespective of user names or other variables that may include spaces. Changes: - Implement encoding for PowerShell commands to handle spaces in usernames and other similar scenarios. - Update script documentation URLs to reflect changes in directory structure. Fixes #351
This commit is contained in:
@@ -0,0 +1,51 @@
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { EncodedPowerShellInvokeCmdCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/EncodedPowerShellInvokeCmdCommandCreator';
|
||||
|
||||
describe('EncodedPowerShellInvokeCmdCommandCreator', () => {
|
||||
describe('createCommandToInvokePowerShell', () => {
|
||||
it('starts with PowerShell base command', () => {
|
||||
// arrange
|
||||
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||
// act
|
||||
const command = sut.createCommandToInvokePowerShell('non-important-command');
|
||||
// assert
|
||||
expect(command.startsWith('PowerShell ')).to.equal(true);
|
||||
});
|
||||
it('includes encoded command as parameter', () => {
|
||||
// arrange
|
||||
const expectedParameterName = '-EncodedCommand';
|
||||
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||
// act
|
||||
const command = sut.createCommandToInvokePowerShell('non-important-command');
|
||||
// assert
|
||||
const args = parsePowerShellArgs(command);
|
||||
const parameterNames = [...args.keys()];
|
||||
expect(parameterNames).to.include(expectedParameterName);
|
||||
});
|
||||
it('correctly encode the command as utf16le base64', () => {
|
||||
// arrange
|
||||
const givenCode = 'Write-Output "Today is $(Get-Date -Format \'dddd, MMMM dd\')."';
|
||||
const expectedEncodedCommand = 'VwByAGkAdABlAC0ATwB1AHQAcAB1AHQAIAAiAFQAbwBkAGEAeQAgAGkAcwAgACQAKABHAGUAdAAtAEQAYQB0AGUAIAAtAEYAbwByAG0AYQB0ACAAJwBkAGQAZABkACwAIABNAE0ATQBNACAAZABkACcAKQAuACIA';
|
||||
const sut = new EncodedPowerShellInvokeCmdCommandCreator();
|
||||
// act
|
||||
const command = sut.createCommandToInvokePowerShell(givenCode);
|
||||
// assert
|
||||
const args = parsePowerShellArgs(command);
|
||||
const actualEncodedCommand = args.get('-EncodedCommand');
|
||||
expect(actualEncodedCommand).to.equal(expectedEncodedCommand);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
function parsePowerShellArgs(command: string): Map<string, string | undefined> {
|
||||
const argsMap = new Map<string, string | undefined>();
|
||||
const argRegex = /(-\w+)(\s+([^ ]+))?/g;
|
||||
let match = argRegex.exec(command);
|
||||
while (match !== null) {
|
||||
const arg = match[1];
|
||||
const value = match[3];
|
||||
argsMap.set(arg, value);
|
||||
match = argRegex.exec(command);
|
||||
}
|
||||
return argsMap;
|
||||
}
|
||||
@@ -1,13 +0,0 @@
|
||||
import { describe } from 'vitest';
|
||||
import { CmdShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/CmdShellArgumentEscaper';
|
||||
import { runEscapeTests } from './ShellArgumentEscaperTestRunner';
|
||||
|
||||
describe('CmdShellArgumentEscaper', () => {
|
||||
runEscapeTests(() => new CmdShellArgumentEscaper(), [
|
||||
{
|
||||
description: 'encloses the path in double quotes',
|
||||
givenPath: 'C:\\Program Files\\app.exe',
|
||||
expectedPath: '"C:\\Program Files\\app.exe"',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -0,0 +1,23 @@
|
||||
import { describe } from 'vitest';
|
||||
import { PowerShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/PowerShellArgumentEscaper';
|
||||
import { runEscapeTests } from './ShellArgumentEscaperTestRunner';
|
||||
|
||||
describe('PowerShellArgumentEscaper', () => {
|
||||
runEscapeTests(() => new PowerShellArgumentEscaper(), [
|
||||
{
|
||||
description: 'encloses the path in single quotes',
|
||||
givenPath: 'C:\\Program Files\\app.exe',
|
||||
expectedPath: '\'C:\\Program Files\\app.exe\'',
|
||||
},
|
||||
{
|
||||
description: 'escapes single internal single quotes',
|
||||
givenPath: 'C:\\Users\\O\'Reilly\\Documents',
|
||||
expectedPath: '\'C:\\Users\\O\'\'Reilly\\Documents\'',
|
||||
},
|
||||
{
|
||||
description: 'escapes multiple internal single quotes',
|
||||
givenPath: 'C:\\Program Files\\User\'s Files\\Today\'s Files',
|
||||
expectedPath: '\'C:\\Program Files\\User\'\'s Files\\Today\'\'s Files\'',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -2,24 +2,66 @@ import { describe, it, expect } from 'vitest';
|
||||
import { WindowsVisibleTerminalCommand } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/WindowsVisibleTerminalCommand';
|
||||
import type { ShellArgumentEscaper } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/ShellArgument/ShellArgumentEscaper';
|
||||
import { ShellArgumentEscaperStub } from '@tests/unit/shared/Stubs/ShellArgumentEscaperStub';
|
||||
import type { Logger } from '@/application/Common/Log/Logger';
|
||||
import { LoggerStub } from '@tests/unit/shared/Stubs/LoggerStub';
|
||||
import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||
import { PowerShellInvokeShellCommandCreatorStub } from '@tests/unit/shared/Stubs/PowerShellInvokeShellCommandCreatorStub';
|
||||
import { expectExists } from '@tests/shared/Assertions/ExpectExists';
|
||||
|
||||
describe('WindowsVisibleTerminalCommand', () => {
|
||||
describe('buildShellCommand', () => {
|
||||
it('returns expected command for given escaped file path', () => {
|
||||
it('creates a PowerShell command with the escaped path', () => {
|
||||
// arrange
|
||||
const escapedFilePath = '/escaped/file/path';
|
||||
const expectedCommand = `PowerShell Start-Process -Verb RunAs -FilePath ${escapedFilePath}`;
|
||||
const expectedCommand = `Start-Process -Verb RunAs -FilePath ${escapedFilePath}`;
|
||||
const escaper = new ShellArgumentEscaperStub();
|
||||
escaper.escapePathArgument = () => escapedFilePath;
|
||||
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||
const sut = new CommandBuilder()
|
||||
.withEscaper(escaper)
|
||||
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||
.build();
|
||||
// act
|
||||
sut.buildShellCommand('unimportant');
|
||||
// assert
|
||||
const calls = powerShellCommandCreator.callHistory.filter((c) => c.methodName === 'createCommandToInvokePowerShell');
|
||||
expect(calls).to.have.lengthOf(1);
|
||||
const [actualCommand] = calls[0].args;
|
||||
expect(actualCommand).to.equal(expectedCommand);
|
||||
});
|
||||
it('returns a command to invoke PowerShell', () => {
|
||||
// arrange
|
||||
const expectedCommand = 'expected command from creator';
|
||||
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||
powerShellCommandCreator.createCommandToInvokePowerShell = () => expectedCommand;
|
||||
const sut = new CommandBuilder()
|
||||
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||
.build();
|
||||
// act
|
||||
const actualCommand = sut.buildShellCommand('unimportant');
|
||||
// assert
|
||||
expect(actualCommand).to.equal(expectedCommand);
|
||||
});
|
||||
it('escapes provided file path correctly', () => {
|
||||
it('logs the powershell command', () => {
|
||||
// arrange
|
||||
let expectedCommand: string | undefined;
|
||||
const powerShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||
powerShellCommandCreator.createCommandToInvokePowerShell = (command) => {
|
||||
expectedCommand = command;
|
||||
return 'unimportant command';
|
||||
};
|
||||
const logger = new LoggerStub();
|
||||
const sut = new CommandBuilder()
|
||||
.withLogger(logger)
|
||||
.withPowerShellCommandCreator(powerShellCommandCreator)
|
||||
.build();
|
||||
// act
|
||||
sut.buildShellCommand('unimportant');
|
||||
// assert
|
||||
expectExists(expectedCommand);
|
||||
logger.assertLogsContainMessagePart('info', expectedCommand);
|
||||
});
|
||||
it('escapes the provided file path', () => {
|
||||
// arrange
|
||||
const expectedFilePath = '/input';
|
||||
const escaper = new ShellArgumentEscaperStub();
|
||||
@@ -61,14 +103,33 @@ describe('WindowsVisibleTerminalCommand', () => {
|
||||
class CommandBuilder {
|
||||
private escaper: ShellArgumentEscaper = new ShellArgumentEscaperStub();
|
||||
|
||||
private logger: Logger = new LoggerStub();
|
||||
|
||||
private powerShellCommandCreator
|
||||
: PowerShellInvokeShellCommandCreator = new PowerShellInvokeShellCommandCreatorStub();
|
||||
|
||||
public withEscaper(escaper: ShellArgumentEscaper): this {
|
||||
this.escaper = escaper;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withPowerShellCommandCreator(
|
||||
powerShellCommandCreator: PowerShellInvokeShellCommandCreator,
|
||||
): this {
|
||||
this.powerShellCommandCreator = powerShellCommandCreator;
|
||||
return this;
|
||||
}
|
||||
|
||||
public withLogger(logger: Logger): this {
|
||||
this.logger = logger;
|
||||
return this;
|
||||
}
|
||||
|
||||
public build(): WindowsVisibleTerminalCommand {
|
||||
return new WindowsVisibleTerminalCommand(
|
||||
this.escaper,
|
||||
this.powerShellCommandCreator,
|
||||
this.logger,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import type { PowerShellInvokeShellCommandCreator } from '@/infrastructure/CodeRunner/Execution/CommandDefinition/Commands/PowerShellInvoke/PowerShellInvokeShellCommandCreator';
|
||||
import { StubWithObservableMethodCalls } from './StubWithObservableMethodCalls';
|
||||
|
||||
export class PowerShellInvokeShellCommandCreatorStub
|
||||
extends StubWithObservableMethodCalls<PowerShellInvokeShellCommandCreator>
|
||||
implements PowerShellInvokeShellCommandCreator {
|
||||
private command: string | undefined;
|
||||
|
||||
public withCreatedCommand(command: string): this {
|
||||
this.command = command;
|
||||
return this;
|
||||
}
|
||||
|
||||
public createCommandToInvokePowerShell(powerShellCommand: string): string {
|
||||
this.registerMethodCall({
|
||||
methodName: 'createCommandToInvokePowerShell',
|
||||
args: [powerShellCommand],
|
||||
});
|
||||
if (this.command === undefined) {
|
||||
return `[${PowerShellInvokeShellCommandCreatorStub.name}] ${powerShellCommand}`;
|
||||
}
|
||||
return this.command;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user