From 189a3f22c8a733835c0c471f3277d87bd34cbee0 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:45:44 -0400 Subject: [PATCH 01/16] Keep startup retries alive after transient storage errors --- PersistentSD.ps1 | 21 +++++++++++++++------ 1 file changed, 15 insertions(+), 6 deletions(-) diff --git a/PersistentSD.ps1 b/PersistentSD.ps1 index 3c8aacc..7eb78d1 100644 --- a/PersistentSD.ps1 +++ b/PersistentSD.ps1 @@ -71,13 +71,22 @@ function Set-PreferredDriveLetter { Write-Log 'PersistentSD started.' # Allow Windows storage discovery to settle; retry for up to about one minute. for ($attempt = 0; $attempt -lt 12; $attempt++) { - Set-PreferredDriveLetter - $disk = Get-TargetDisk - if ($disk) { - $hasLetter = @(Get-Partition -DiskNumber $disk.Number -ErrorAction SilentlyContinue | Where-Object { [string]$_.DriveLetter -eq $script:PreferredLetter }).Count -gt 0 - if ($hasLetter) { break } + try { + Set-PreferredDriveLetter + # Storage queries can fail transiently while Windows discovers a device. + # Keep the retry loop alive so the event watcher can still be registered. + $disk = Get-TargetDisk + if ($disk) { + $hasLetter = @(Get-Partition -DiskNumber $disk.Number -ErrorAction Stop | Where-Object { + [string]$_.DriveLetter -eq $script:PreferredLetter + }).Count -gt 0 + if ($hasLetter) { break } + } + } + catch { + Write-Log ("Startup retry {0}/12 failed: {1}" -f ($attempt + 1), $_.Exception.Message) } - Start-Sleep -Seconds 5 + if ($attempt -lt 11) { Start-Sleep -Seconds 5 } } try { From 58f6d7a0a1db82b3c1568071631c967c1bf02b09 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:45:51 -0400 Subject: [PATCH 02/16] Correct safety documentation and release download guidance --- README.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index f5ba9d9..2fccd22 100644 --- a/README.md +++ b/README.md @@ -8,12 +8,12 @@ A lightweight Windows utility designed to configure a preferred drive letter and PersistentSD application window showing the scheduled task status -[Download v1.0](https://github.com/Cutter81/PersistentSD/releases/tag/v1.0) · [Report a problem](https://github.com/Cutter81/PersistentSD/issues) +[Download latest release](https://github.com/Cutter81/PersistentSD/releases) · [Report a problem](https://github.com/Cutter81/PersistentSD/issues) -> [!WARNING] -> **Read before installing:** PersistentSD may remove the selected drive letter from another non-protected volume to assign it to your chosen device. That can break shortcuts, applications, or other paths that depend on the previous letter. Double-check the disk and letter before confirming any changes. +> [!IMPORTANT] +> **Drive-letter safety:** The current runtime script checks whether the preferred letter is already assigned to a partition. If another partition owns it, PersistentSD logs the conflict and leaves that volume and its drive letter untouched. It does not force the reassignment. Always double-check the selected disk and preferred letter before installing. ## What PersistentSD is designed to do @@ -34,8 +34,8 @@ The manager uses the Windows disk `UniqueId` to identify the selected device rat ### Option 1 — Download the release -1. Open the [PersistentSD v1.0 release page](https://github.com/Cutter81/PersistentSD/releases/tag/v1.0). -2. Download `PersistentSD-v1.0.zip` from the release assets. +1. Open the [PersistentSD releases page](https://github.com/Cutter81/PersistentSD/releases). +2. Download the ZIP attached to the release you intend to use. 3. Extract the ZIP to a folder you can keep in place. 4. Keep `PersistentSD.exe` and `PersistentSD.ps1` together in that extracted folder, then run the manager as **Administrator**. @@ -70,7 +70,7 @@ Task Scheduler can also be opened from the app if you want to inspect the task d | **Device matching** | The script is designed to match the selected disk using its Windows `UniqueId`. If it cannot find exactly one matching disk, it should make no drive-letter changes. | | **Partition selection** | The script is designed to skip system, boot, and recovery partitions. Among eligible data partitions on the selected disk, it prefers one already using the chosen letter, then one with an existing drive letter, then the first eligible partition. | | **Protected partitions** | System, boot, and recovery partitions are intended to be excluded from reassignment. | -| **Existing drive letters** | A preferred letter may be taken from another non-protected partition. Check what currently uses that letter first. | +| **Existing drive letters** | If another partition already owns the preferred letter, the runtime script leaves it untouched and retries later. It does not remove the other partition's letter. | | **Backups** | Existing target scripts are backed up before replacement. When changing script locations, the previously referenced script is backed up where applicable. | | **Old files** | Previous scripts are not automatically deleted. Verify the new setup before manually removing any old copies. | | **Failures** | Backups are retained if a later step fails; do not assume the app has automatically rolled everything back. | From 2a77f39f469534c203ea65a0d352923929e18c86 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:45:56 -0400 Subject: [PATCH 03/16] Rebuild package when documentation or license changes --- .github/workflows/build-persistentsd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-persistentsd.yml b/.github/workflows/build-persistentsd.yml index 33323ac..2df3fda 100644 --- a/.github/workflows/build-persistentsd.yml +++ b/.github/workflows/build-persistentsd.yml @@ -8,6 +8,8 @@ on: - 'PersistentSDManager.exe.manifest' - 'RazorBlade.ico' - 'Build-PersistentSD.bat' + - 'README.md' + - 'LICENSE' - '.github/workflows/build-persistentsd.yml' workflow_dispatch: From d24a92b24fda2e091fce27386a7d95c7115e874a Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:46:04 -0400 Subject: [PATCH 04/16] Run build validation for pull requests --- .github/workflows/build-persistentsd.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/.github/workflows/build-persistentsd.yml b/.github/workflows/build-persistentsd.yml index 2df3fda..3f252f7 100644 --- a/.github/workflows/build-persistentsd.yml +++ b/.github/workflows/build-persistentsd.yml @@ -1,5 +1,16 @@ name: Build PersistentSD on: + pull_request: + branches: [main] + paths: + - 'PersistentSDManager.cs' + - 'PersistentSD.ps1' + - 'PersistentSDManager.exe.manifest' + - 'RazorBlade.ico' + - 'Build-PersistentSD.bat' + - 'README.md' + - 'LICENSE' + - '.github/workflows/build-persistentsd.yml' push: branches: [main] paths: From 7473f4f493627ca48440f97f4455f82a0b55697c Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:52:52 -0400 Subject: [PATCH 05/16] Add manual Windows validation checklist --- TESTING.md | 56 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 TESTING.md diff --git a/TESTING.md b/TESTING.md new file mode 100644 index 0000000..d6b72b3 --- /dev/null +++ b/TESTING.md @@ -0,0 +1,56 @@ +# PersistentSD Manual Test Plan + +This checklist is for validating a build on a disposable test setup before publishing a release. The GitHub Actions build verifies compilation and packaging; it does **not** execute these Windows storage-behavior tests. + +## Before testing + +- Use a test PC or disposable Windows installation where possible. +- Back up important data and record current drive-letter assignments. +- Do not use a disk containing irreplaceable data for destructive or ambiguous test cases. +- Run the manager elevated, and keep the release ZIP and source/build artifact separate from any existing installed copy. +- Record Windows version, build commit, test result, and relevant PersistentSD.log entries. + +## Manager and configuration + +- [ ] Launch the manager as Administrator. +- [ ] Refresh the disk list; confirm device names, sizes, and IDs are understandable. +- [ ] Confirm a saved device selection and preferred letter survive closing/reopening the manager. +- [ ] Confirm the profile/Documents script-location preference survives closing/reopening. +- [ ] Confirm the UI rejects missing devices, missing letters, and unsupported letters. +- [ ] Confirm Check Status clearly reports missing tasks and unresolved script paths. +- [ ] Confirm Remove Task requires confirmation and leaves scripts/configuration intact. + +## Installation and recovery + +- [ ] Install with no existing task and a valid destination. +- [ ] Confirm the task exists, its action points to the generated script, and it runs as SYSTEM at startup. +- [ ] Update an existing task in the same destination; verify a backup of the old script and task XML is created. +- [ ] Update an existing task whose script is in the other supported destination; verify the prompt and backups are correct. +- [ ] Test with an existing task whose action cannot be parsed or whose script is missing. Confirm the app does not silently claim the task is healthy. +- [ ] Test a read-only/unwritable destination and a task-registration failure. Record whether preferences, script files, and the prior task remain consistent. +- [ ] Confirm backups are readable and no previous script is deleted automatically. + +## Drive-letter safety (use disposable test disks) + +- [ ] With the preferred letter free, confirm the intended eligible data partition receives it. +- [ ] With the preferred letter owned by a different volume, confirm that volume keeps its letter and PersistentSD logs the conflict. +- [ ] Confirm system, boot, recovery, and reserved partitions are not selected for reassignment. +- [ ] Test the selected disk offline; confirm PersistentSD does not bring it online automatically. +- [ ] Disconnect/reconnect the selected removable disk and confirm the arrival watcher and periodic retry behave as expected. +- [ ] Test a disk that is absent at startup and connected later. +- [ ] Test a disk with multiple eligible partitions and verify the chosen partition matches the documented selection order. +- [ ] Test a missing, duplicate, or changed disk UniqueId; confirm no drive-letter changes occur unless exactly one disk matches. +- [ ] Inspect logs for useful error messages when storage queries or assignment attempts fail. + +## Release checks + +- [ ] Confirm the build workflow succeeds on the exact commit intended for release. +- [ ] Download and extract the packaged ZIP on a clean test account or test machine. +- [ ] Confirm the ZIP contains the EXE, PowerShell template, README, and license, with the EXE and template together. +- [ ] Test the exact ZIP intended for publication, not only an EXE built locally. +- [ ] Publish a new version tag/release for changed binaries; do not overwrite an existing version's asset. +- [ ] Update release notes to distinguish successful compilation from runtime testing. + +## Reporting results + +For each failed or skipped item, record the step, Windows version, expected result, actual result, and relevant log output. Do not describe runtime behavior as verified unless the corresponding test was actually performed. From 29ebd46db89c6db359e6f51eb640f38310e9483b Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:53:03 -0400 Subject: [PATCH 06/16] Clarify status checks and document testing limits --- README.md | 31 ++++++++++++++++--------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 2fccd22..cf1005a 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ The manager uses the Windows disk `UniqueId` to identify the selected device rat - **Choose the device** from the detected Windows storage disks. - **Choose a preferred letter** from D: to Z:. - **Install or update** a startup task that runs as SYSTEM. -- **Check status** to inspect the scheduled task and the script it runs. +- **Check status** to inspect whether the task is registered and whether its script path can be resolved. This is not a complete health check of runtime drive assignment. - **Manage the task** from the app, including opening Task Scheduler or removing the task. - **Keep a safety trail** with backups of scripts being replaced and logs from the runtime script. @@ -39,7 +39,7 @@ The manager uses the Windows disk `UniqueId` to identify the selected device rat 3. Extract the ZIP to a folder you can keep in place. 4. Keep `PersistentSD.exe` and `PersistentSD.ps1` together in that extracted folder, then run the manager as **Administrator**. -The ZIP includes the application, script template, license, and quick-start instructions. You do not need to download the EXE or script separately. +The ZIP includes the application, script template, license, and README. You do not need to download the EXE or script separately. ### Option 2 — Build from source @@ -50,7 +50,7 @@ The ZIP includes the application, script template, license, and quick-start inst 3. Run `Build-PersistentSD.bat` on Windows. 4. After a successful build, launch `PersistentSD.exe` as **Administrator**. -The build script looks for the .NET Framework compiler in the standard 64-bit and 32-bit Windows locations. +The build script looks for the .NET Framework compiler in the standard 64-bit and 32-bit Windows locations. GitHub Actions also compiles and packages the app, but that automated build does not exercise real disk assignment behavior. ## How to use it @@ -59,7 +59,7 @@ The build script looks for the .NET Framework compiler in the standard 64-bit an 3. Select the preferred drive letter (**D:** through **Z:**). 4. Choose one of the offered Scripts folders. 5. Select **Save / Install / Update** and read the confirmation prompts carefully. -6. Use **Check Status** to review the registered task and script path. +6. Use **Check Status** to review whether the task and its script path can be found. Task Scheduler can also be opened from the app if you want to inspect the task directly. @@ -67,17 +67,21 @@ Task Scheduler can also be opened from the app if you want to inspect the task d | Area | What to know | | --- | --- | -| **Device matching** | The script is designed to match the selected disk using its Windows `UniqueId`. If it cannot find exactly one matching disk, it should make no drive-letter changes. | -| **Partition selection** | The script is designed to skip system, boot, and recovery partitions. Among eligible data partitions on the selected disk, it prefers one already using the chosen letter, then one with an existing drive letter, then the first eligible partition. | -| **Protected partitions** | System, boot, and recovery partitions are intended to be excluded from reassignment. | +| **Device matching** | The script matches the selected disk using its Windows `UniqueId`. If it cannot find exactly one matching disk, it should make no drive-letter changes. | +| **Partition selection** | The script skips partitions marked as system, boot, or recovery and certain partition types. Among eligible data partitions on the selected disk, it prefers one already using the chosen letter, then one with an existing drive letter, then the first eligible partition. | +| **Protected partitions** | System, boot, and recovery partitions are intended to be excluded from reassignment. Validate unusual disk layouts before relying on the utility. | | **Existing drive letters** | If another partition already owns the preferred letter, the runtime script leaves it untouched and retries later. It does not remove the other partition's letter. | -| **Backups** | Existing target scripts are backed up before replacement. When changing script locations, the previously referenced script is backed up where applicable. | +| **Backups** | Existing target scripts and scheduled-task XML are backed up before replacement when the manager can identify and read them. Backups are recovery copies, not an automatic rollback mechanism. | +| **Failed updates** | If a later installation step fails, a script or preferences file may already have been written. Inspect Task Scheduler and the backup folder before retrying; do not assume the previous state was automatically restored. | | **Old files** | Previous scripts are not automatically deleted. Verify the new setup before manually removing any old copies. | -| **Failures** | Backups are retained if a later step fails; do not assume the app has automatically rolled everything back. | | **Disk state** | The script does not automatically bring an offline disk online or clear its read-only flag. | -| **Logging** | Runtime activity is written to `PersistentSD.log`. | +| **Logging** | Runtime activity is written to `PersistentSD.log` beside the generated script, subject to the script folder being writable. | -Preferences are stored at `%LOCALAPPDATA%\PersistentSD\preferences.txt`. The startup task runs as **SYSTEM**, so its configured script must remain at the registered path and accessible to that account. +Preferences are stored at `%LOCALAPPDATA%\PersistentSD\preferences.txt`. The startup task runs as **SYSTEM**, so its configured script must remain at the registered path and accessible to that account. The user-profile preference file is not the runtime task's source of truth; the generated script contains the selected device ID and drive letter. + +## Testing status + +The automated workflow verifies that the C# application compiles and that a ZIP can be packaged. It does not run the application or validate Windows storage changes. See the [manual Windows test plan](TESTING.md) for installation, failure-recovery, drive-letter conflict, and device reconnect checks. Treat behavior as unverified until the relevant tests have actually been run on a test system. ## Project files @@ -89,10 +93,7 @@ Preferences are stored at `%LOCALAPPDATA%\PersistentSD\preferences.txt`. The sta | `RazorBlade.ico` | Application icon | | `PersistentSD.ps1` | PowerShell script template used by the manager | | `PersistentSD.png` | Application screenshot displayed above | - -## Testing notes - -This README describes the software's intended behavior and documented safeguards; it is not a claim that every Windows version, disk layout, or failure scenario has been exhaustively tested. Before installing or updating the startup task, verify the selected disk and current drive-letter assignments, and read each confirmation prompt carefully. +| `TESTING.md` | Manual Windows validation checklist | ## Feedback From 1259b7b7dd49482ec4ddd1d5ec4ae3e7a97fd3fc Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:53:12 -0400 Subject: [PATCH 07/16] Include manual test plan in workflow path filters --- .github/workflows/build-persistentsd.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/build-persistentsd.yml b/.github/workflows/build-persistentsd.yml index 3f252f7..e0c96ca 100644 --- a/.github/workflows/build-persistentsd.yml +++ b/.github/workflows/build-persistentsd.yml @@ -9,6 +9,7 @@ on: - 'RazorBlade.ico' - 'Build-PersistentSD.bat' - 'README.md' + - 'TESTING.md' - 'LICENSE' - '.github/workflows/build-persistentsd.yml' push: @@ -20,6 +21,7 @@ on: - 'RazorBlade.ico' - 'Build-PersistentSD.bat' - 'README.md' + - 'TESTING.md' - 'LICENSE' - '.github/workflows/build-persistentsd.yml' workflow_dispatch: From 9dba11c2bdc5f084d30fb9dcaf82a733dac3c49c Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:54:08 -0400 Subject: [PATCH 08/16] Restore script state when task registration fails --- PersistentSDManager.cs | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/PersistentSDManager.cs b/PersistentSDManager.cs index f689518..5770a4e 100644 --- a/PersistentSDManager.cs +++ b/PersistentSDManager.cs @@ -215,6 +215,8 @@ private void InstallTask(object sender, EventArgs e) if (generated.Contains("__DEVICE_UNIQUE_ID_BASE64__") || generated.Contains("__PREFERRED_LETTER__")) throw new InvalidOperationException("The script template contains unresolved configuration placeholders. No files were changed."); string backupPath = null; + string targetScriptBackupPath = null; + bool targetScriptExisted = File.Exists(targetScript); string backupDir = Path.Combine(destination, "PersistentSD-Backups", DateTime.Now.ToString("yyyyMMdd-HHmmss")); // Always create a recovery point before replacing an existing task, even when // its script path cannot be resolved or the new target file does not exist yet. @@ -225,6 +227,7 @@ private void InstallTask(object sender, EventArgs e) { backupPath = Path.Combine(backupDir, Path.GetFileName(targetScript) + ".bak"); File.Copy(targetScript, backupPath, false); + targetScriptBackupPath = backupPath; } // If switching destinations, back up the old task's actual script too. Never remove it here. if (existingScript != null && File.Exists(existingScript) && !String.Equals(Path.GetFullPath(existingScript), Path.GetFullPath(targetScript), StringComparison.OrdinalIgnoreCase)) @@ -242,11 +245,11 @@ private void InstallTask(object sender, EventArgs e) } } File.WriteAllText(targetScript, generated, new UTF8Encoding(false)); - SavePreferences(disk.Id, letters.SelectedItem.ToString().TrimEnd(':')); string xml = BuildTaskXml(targetScript); string temp = Path.Combine(Path.GetTempPath(), "PersistentSD-" + Guid.NewGuid().ToString("N") + ".xml"); File.WriteAllText(temp, xml, new UTF8Encoding(false)); var p = Run("schtasks.exe", "/Create /TN " + Quote("\\" + (existingTask ? existingName : TaskName)) + " /XML " + Quote(temp) + " /F", false); try { File.Delete(temp); } catch { } if (p.ExitCode == 0) { + SavePreferences(disk.Id, letters.SelectedItem.ToString().TrimEnd(':')); string verified = FindExistingScript(); if (verified != null && String.Equals(Path.GetFullPath(verified), Path.GetFullPath(targetScript), StringComparison.OrdinalIgnoreCase)) { status.Text = "Installed and verified. Task: " + (existingTask ? existingName : TaskName) + " | Letter: " + letters.SelectedItem + (backupPath == null ? "" : " | Backup: " + backupPath); @@ -258,7 +261,22 @@ private void InstallTask(object sender, EventArgs e) MessageBox.Show(this, status.Text + "\n\nReview Task Scheduler before relying on this setup.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Warning); } } - else MessageBox.Show(this, "Task creation failed. The generated script was saved, but the task was not confirmed.\n\n" + p.Output + "\n\nRun this utility as Administrator.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Error); + else { + string recovery = ""; + try { + if (targetScriptBackupPath != null && File.Exists(targetScriptBackupPath)) { + File.Copy(targetScriptBackupPath, targetScript, true); + recovery = " The previous script was restored from its backup."; + } else if (!targetScriptExisted && File.Exists(targetScript) && + (!existingTask || existingScript == null || !String.Equals(Path.GetFullPath(existingScript), Path.GetFullPath(targetScript), StringComparison.OrdinalIgnoreCase))) { + File.Delete(targetScript); + recovery = " The newly generated script was removed."; + } + } catch (Exception recoveryError) { + recovery = " Automatic script recovery failed: " + recoveryError.Message; + } + MessageBox.Show(this, "Task creation failed. Preferences were not updated.\n\n" + p.Output + "\n\n" + recovery + "\n\nRun this utility as Administrator and inspect the backup folder before retrying.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Error); + } RefreshStatus(); } catch (Exception ex) { MessageBox.Show(this, ex.Message, TaskName, MessageBoxButtons.OK, MessageBoxIcon.Error); } } From cac4ea9a48f0c86ccc01cd3551a8ab9ab8a76519 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:54:16 -0400 Subject: [PATCH 09/16] Document task-registration recovery behavior --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index cf1005a..639893f 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,7 @@ Task Scheduler can also be opened from the app if you want to inspect the task d | **Protected partitions** | System, boot, and recovery partitions are intended to be excluded from reassignment. Validate unusual disk layouts before relying on the utility. | | **Existing drive letters** | If another partition already owns the preferred letter, the runtime script leaves it untouched and retries later. It does not remove the other partition's letter. | | **Backups** | Existing target scripts and scheduled-task XML are backed up before replacement when the manager can identify and read them. Backups are recovery copies, not an automatic rollback mechanism. | -| **Failed updates** | If a later installation step fails, a script or preferences file may already have been written. Inspect Task Scheduler and the backup folder before retrying; do not assume the previous state was automatically restored. | +| **Failed updates** | If task registration fails, the manager attempts to restore the previous target script (or remove a newly created one) and does not save new preferences. Recovery itself can fail; inspect the error message, Task Scheduler, and backup folder before retrying. If registration succeeds but later verification fails, inspect the task and script manually. | | **Old files** | Previous scripts are not automatically deleted. Verify the new setup before manually removing any old copies. | | **Disk state** | The script does not automatically bring an offline disk online or clear its read-only flag. | | **Logging** | Runtime activity is written to `PersistentSD.log` beside the generated script, subject to the script folder being writable. | From 87d83971033fd11a1ef69dcca7244ef5e073f173 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:54:18 -0400 Subject: [PATCH 10/16] Add checks for failed-install recovery behavior --- TESTING.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TESTING.md b/TESTING.md index d6b72b3..68fd075 100644 --- a/TESTING.md +++ b/TESTING.md @@ -27,7 +27,7 @@ This checklist is for validating a build on a disposable test setup before publi - [ ] Update an existing task in the same destination; verify a backup of the old script and task XML is created. - [ ] Update an existing task whose script is in the other supported destination; verify the prompt and backups are correct. - [ ] Test with an existing task whose action cannot be parsed or whose script is missing. Confirm the app does not silently claim the task is healthy. -- [ ] Test a read-only/unwritable destination and a task-registration failure. Record whether preferences, script files, and the prior task remain consistent. +- [ ] Test a read-only/unwritable destination and a task-registration failure. Confirm preferences are not saved on registration failure and the manager attempts to restore the prior script or remove a newly created script; record any recovery failure. - [ ] Confirm backups are readable and no previous script is deleted automatically. ## Drive-letter safety (use disposable test disks) From 2720dbe37d1328c7f80c38ab16045a648f718850 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 00:54:37 -0400 Subject: [PATCH 11/16] Validate PowerShell syntax in CI --- .github/workflows/build-persistentsd.yml | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/.github/workflows/build-persistentsd.yml b/.github/workflows/build-persistentsd.yml index e0c96ca..dd9f728 100644 --- a/.github/workflows/build-persistentsd.yml +++ b/.github/workflows/build-persistentsd.yml @@ -36,6 +36,18 @@ jobs: - name: Check out source uses: actions/checkout@v4 + - name: Validate PowerShell template syntax + shell: pwsh + run: | + $tokens = $null + $errors = $null + [System.Management.Automation.Language.Parser]::ParseFile((Resolve-Path 'PersistentSD.ps1'), [ref]$tokens, [ref]$errors) | Out-Null + if ($errors.Count -gt 0) { + $errors | ForEach-Object { Write-Error $_.ToString() } + throw 'PowerShell template contains syntax errors.' + } + Write-Output 'PowerShell template syntax is valid.' + - name: Compile Windows Forms application shell: pwsh run: | From 2f169e7f72dc8d02c9205df78c0cd2bee7734f6f Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 01:20:48 -0400 Subject: [PATCH 12/16] Update build actions for Node.js 24 --- .github/workflows/build-persistentsd.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-persistentsd.yml b/.github/workflows/build-persistentsd.yml index dd9f728..437d04d 100644 --- a/.github/workflows/build-persistentsd.yml +++ b/.github/workflows/build-persistentsd.yml @@ -34,7 +34,7 @@ jobs: runs-on: windows-latest steps: - name: Check out source - uses: actions/checkout@v4 + uses: actions/checkout@v5 - name: Validate PowerShell template syntax shell: pwsh @@ -71,7 +71,7 @@ jobs: Get-FileHash PersistentSD-build.zip -Algorithm SHA256 | Format-List - name: Upload build artifact - uses: actions/upload-artifact@v4 + uses: actions/upload-artifact@v5 with: name: PersistentSD-Windows-build path: PersistentSD-build.zip From 81cf5614c227b342ce08b4bea860ca0aecf6b8ea Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 01:34:09 -0400 Subject: [PATCH 13/16] Report task and preference failures separately --- PersistentSDManager.cs | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/PersistentSDManager.cs b/PersistentSDManager.cs index 5770a4e..d8c9313 100644 --- a/PersistentSDManager.cs +++ b/PersistentSDManager.cs @@ -249,16 +249,23 @@ private void InstallTask(object sender, EventArgs e) string temp = Path.Combine(Path.GetTempPath(), "PersistentSD-" + Guid.NewGuid().ToString("N") + ".xml"); File.WriteAllText(temp, xml, new UTF8Encoding(false)); var p = Run("schtasks.exe", "/Create /TN " + Quote("\\" + (existingTask ? existingName : TaskName)) + " /XML " + Quote(temp) + " /F", false); try { File.Delete(temp); } catch { } if (p.ExitCode == 0) { - SavePreferences(disk.Id, letters.SelectedItem.ToString().TrimEnd(':')); + // Task registration succeeded. Saving preferences is separate; report a + // preference failure accurately without implying the task was not installed. + string preferenceWarning = ""; + try { SavePreferences(disk.Id, selectedLetter.TrimEnd(':')); } + catch (Exception preferenceError) { + preferenceWarning = "\\n\\nWARNING: The task was registered, but preferences could not be saved: " + preferenceError.Message + + "\\nThe generated script still contains the selected device ID and drive letter. Resolve the error, then save preferences again."; + } string verified = FindExistingScript(); if (verified != null && String.Equals(Path.GetFullPath(verified), Path.GetFullPath(targetScript), StringComparison.OrdinalIgnoreCase)) { - status.Text = "Installed and verified. Task: " + (existingTask ? existingName : TaskName) + " | Letter: " + letters.SelectedItem + (backupPath == null ? "" : " | Backup: " + backupPath); - status.ForeColor = Color.LightGreen; - MessageBox.Show(this, "PersistentSD was installed and verified successfully.\n\nRestart Windows for the preferred drive-letter assignment to take effect at startup. If that letter is already being used by another volume, PersistentSD will leave that volume untouched and will not force the letter.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Information); + status.Text = "Task registered and script path verified. Task: " + (existingTask ? existingName : TaskName) + " | Letter: " + selectedLetter + (backupPath == null ? "" : " | Backup: " + backupPath); + status.ForeColor = preferenceWarning.Length == 0 ? Color.LightGreen : Color.Khaki; + MessageBox.Show(this, "PersistentSD task registration and script path verification succeeded." + preferenceWarning + "\\n\\nRestart Windows for the preferred drive-letter assignment to take effect at startup. If that letter is already being used by another volume, PersistentSD will leave that volume untouched and will not force the letter.", TaskName, MessageBoxButtons.OK, preferenceWarning.Length == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning); } else { - status.Text = "Task registration returned success, but script path verification failed. Backup retained: " + (backupPath ?? "none"); + status.Text = "Task registration succeeded, but script path verification failed. Backup retained: " + (backupPath ?? "none"); status.ForeColor = Color.Khaki; - MessageBox.Show(this, status.Text + "\n\nReview Task Scheduler before relying on this setup.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show(this, status.Text + preferenceWarning + "\\n\\nThe task may already be active. Inspect Task Scheduler and its action before retrying; do not assume the task was rolled back.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Warning); } } else { From f2da49082bc39284e2a02924daf7e47f2d3a5607 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 01:34:28 -0400 Subject: [PATCH 14/16] Keep periodic retries running if volume watcher registration fails --- PersistentSD.ps1 | 34 ++++++++++++++++++++++++++-------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/PersistentSD.ps1 b/PersistentSD.ps1 index 7eb78d1..f475714 100644 --- a/PersistentSD.ps1 +++ b/PersistentSD.ps1 @@ -89,22 +89,40 @@ for ($attempt = 0; $attempt -lt 12; $attempt++) { if ($attempt -lt 11) { Start-Sleep -Seconds 5 } } +$watcherRegistered = $false try { Register-WmiEvent -Class Win32_VolumeChangeEvent -SourceIdentifier $script:EventSource -ErrorAction Stop | Out-Null + $watcherRegistered = $true Write-Log 'Volume arrival watcher registered.' +} +catch { + # A watcher failure must not terminate the task. Periodic retries still allow + # the selected device to receive its preferred letter when it becomes available. + Write-Log ("Volume arrival watcher registration failed; using periodic retries: {0}" -f $_.Exception.Message) +} + +try { while ($true) { - $event = Wait-Event -SourceIdentifier $script:EventSource -Timeout 60 - if ($event) { - if ($event.SourceEventArgs.NewEvent.EventType -eq 2) { - Start-Sleep -Seconds 2 + if ($watcherRegistered) { + $event = Wait-Event -SourceIdentifier $script:EventSource -Timeout 60 + if ($event) { + if ($event.SourceEventArgs.NewEvent.EventType -eq 2) { + Start-Sleep -Seconds 2 + Set-PreferredDriveLetter + } + Remove-Event -EventIdentifier $event.EventIdentifier -ErrorAction SilentlyContinue + } else { + # Periodic retry also covers cases where Windows misses an arrival event. Set-PreferredDriveLetter } - Remove-Event -EventIdentifier $event.EventIdentifier -ErrorAction SilentlyContinue } else { - # Periodic retry also covers cases where Windows misses an arrival event. + # Keep running even if WMI event registration is unavailable. + Start-Sleep -Seconds 60 Set-PreferredDriveLetter } } } -catch { Write-Log ("Watcher error: {0}" -f $_.Exception.Message); exit 1 } -finally { Unregister-Event -SourceIdentifier $script:EventSource -ErrorAction SilentlyContinue } +catch { Write-Log ("Runtime retry loop error: {0}" -f $_.Exception.Message); exit 1 } +finally { + if ($watcherRegistered) { Unregister-Event -SourceIdentifier $script:EventSource -ErrorAction SilentlyContinue } +} From f8ea68dcdcdbd4ddc1794601be0a188e48ac14a9 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 01:34:37 -0400 Subject: [PATCH 15/16] Cover watcher registration fallback in manual tests --- TESTING.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TESTING.md b/TESTING.md index 68fd075..d616259 100644 --- a/TESTING.md +++ b/TESTING.md @@ -41,6 +41,7 @@ This checklist is for validating a build on a disposable test setup before publi - [ ] Test a disk with multiple eligible partitions and verify the chosen partition matches the documented selection order. - [ ] Test a missing, duplicate, or changed disk UniqueId; confirm no drive-letter changes occur unless exactly one disk matches. - [ ] Inspect logs for useful error messages when storage queries or assignment attempts fail. +- [ ] Simulate volume-arrival watcher registration failure where practical; confirm the task stays running and falls back to periodic retries rather than exiting. ## Release checks From 1fd13cae048c3abbb061eadf5f73f3beee6c2641 Mon Sep 17 00:00:00 2001 From: Cutter81 Date: Mon, 19 Oct 2026 01:34:55 -0400 Subject: [PATCH 16/16] Fix warning message line-break escapes --- PersistentSDManager.cs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/PersistentSDManager.cs b/PersistentSDManager.cs index d8c9313..0e3b8f8 100644 --- a/PersistentSDManager.cs +++ b/PersistentSDManager.cs @@ -254,18 +254,18 @@ private void InstallTask(object sender, EventArgs e) string preferenceWarning = ""; try { SavePreferences(disk.Id, selectedLetter.TrimEnd(':')); } catch (Exception preferenceError) { - preferenceWarning = "\\n\\nWARNING: The task was registered, but preferences could not be saved: " + preferenceError.Message + - "\\nThe generated script still contains the selected device ID and drive letter. Resolve the error, then save preferences again."; + preferenceWarning = "\n\nWARNING: The task was registered, but preferences could not be saved: " + preferenceError.Message + + "\nThe generated script still contains the selected device ID and drive letter. Resolve the error, then save preferences again."; } string verified = FindExistingScript(); if (verified != null && String.Equals(Path.GetFullPath(verified), Path.GetFullPath(targetScript), StringComparison.OrdinalIgnoreCase)) { status.Text = "Task registered and script path verified. Task: " + (existingTask ? existingName : TaskName) + " | Letter: " + selectedLetter + (backupPath == null ? "" : " | Backup: " + backupPath); status.ForeColor = preferenceWarning.Length == 0 ? Color.LightGreen : Color.Khaki; - MessageBox.Show(this, "PersistentSD task registration and script path verification succeeded." + preferenceWarning + "\\n\\nRestart Windows for the preferred drive-letter assignment to take effect at startup. If that letter is already being used by another volume, PersistentSD will leave that volume untouched and will not force the letter.", TaskName, MessageBoxButtons.OK, preferenceWarning.Length == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning); + MessageBox.Show(this, "PersistentSD task registration and script path verification succeeded." + preferenceWarning + "\n\nRestart Windows for the preferred drive-letter assignment to take effect at startup. If that letter is already being used by another volume, PersistentSD will leave that volume untouched and will not force the letter.", TaskName, MessageBoxButtons.OK, preferenceWarning.Length == 0 ? MessageBoxIcon.Information : MessageBoxIcon.Warning); } else { status.Text = "Task registration succeeded, but script path verification failed. Backup retained: " + (backupPath ?? "none"); status.ForeColor = Color.Khaki; - MessageBox.Show(this, status.Text + preferenceWarning + "\\n\\nThe task may already be active. Inspect Task Scheduler and its action before retrying; do not assume the task was rolled back.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Warning); + MessageBox.Show(this, status.Text + preferenceWarning + "\n\nThe task may already be active. Inspect Task Scheduler and its action before retrying; do not assume the task was rolled back.", TaskName, MessageBoxButtons.OK, MessageBoxIcon.Warning); } } else {