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:
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
import { WindowVariables } from '@/infrastructure/WindowVariables/WindowVariables';
|
||||
|
||||
export function useScriptDiagnosticsCollector(
|
||||
window: Partial<WindowVariables> = globalThis.window,
|
||||
) {
|
||||
return {
|
||||
scriptDiagnosticsCollector: window?.scriptDiagnosticsCollector,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user