Improve script error dialogs #304

- Include the script's directory path #304.
- Exclude Windows-specific instructions on non-Windows OS.
- Standardize language across dialogs for consistency.

Other supporting changes:

- Add script diagnostics data collection from main process.
- Document script file storage and execution tamper protection in
  SECURITY.md.
- Remove redundant comment in `NodeReadbackFileWriter`.
- Centralize error display for uniformity and simplicity.
- Simpify `WindowVariablesValidator` to omit checks when not on the
  renderer process.
- Improve and centralize Electron environment detection.
- Use more emphatic language (don't worry) in error messages.
This commit is contained in:
undergroundwires
2024-01-17 23:59:05 +01:00
parent f03fc24098
commit 6ada8d425c
34 changed files with 1182 additions and 450 deletions

View File

@@ -11,9 +11,8 @@
import { defineComponent, computed } from 'vue';
import { injectKey } from '@/presentation/injectionSymbols';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { Dialog } from '@/presentation/common/Dialog';
import { CodeRunError } from '@/application/CodeRunner/CodeRunner';
import IconButton from './IconButton.vue';
import { createScriptErrorDialog } from './ScriptErrorDialog';
export default defineComponent({
components: {
@@ -24,6 +23,7 @@ export default defineComponent({
const { os, isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
const { codeRunner } = injectKey((keys) => keys.useCodeRunner);
const { dialog } = injectKey((keys) => keys.useDialog);
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
const canRun = computed<boolean>(() => getCanRunState(
currentState.value.os,
@@ -38,7 +38,12 @@ export default defineComponent({
currentContext.state.collection.scripting.fileExtension,
);
if (!success) {
showScriptRunError(dialog, error);
dialog.showError(...(await createScriptErrorDialog({
errorContext: 'run',
errorType: error.type,
errorMessage: error.message,
isFileReadbackError: error.type === 'FileReadbackVerificationError',
}, scriptDiagnosticsCollector)));
}
}
@@ -57,65 +62,4 @@ function getCanRunState(
const isRunningOnSelectedOs = selectedOs === hostOs;
return isRunningAsDesktopApplication && isRunningOnSelectedOs;
}
function showScriptRunError(dialog: Dialog, error: CodeRunError) {
const technicalDetails = `[${error.type}] ${error.message}`;
dialog.showError(
...(
error.type === 'FileReadbackVerificationError'
? createAntivirusErrorDialog(technicalDetails)
: createGenericErrorDialog(technicalDetails)),
);
}
function createGenericErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
return [
'Error Running Script',
[
'We encountered an issue while running the script.',
'This could be due to a variety of factors such as system permissions, resource constraints, or security software interventions.',
'\n',
'Here are some steps you can take:',
'- Confirm that you have the necessary permissions to execute scripts on your system.',
'- Check if there is sufficient disk space and system resources available.',
[
'- Antivirus or security software can sometimes mistakenly block script execution.',
'Verify your security settings, or temporarily disable the security software to see if that resolves the issue.',
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
].join(' '),
'- If possible, try running a different script to determine if the issue is specific to a particular script.',
'- Should the problem persist, reach out to the community for further assistance.',
'\n',
'Technical Details:',
technicalDetails,
].join('\n'),
];
}
function createAntivirusErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
return [
'Potential Antivirus Intervention',
[
[
'We\'ve encountered a problem which may be due to your antivirus software intervening.',
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software such as Defender.',
].join(' '),
'\n',
'To address this, you can:',
'1. Temporarily disable your antivirus (real-time protection) or add an exclusion for privacy.sexy scripts.',
'2. Re-try running or downloading the script.',
'3. If the issue persists, check your antivirus logs for more details and consider reporting this as a false positive to your antivirus provider.',
'\n',
'To handle false warnings in Defender: Open "Virus & threat protection" from the "Start" menu.',
'\n',
[
'Remember to re-enable your antivirus protection as soon as possible for your security.',
'For more guidance, refer to your antivirus documentation.',
].join(' '),
'\n',
'Technical Details:',
technicalDetails,
].join('\n'),
];
}
</script>

View File

@@ -20,8 +20,9 @@ import ModalDialog from '@/presentation/components/Shared/Modal/ModalDialog.vue'
import { ScriptingLanguage } from '@/domain/ScriptingLanguage';
import { IScriptingDefinition } from '@/domain/IScriptingDefinition';
import { ScriptFilename } from '@/application/CodeRunner/ScriptFilename';
import { Dialog, FileType, SaveFileError } from '@/presentation/common/Dialog';
import { FileType } from '@/presentation/common/Dialog';
import IconButton from '../IconButton.vue';
import { createScriptErrorDialog } from '../ScriptErrorDialog';
import RunInstructions from './RunInstructions/RunInstructions.vue';
export default defineComponent({
@@ -34,6 +35,7 @@ export default defineComponent({
const { currentState } = injectKey((keys) => keys.useCollectionState);
const { isRunningAsDesktopApplication } = injectKey((keys) => keys.useRuntimeEnvironment);
const { dialog } = injectKey((keys) => keys.useDialog);
const { scriptDiagnosticsCollector } = injectKey((keys) => keys.useScriptDiagnosticsCollector);
const areInstructionsVisible = ref(false);
const filename = computed<string>(() => buildFilename(currentState.value.collection.scripting));
@@ -45,7 +47,12 @@ export default defineComponent({
getType(currentState.value.collection.scripting.language),
);
if (!success) {
showScriptSaveError(dialog, error);
dialog.showError(...(await createScriptErrorDialog({
errorContext: 'save',
errorType: error.type,
errorMessage: error.message,
isFileReadbackError: error.type === 'FileReadbackVerificationError',
}, scriptDiagnosticsCollector)));
return;
}
areInstructionsVisible.value = true;
@@ -77,60 +84,4 @@ function buildFilename(scripting: IScriptingDefinition) {
}
return ScriptFilename;
}
function showScriptSaveError(dialog: Dialog, error: SaveFileError) {
const technicalDetails = `[${error.type}] ${error.message}`;
dialog.showError(
...(
error.type === 'FileReadbackVerificationError'
? createAntivirusErrorDialog(technicalDetails)
: createGenericErrorDialog(technicalDetails)),
);
}
function createGenericErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
return [
'Error Saving Script',
[
'An error occurred while saving the script.',
'This issue may arise from insufficient permissions, limited disk space, or interference from security software.',
'\n',
'To address this:',
'- Verify your permissions for the selected save directory.',
'- Check available disk space.',
'- Review your antivirus or security settings; adding an exclusion for privacy.sexy might be necessary.',
'- Try saving the script to a different location or modifying your selection.',
'- If the problem persists, reach out to the community for further assistance.',
'\n',
'Technical Details:',
technicalDetails,
].join('\n'),
];
}
function createAntivirusErrorDialog(technicalDetails: string): Parameters<Dialog['showError']> {
return [
'Potential Antivirus Intervention',
[
[
'It seems your antivirus software might have blocked the saving of the script.',
'privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software such as Defender.',
].join(' '),
'\n',
'To resolve this, consider:',
'1. Checking your antivirus for any blocking notifications and allowing the script.',
'2. Temporarily disabling real-time protection or adding an exclusion for privacy.sexy scripts.',
'3. Re-attempting to save the script.',
'4. If the problem continues, review your antivirus logs for more details.',
'\n',
'To handle false warnings in Defender: Open "Virus & threat protection" from the "Start" menu.',
'\n',
'Always ensure to re-enable your antivirus protection promptly.',
'For more guidance, refer to your antivirus documentation.',
'\n',
'Technical Details:',
technicalDetails,
].join('\n'),
];
}
</script>

View File

@@ -49,7 +49,7 @@
</p>
<p>
These false positives are common for scripts that modify system settings.
privacy.sexy is secure, transparent, and open-source.
Don't worry; privacy.sexy is secure, transparent, and open-source.
</p>
<p>
To handle false warnings in Microsoft Defender:

View File

@@ -0,0 +1,205 @@
import { ScriptDiagnosticData, ScriptDiagnosticsCollector } from '@/application/ScriptDiagnostics/ScriptDiagnosticsCollector';
import { OperatingSystem } from '@/domain/OperatingSystem';
import { Dialog } from '@/presentation/common/Dialog';
export async function createScriptErrorDialog(
information: ScriptErrorDetails,
scriptDiagnosticsCollector: ScriptDiagnosticsCollector | undefined,
): Promise<Parameters<Dialog['showError']>> {
const diagnostics = await scriptDiagnosticsCollector?.collectDiagnosticInformation();
if (information.isFileReadbackError) {
return createAntivirusErrorDialog(information, diagnostics);
}
return createGenericErrorDialog(information, diagnostics);
}
export interface ScriptErrorDetails {
readonly errorContext: 'run' | 'save';
readonly errorType: string;
readonly errorMessage: string;
readonly isFileReadbackError: boolean;
}
function createGenericErrorDialog(
information: ScriptErrorDetails,
diagnostics: ScriptDiagnosticData | undefined,
): Parameters<Dialog['showError']> {
return [
selectBasedOnErrorContext({
runningScript: 'Error Running Script',
savingScript: 'Error Saving Script',
}, information),
[
selectBasedOnErrorContext({
runningScript: 'An error occurred while running the script.',
savingScript: 'An error occurred while saving the script.',
}, information),
'This error could be caused by insufficient permissions, limited disk space, or security software interference.',
'\n',
generateUnorderedSolutionList({
title: 'To address this, you can:',
solutions: [
'Check if there is enough disk space and system resources are available.',
selectBasedOnDirectoryPath({
withoutDirectoryPath: 'Verify your access rights to the script\'s folder.',
withDirectoryPath: (directory) => `Verify your access rights to the script's folder: "${directory}".`,
}, diagnostics),
[
'Check if antivirus or security software has mistakenly blocked the script.',
'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
'Temporarily disabling the security software may resolve this.',
].join(' '),
selectBasedOnErrorContext({
runningScript: 'Confirm that you have the necessary permissions to execute scripts on your system.',
savingScript: 'Try saving the script to a different location.',
}, information),
generateTryDifferentSelectionAdvice(information),
'If the problem persists, reach out to the community for further assistance.',
],
}),
'\n',
generateTechnicalDetails(information),
].join('\n'),
];
}
function createAntivirusErrorDialog(
information: ScriptErrorDetails,
diagnostics: ScriptDiagnosticData | undefined,
): Parameters<Dialog['showError']> {
const defenderSteps = generateDefenderSteps(information, diagnostics);
return [
'Possible Antivirus Script Block',
[
[
'It seems your antivirus software might have removed the script.',
'Don\'t worry; privacy.sexy is secure, transparent, and open-source, but the scripts might still be mistakenly flagged by antivirus software.',
].join(' '),
'\n',
selectBasedOnErrorContext({
savingScript: generateOrderedSolutionList({
title: 'To address this, you can:',
solutions: [
'Check your antivirus for any blocking notifications and allow the script.',
'Disable antivirus or security software temporarily or add an exclusion.',
'Save the script again.',
],
}),
runningScript: generateOrderedSolutionList({
title: 'To address this, you can:',
solutions: [
selectBasedOnDirectoryPath({
withoutDirectoryPath: 'Disable antivirus or security software temporarily or add an exclusion.',
withDirectoryPath: (directory) => `Disable antivirus or security software temporarily or add a directory exclusion for scripts executed from: "${directory}".`,
}, diagnostics),
'Run the script again.',
],
}),
}, information),
defenderSteps ? `\n${defenderSteps}\n` : '\n',
[
'It\'s important to re-enable your antivirus protection after resolving the issue for your security.',
'For more guidance, refer to your antivirus documentation.',
].join(' '),
'\n',
generateUnorderedSolutionList({
title: 'If the problem persists:',
solutions: [
generateTryDifferentSelectionAdvice(information),
'Consider reporting this as a false positive to your antivirus provider.',
'Review your antivirus logs for more details.',
'Reach out to the community for further assistance.',
],
}),
'\n',
generateTechnicalDetails(information),
].join('\n'),
];
}
interface SolutionListOptions {
readonly solutions: readonly string[];
readonly title: string;
}
function generateUnorderedSolutionList(options: SolutionListOptions) {
return [
options.title,
...options.solutions.map((step) => `- ${step}`),
].join('\n');
}
function generateTechnicalDetails(information: ScriptErrorDetails) {
const maxErrorMessageCharacters = 100;
const trimmedErrorMessage = information.errorMessage.length > maxErrorMessageCharacters
? `${information.errorMessage.substring(0, maxErrorMessageCharacters - 3)}...`
: information.errorMessage;
return `Technical Details: [${information.errorType}] ${trimmedErrorMessage}`;
}
function generateTryDifferentSelectionAdvice(information: ScriptErrorDetails) {
return selectBasedOnErrorContext({
runningScript: 'Run a different script selection to check if the problem is script-specific.',
savingScript: 'Save a different script selection to check if the problem is script-specific.',
}, information);
}
function selectBasedOnDirectoryPath<T>(
options: {
readonly withoutDirectoryPath: T,
withDirectoryPath: (directoryPath: string) => T,
},
diagnostics: ScriptDiagnosticData | undefined,
): T {
if (!diagnostics?.scriptsDirectoryAbsolutePath) {
return options.withoutDirectoryPath;
}
return options.withDirectoryPath(diagnostics.scriptsDirectoryAbsolutePath);
}
function generateOrderedSolutionList(options: SolutionListOptions): string {
return [
options.title,
...options.solutions.map((step, index) => `${index + 1}. ${step}`),
].join('\n');
}
function generateDefenderSteps(
information: ScriptErrorDetails,
diagnostics: ScriptDiagnosticData | undefined,
): string | undefined {
if (diagnostics?.currentOperatingSystem !== OperatingSystem.Windows) {
return undefined;
}
return generateOrderedSolutionList({
title: 'To handle false warnings in Defender:',
solutions: [
'Open "Virus & threat protection" via the "Start" menu.',
'Open "Manage settings" under "Virus & threat protection settings" heading.',
...selectBasedOnErrorContext({
savingScript: [
'Disable "Real-time protection" or add an exclusion by selecting "Add or remove exclusions".',
],
runningScript: selectBasedOnDirectoryPath({
withoutDirectoryPath: [
'Disable real-time protection or add exclusion for scripts.',
],
withDirectoryPath: (directory) => [
'Open "Add or remove exclusions" under "Add or remove exclusions".',
`Add directory exclusion for "${directory}".`,
],
}, diagnostics),
}, information),
],
});
}
function selectBasedOnErrorContext<T>(options: {
readonly savingScript: T;
readonly runningScript: T;
}, information: ScriptErrorDetails): T {
if (information.errorContext === 'run') {
return options.runningScript;
}
return options.savingScript;
}