Skip to content

Commit 67db7af

Browse files
Handle invalid CWD when creating file dialogs
1 parent 58d3039 commit 67db7af

2 files changed

Lines changed: 108 additions & 13 deletions

File tree

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
# Bug: SaveFileDialog crashes when current working directory is deleted/unmounted at runtime
2+
3+
## Summary
4+
On Linux (GTK/Eto), NAPS2 can throw an unhandled `System.IO.FileNotFoundException` when opening **Save Images** if the process current working directory becomes invalid after startup (for example, removable media unmounted or directory removed).
5+
6+
The dialog never opens and users may be unable to save scanned pages.
7+
8+
## Affected area
9+
- `NAPS2.Lib/EtoForms/EtoDialogHelper.cs`
10+
- GTK `SaveFileDialog` / `OpenFileDialog` construction path
11+
12+
## Observed stack trace
13+
```text
14+
System.IO.FileNotFoundException: Unable to find the specified file.
15+
at string Interop+Sys.GetCwd()
16+
at string Environment.get_CurrentDirectoryCore()
17+
at string Environment.get_CurrentDirectory()
18+
at string System.IO.Directory.GetCurrentDirectory()
19+
at new Eto.GtkSharp.Forms.SaveFileDialogHandler()
20+
at bool NAPS2.EtoForms.EtoDialogHelper.PromptToSaveImage(...)
21+
at async Task<bool> NAPS2.ImportExport.ExportController.SaveImages(...)
22+
```
23+
24+
## Steps to reproduce
25+
1. Launch NAPS2 from a directory that is later removed or unmounted.
26+
2. Scan/import pages.
27+
3. Click **Save Images**.
28+
29+
## Expected behavior
30+
NAPS2 should recover from an invalid CWD and still open the file dialog, falling back to a safe directory (typically user home).
31+
32+
## Actual behavior
33+
An unhandled exception is thrown while creating the dialog handler and the save dialog does not open.
34+
35+
## Proposed resolution
36+
Harden dialog creation in `EtoDialogHelper`:
37+
38+
- Before creating dialogs, verify current directory is accessible.
39+
- If inaccessible (exception or non-existent path), set `Environment.CurrentDirectory` to:
40+
1. `Environment.GetFolderPath(Environment.SpecialFolder.UserProfile)`
41+
2. fallback `Path.GetTempPath()` if needed
42+
- Wrap `SaveFileDialog` construction in a retry path for `FileNotFoundException`.
43+
- Apply the same CWD guard before `OpenFileDialog` creation.
44+
45+
## Patch status
46+
Implemented on branch:
47+
- `fix/savefiledialog-invalid-cwd-crash`
48+
49+
Files changed:
50+
- `NAPS2.Lib/EtoForms/EtoDialogHelper.cs`
51+
52+
## Validation done
53+
- File-level diagnostics clean for `EtoDialogHelper.cs`.
54+
- Full build was not run in this environment (`dotnet` CLI unavailable here).
55+
56+
## Suggested reviewer checklist
57+
- [ ] Reproduce with invalid CWD on Linux/GTK before patch
58+
- [ ] Confirm **Save Images** opens after patch
59+
- [ ] Confirm **Import** dialog also opens with invalid CWD
60+
- [ ] Confirm no regressions in normal save/import flows

NAPS2.Lib/EtoForms/EtoDialogHelper.cs

Lines changed: 48 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,8 @@ public override bool PromptToSavePdfOrImage(string? defaultPath, out string? sav
2222
{
2323
lastExt = "pdf";
2424
}
25-
var sd = new SaveFileDialog
26-
{
27-
FileName = GetDefaultFileName(defaultPath, lastExt!)
28-
};
25+
var sd = CreateSaveFileDialog();
26+
sd.FileName = GetDefaultFileName(defaultPath, lastExt!);
2927
_fileFilters.Set(sd, FileFilterGroup.Pdf | FileFilterGroup.Image, lastExt);
3028
SetDir(sd, defaultPath);
3129
EtoPlatform.Current.ConfigureFileDialog(sd);
@@ -41,10 +39,8 @@ public override bool PromptToSavePdfOrImage(string? defaultPath, out string? sav
4139

4240
public override bool PromptToSavePdf(string? defaultPath, out string? savePath)
4341
{
44-
var sd = new SaveFileDialog
45-
{
46-
FileName = GetDefaultFileName(defaultPath, "pdf")
47-
};
42+
var sd = CreateSaveFileDialog();
43+
sd.FileName = GetDefaultFileName(defaultPath, "pdf");
4844
_fileFilters.Set(sd, FileFilterGroup.Pdf);
4945
SetDir(sd, defaultPath);
5046
EtoPlatform.Current.ConfigureFileDialog(sd);
@@ -64,10 +60,8 @@ public override bool PromptToSaveImage(string? defaultPath, out string? savePath
6460
{
6561
lastExt = "jpg";
6662
}
67-
var sd = new SaveFileDialog
68-
{
69-
FileName = GetDefaultFileName(defaultPath, lastExt!)
70-
};
63+
var sd = CreateSaveFileDialog();
64+
sd.FileName = GetDefaultFileName(defaultPath, lastExt!);
7165
var filterGroups = EtoPlatform.Current.IsGtk
7266
? FileFilterGroup.AllImages | FileFilterGroup.Image
7367
: FileFilterGroup.Image;
@@ -96,6 +90,46 @@ public override bool PromptToSaveImage(string? defaultPath, out string? savePath
9690
: normPath;
9791
}
9892

93+
private static SaveFileDialog CreateSaveFileDialog()
94+
{
95+
EnsureCurrentDirectoryIsAccessible();
96+
try
97+
{
98+
return new SaveFileDialog();
99+
}
100+
catch (FileNotFoundException)
101+
{
102+
SetSafeCurrentDirectory();
103+
return new SaveFileDialog();
104+
}
105+
}
106+
107+
private static void EnsureCurrentDirectoryIsAccessible()
108+
{
109+
try
110+
{
111+
var currentDirectory = Directory.GetCurrentDirectory();
112+
if (!Directory.Exists(currentDirectory))
113+
{
114+
SetSafeCurrentDirectory();
115+
}
116+
}
117+
catch
118+
{
119+
SetSafeCurrentDirectory();
120+
}
121+
}
122+
123+
private static void SetSafeCurrentDirectory()
124+
{
125+
var fallbackDirectory = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
126+
if (string.IsNullOrWhiteSpace(fallbackDirectory) || !Directory.Exists(fallbackDirectory))
127+
{
128+
fallbackDirectory = Path.GetTempPath();
129+
}
130+
Environment.CurrentDirectory = fallbackDirectory;
131+
}
132+
99133
private void SetDir(SaveFileDialog dialog, string? defaultPath)
100134
{
101135
string? path = null;
@@ -128,6 +162,7 @@ private static string NormalizePath(string path)
128162

129163
public override bool PromptToImport(out string[]? filePaths)
130164
{
165+
EnsureCurrentDirectoryIsAccessible();
131166
var ofd = new OpenFileDialog
132167
{
133168
MultiSelect = true,
@@ -149,4 +184,4 @@ public override bool PromptToImport(out string[]? filePaths)
149184
filePaths = null;
150185
return false;
151186
}
152-
}
187+
}

0 commit comments

Comments
 (0)