Show save/execution error dialogs on desktop #264

This commit introduces system-native error dialogs on desktop
application for code save or execution failures, addressing user confusion
described in issue #264.

This commit adds informative feedback when script execution or saving
fails.

Changes:

- Implement support for system-native error dialogs.
- Refactor `CodeRunner` and `Dialog` interfaces and their
  implementations to improve error handling and provide better type
  safety.
- Introduce structured error handling, allowing UI to display detailed
  error messages.
- Replace error throwing with an error object interface for controlled
  handling. This ensures that errors are propagated to the renderer
  process without being limited by Electron's error object
  serialization limitations as detailed in electron/electron#24427.
- Add logging for dialog actions to aid in troubleshooting.
- Rename `fileName` to `defaultFilename` in `saveFile` functions
  to clarify its purpose.
- Centralize message assertion in `LoggerStub` for consistency.
- Introduce `expectTrue` in tests for clearer boolean assertions.
- Standardize `filename` usage across the codebase.
- Enhance existing test names and organization for clarity.
- Update related documentation.
This commit is contained in:
undergroundwires
2024-01-14 22:35:53 +01:00
parent c546a33eff
commit e09db0f1bd
48 changed files with 1986 additions and 578 deletions

View File

@@ -1,40 +1,116 @@
import { Logger } from '@/application/Common/Log/Logger';
import { ElectronLogger } from '@/infrastructure/Log/ElectronLogger';
import { CodeRunError, CodeRunErrorType } from '@/application/CodeRunner/CodeRunner';
import { SystemOperations } from '../../System/SystemOperations';
import { NodeElectronSystemOperations } from '../../System/NodeElectronSystemOperations';
import { ScriptDirectoryProvider } from './ScriptDirectoryProvider';
import { ScriptDirectoryOutcome, ScriptDirectoryProvider } from './ScriptDirectoryProvider';
export const ExecutionSubdirectory = 'runs';
/**
* Provides a dedicated directory for script execution.
* Benefits of using a persistent directory:
* - Antivirus Exclusions: Easier antivirus configuration.
* - Auditability: Stores script execution history for troubleshooting.
* - Reliability: Avoids issues with directory clean-ups during execution,
* seen in Windows Pro Azure VMs when stored on Windows temporary directory.
*/
export class PersistentDirectoryProvider implements ScriptDirectoryProvider {
constructor(
private readonly system: SystemOperations = new NodeElectronSystemOperations(),
private readonly logger: Logger = ElectronLogger,
) { }
async provideScriptDirectory(): Promise<string> {
const scriptsDirectory = this.system.location.combinePaths(
/*
Switched from temporary to persistent directory for script storage for improved reliability.
Temporary directories in some environments, such certain Windows Pro Azure VMs, showed
issues where scripts were interrupted due to directory cleanup during script execution.
This was observed with system temp directories (e.g., `%LOCALAPPDATA%\Temp`).
Persistent directories offer better stability during long executions and aid in auditability
and troubleshooting.
*/
this.system.operatingSystem.getUserDataDirectory(),
ExecutionSubdirectory,
);
this.logger.info(`Attempting to create script directory at path: ${scriptsDirectory}`);
try {
await this.system.fileSystem.createDirectory(scriptsDirectory, true);
this.logger.info(`Script directory successfully created at: ${scriptsDirectory}`);
} catch (error) {
this.logger.error(`Error creating script directory at ${scriptsDirectory}: ${error.message}`, error);
throw error;
public async provideScriptDirectory(): Promise<ScriptDirectoryOutcome> {
const {
success: isPathConstructed,
error: pathConstructionError,
directoryPath,
} = this.constructScriptDirectoryPath();
if (!isPathConstructed) {
return {
success: false,
error: pathConstructionError,
};
}
return scriptsDirectory;
const {
success: isDirectoryCreated,
error: directoryCreationError,
} = await this.createDirectory(directoryPath);
if (!isDirectoryCreated) {
return {
success: false,
error: directoryCreationError,
};
}
return {
success: true,
directoryAbsolutePath: directoryPath,
};
}
private async createDirectory(directoryPath: string): Promise<DirectoryPathCreationOutcome> {
try {
this.logger.info(`Attempting to create script directory at path: ${directoryPath}`);
await this.system.fileSystem.createDirectory(directoryPath, true);
this.logger.info(`Script directory successfully created at: ${directoryPath}`);
return {
success: true,
};
} catch (error) {
return {
success: false,
error: this.handleException(error, 'DirectoryCreationError'),
};
}
}
private constructScriptDirectoryPath(): DirectoryPathConstructionOutcome {
try {
const parentDirectory = this.system.operatingSystem.getUserDataDirectory();
const scriptDirectory = this.system.location.combinePaths(
parentDirectory,
ExecutionSubdirectory,
);
return {
success: true,
directoryPath: scriptDirectory,
};
} catch (error) {
return {
success: false,
error: this.handleException(error, 'DirectoryCreationError'),
};
}
}
private handleException(
exception: Error,
errorType: CodeRunErrorType,
): CodeRunError {
const errorMessage = 'Error during script directory creation';
this.logger.error(errorType, errorMessage, exception);
return {
type: errorType,
message: `${errorMessage}: ${exception.message}`,
};
}
}
type DirectoryPathConstructionOutcome = {
readonly success: false;
readonly error: CodeRunError;
readonly directoryPath?: undefined;
} | {
readonly success: true;
readonly directoryPath: string;
readonly error?: undefined;
};
type DirectoryPathCreationOutcome = {
readonly success: false;
readonly error: CodeRunError;
} | {
readonly success: true;
readonly error?: undefined;
};