v5.1: install RemSound as a proper Windows app, plus smaller sounds
New Options -> Install / Uninstall RemSound on this PC: a per-user self-installer (%LOCALAPPDATA%\Programs\RemSound, no admin) with optional desktop + Start-menu shortcuts, login auto-start (reuses StartupAutoStart), Windows Installed-apps registration, and copy-across of profiles+config, recordings and logs. Install state is decided by a marker file, not a folder-path guess; the post-install relaunch hands over foreground via AllowSetForegroundWindow so the installed copy comes to the front; uninstall uses a batch remover (no PowerShell) and confirms with two independent tick-boxes. All new dialogs use the house accessible controls (AccessibleCheckBox, Theme.Heading). Also: iOS (TestFlight) companion link alongside Android in README + manual; slimmed-down default cue WAVs; About/RELEASE_NOTES/manual updated; version -> 5.1. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.8
parent
a92928d357
commit
7038fef67c
@@ -278,8 +278,28 @@ Item| Shortcut| What it does
|
||||
**Manage named peers …**| Alt+O, N| Opens a list of every peer you've given a friendly name to, showing each one's machine name and where and when you last connected to it. Pick one and **Rename (Alt+R** , or F2) to change its name, or **Delete (Alt+D** , or the Del key) to forget it — deleting only drops the name, the peer still connects as normal under its machine name. See the peer-naming notes on the Connectivity tab.
|
||||
**Reset the default audio device prompt**| Alt+O, R| Brings back the “use only the default audio device?” question if you previously ticked “Don't ask me this again” on it. See Following the Windows default audio device.
|
||||
**Enable / Disable Realtek ASIO**| —| Only shown if a Realtek ASIO driver is installed. Lets you reverse the choice RemSound offered about disabling that driver (Realtek's generic ASIO driver tends to grab the wrong device and clash with your screen reader).
|
||||
**Install RemSound on this PC …** (or **Uninstall …**)| Alt+O, I (Alt+O, U once installed)| Turns the copy you're running into a properly installed Windows app — on the Start menu, with a desktop shortcut, and listed in Windows’ Installed apps — or removes it again. Once it's installed, this item changes to **Uninstall RemSound from this PC**. See Installing RemSound on your PC.
|
||||
**Preferences …**| Ctrl+P, or Alt+O, P| Opens the Preferences dialog, organised into six tabs (move between them with Ctrl+Tab, or the arrow keys when the tab names have focus): **General** — the profiles folder, accept remote volume commands, and UPnP router opening; **Appearance** — **Colour theme (Alt+T)** (Match Windows / Light / Dark; changes only how the window looks, takes effect next launch, no effect on the screen reader), “Show the volume, pan and EQ for peers tab” (on by default; hides the Volume, pan and EQ for peers tab if you untick it), **Tab order (Alt+O)** — a list of the window's tabs with **Move up (Alt+U)** and **Move down (Alt+N)** buttons to reorder them, and two toggles to **enable the Discovered (Alt+D)** and **Remembered (Alt+R)** peer lists on the Connectivity tab (both on by default — untick one to hide that list); **Audio cues** — the cue list and its sounds (see Audio cue sounds); **Startup behaviour** — start minimised / with Windows / with a specific profile; **Update settings** — the update checks and install options; and **Logging** — enable logs, write logs now, and the log-folder housekeeping (see Logs and diagnostics). Esc or the Close button dismisses it.
|
||||
|
||||
### Installing RemSound on your PC
|
||||
|
||||
RemSound normally runs “portable” — you unzip it and run it straight from whatever folder you unzipped it to, and it keeps its settings, profiles and recordings inside that same folder. That works perfectly well and you never have to install anything. But if you'd rather have RemSound set up as a proper Windows app — on the Start menu, with a desktop shortcut, and listed in Windows’ _Installed apps_ — open **Options → Install RemSound on this PC** (Alt+O, I).
|
||||
|
||||
It installs into your own user area (`…\AppData\Local\Programs\RemSound`), so it **never asks for administrator rights** and only affects your account. A dialog lets you choose what to set up. Tab through the tick-boxes, then select **Install** (pressing Enter on a tick-box won't skip ahead — you reach the Install button by tabbing to it):
|
||||
|
||||
* **Create a desktop shortcut** — on by default.
|
||||
* **Add to the Start menu** — makes a “RemSound” folder on the Start menu holding three shortcuts: the program, this manual, and an uninstall shortcut. On by default.
|
||||
* **Run RemSound when I sign in to Windows** — the same login auto-start as the Startup behaviour tab. Off unless you already had it turned on.
|
||||
* **Copy my profiles and settings across** — brings your profiles and every setting (keyboard shortcuts, named peers, colour theme, and the rest) from the copy you're running into the install folder. On by default.
|
||||
* **Copy my recordings across** — on by default.
|
||||
* **Copy my logs across** — off by default; you rarely need old logs in the installed copy.
|
||||
|
||||
|
||||
|
||||
When you select Install, RemSound copies itself into place and then **closes and reopens from the installed location**. A confirmation dialog tells you this is about to happen before it does. From then on your settings, profiles and recordings live inside the installed folder, exactly as they did in the portable copy, and RemSound still updates itself in place as usual.
|
||||
|
||||
**Uninstalling.** Once RemSound is installed, that same menu item becomes **Uninstall RemSound from this PC** (Alt+O, U). You can also uninstall from the “Uninstall RemSound” shortcut in its Start-menu folder, or from Windows’ _Installed apps_ list — all three do exactly the same thing. RemSound asks you to confirm first, with two tick-boxes — **Remove profiles, config and logs** and **Remove recordings** — both **off by default** , so unless you tick them your own files are kept even after the program itself is removed. A short message confirms once it's done.
|
||||
|
||||
### Help menu
|
||||
|
||||
The Help menu opens this manual, checks for updates, and shows the About dialog.
|
||||
|
||||
+16
-27
@@ -1,49 +1,38 @@
|
||||
# RemSound v5.0
|
||||
# RemSound v5.1
|
||||
|
||||
A big release. Shape each person you're connected to, record everyone onto separate tracks, give your peers names that stick, and make the window your own with themes and reorderable tabs.
|
||||
Install RemSound as a proper Windows app — or keep running it portable, whichever you prefer.
|
||||
|
||||
## Shape each peer — volume, pan and EQ
|
||||
## Install RemSound on this PC
|
||||
|
||||
A new **Volume, pan and EQ for peers** tab lets you treat every connected person as their own channel:
|
||||
A new **Options → Install RemSound on this PC** turns the copy you're running into a properly installed Windows app:
|
||||
|
||||
- **Volume** — an individual level for each person, so you can balance everyone against each other.
|
||||
- **Pan** — lean someone to the left or right. It keeps their stereo image; it never folds them down to mono.
|
||||
- **EQ** — change each person's tone with one of three equalisers: a simple **3-band** tone control, a **12-band graphic** EQ (31 Hz up to 16 kHz), or a **16-band parametric** EQ where you place each band yourself by typing a start frequency, an end frequency and a gain. On the parametric bands list, left and right arrow nudge the selected band by half a dB, live.
|
||||
- It sets RemSound up on the **Start menu** (with the program, the manual and an uninstall shortcut), adds a **desktop shortcut**, and lists it in Windows' **Installed apps**.
|
||||
- It installs into your own user area, so it **never asks for administrator rights** and only affects your account.
|
||||
- A tick-box dialog lets you choose what to set up — desktop and Start-menu shortcuts, run at startup — and bring your **profiles and settings, recordings and logs** across from the copy you're running.
|
||||
- When it's done it reopens from the installed location; everything carries on exactly as before, and RemSound still updates itself in place as usual.
|
||||
|
||||
Everything is live — you hear it as you move a control — and it adds no delay. One master switch turns it all on or off (there's a global shortcut for that too), and each person has a tick so you can bypass one without losing their settings. Crucially, **what you dial in is captured in your recordings**, so a shaped mix records the way it sounds (with a "record the raw audio" option if you'd rather keep it clean).
|
||||
Once it's installed, that same menu item becomes **Uninstall RemSound from this PC** (with matching entries in the Start menu and Windows' Installed apps). Uninstalling asks first, with two tick-boxes — **remove profiles, config and logs**, and **remove recordings** — both off by default, so your own files are kept unless you say otherwise.
|
||||
|
||||
## Record everyone on their own track
|
||||
You never *have* to install: running RemSound straight from the unzipped folder works exactly as it always has.
|
||||
|
||||
Recording can now **split into a separate file per peer** — each connected person on their own track — so you can take a jam or a conversation away and mix the parts afterwards. The default recording source is now "both sent and received".
|
||||
## Smaller download
|
||||
|
||||
## Name your peers, and see who's who
|
||||
|
||||
- **Rename peer (Alt+M or F2)** on the Connectivity tab gives someone a friendly name that sticks to their machine for good — across restarts, address changes and networks — and shows everywhere that peer appears.
|
||||
- **Peer details (Alt+E)** shows the highlighted peer's name, address, how long you've been connected, the link health and ping, what they're sending (how many devices, WASAPI or ASIO, sample rate and codec), and whether they're receiving your audio.
|
||||
- **Options → Manage named peers** lists everyone you've named, with where and when you last connected, so you can rename or delete them in one place.
|
||||
|
||||
## Make it yours — Appearance
|
||||
|
||||
Preferences has a new **Appearance** tab:
|
||||
|
||||
- **Colour theme** — Match Windows (the default), Light, or Dark. RemSound now follows your Windows light/dark setting, has a modern font and its own app icon.
|
||||
- **Tab order** — reorder the main window's tabs with Move up / Move down, and hide the ones you don't use (including the volume/pan/EQ tab and the Discovered / Remembered peer lists on the Connectivity tab).
|
||||
- **Ctrl and a number** jumps straight to a tab by its position, following whatever order you've set.
|
||||
The built-in cue sounds have been slimmed down, so RemSound is a smaller download and takes up less space.
|
||||
|
||||
## Compatibility
|
||||
|
||||
**v5.0 talks to v3.3 through v4.9 with no trouble** — the over-the-network format is unchanged, so you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
||||
The over-the-network format is unchanged, so **v5.1 talks to v3.3 through v5.0** with no trouble — you don't have to update both ends at once. (Everyone still needs **v3.3 or newer**, where end-to-end encryption came in.)
|
||||
|
||||
## Install
|
||||
|
||||
1. Download `RemSound-v5.0.zip` from this release.
|
||||
1. Download `RemSound-v5.1.zip` from this release.
|
||||
2. Close RemSound.
|
||||
3. Extract the zip **over your existing RemSound folder**, overwriting program files when prompted. The zip is program files only — it won't touch your settings, profiles, logs or recordings.
|
||||
4. Run `RemSound.exe`.
|
||||
4. Run `RemSound.exe`. (If you'd like it set up as a proper Windows app, use **Options → Install RemSound on this PC**.)
|
||||
|
||||
## Upgrading
|
||||
|
||||
**From v3.6 or newer:** Help → Check for updates installs v5.0 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
||||
**From v3.6 or newer:** Help → Check for updates installs v5.1 with the in-app updater — and if it can't finish, it puts your old version back exactly as it was.
|
||||
|
||||
**From v1.9–v3.5:** Check for updates works, but uses your current version's older updater for this one hop. If auto-update has been failing on your machine, install by hand using the steps above.
|
||||
|
||||
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
+20
@@ -316,9 +316,29 @@ ul, ol { padding-left: 1.4em; }
|
||||
<tr><td><strong>Manage named peers…</strong></td><td>Alt+O, N</td><td>Opens a list of every peer you've given a friendly name to, showing each one's machine name and where and when you last connected to it. Pick one and <strong>Rename (Alt+R</strong>, or F2) to change its name, or <strong>Delete (Alt+D</strong>, or the Del key) to forget it — deleting only drops the name, the peer still connects as normal under its machine name. See <a href="#tab-connectivity">the peer-naming notes on the Connectivity tab</a>.</td></tr>
|
||||
<tr><td><strong>Reset the default audio device prompt</strong></td><td>Alt+O, R</td><td>Brings back the “use only the default audio device?” question if you previously ticked “Don't ask me this again” on it. See <a href="#tab-io">Following the Windows default audio device</a>.</td></tr>
|
||||
<tr><td><strong>Enable / Disable Realtek ASIO</strong></td><td>—</td><td>Only shown if a Realtek ASIO driver is installed. Lets you reverse the choice RemSound offered about disabling that driver (Realtek's generic ASIO driver tends to grab the wrong device and clash with your screen reader).</td></tr>
|
||||
<tr><td><strong>Install RemSound on this PC…</strong> (or <strong>Uninstall…</strong>)</td><td>Alt+O, I (Alt+O, U once installed)</td><td>Turns the copy you're running into a properly installed Windows app — on the Start menu, with a desktop shortcut, and listed in Windows’ Installed apps — or removes it again. Once it's installed, this item changes to <strong>Uninstall RemSound from this PC</strong>. See <a href="#install">Installing RemSound on your PC</a>.</td></tr>
|
||||
<tr><td><strong>Preferences…</strong></td><td>Ctrl+P, or Alt+O, P</td><td>Opens the Preferences dialog, organised into six tabs (move between them with Ctrl+Tab, or the arrow keys when the tab names have focus): <strong>General</strong> — the profiles folder, accept remote volume commands, and UPnP router opening; <strong>Appearance</strong> — <strong>Colour theme (Alt+T)</strong> (Match Windows / Light / Dark; changes only how the window looks, takes effect next launch, no effect on the screen reader), “Show the volume, pan and EQ for peers tab” (on by default; hides the <a href="#pan-eq">Volume, pan and EQ for peers tab</a> if you untick it), <strong>Tab order (Alt+O)</strong> — a list of the window's tabs with <strong>Move up (Alt+U)</strong> and <strong>Move down (Alt+N)</strong> buttons to reorder them, and two toggles to <strong>enable the Discovered (Alt+D)</strong> and <strong>Remembered (Alt+R)</strong> peer lists on the Connectivity tab (both on by default — untick one to hide that list); <strong>Audio cues</strong> — the cue list and its sounds (see <a href="#audio-cues">Audio cue sounds</a>); <strong>Startup behaviour</strong> — start minimised / with Windows / with a specific profile; <strong>Update settings</strong> — the update checks and install options; and <strong>Logging</strong> — enable logs, write logs now, and the log-folder housekeeping (see <a href="#logs">Logs and diagnostics</a>). Esc or the Close button dismisses it.</td></tr>
|
||||
</table>
|
||||
|
||||
<h3 id="install">Installing RemSound on your PC</h3>
|
||||
|
||||
<p>RemSound normally runs “portable” — you unzip it and run it straight from whatever folder you unzipped it to, and it keeps its settings, profiles and recordings inside that same folder. That works perfectly well and you never have to install anything. But if you'd rather have RemSound set up as a proper Windows app — on the Start menu, with a desktop shortcut, and listed in Windows’ <em>Installed apps</em> — open <strong>Options → Install RemSound on this PC</strong> (Alt+O, I).</p>
|
||||
|
||||
<p>It installs into your own user area (<code>…\AppData\Local\Programs\RemSound</code>), so it <strong>never asks for administrator rights</strong> and only affects your account. A dialog lets you choose what to set up. Tab through the tick-boxes, then select <strong>Install</strong> (pressing Enter on a tick-box won't skip ahead — you reach the Install button by tabbing to it):</p>
|
||||
|
||||
<ul>
|
||||
<li><strong>Create a desktop shortcut</strong> — on by default.</li>
|
||||
<li><strong>Add to the Start menu</strong> — makes a “RemSound” folder on the Start menu holding three shortcuts: the program, this manual, and an uninstall shortcut. On by default.</li>
|
||||
<li><strong>Run RemSound when I sign in to Windows</strong> — the same login auto-start as the <a href="#startup">Startup behaviour</a> tab. Off unless you already had it turned on.</li>
|
||||
<li><strong>Copy my profiles and settings across</strong> — brings your profiles and every setting (keyboard shortcuts, named peers, colour theme, and the rest) from the copy you're running into the install folder. On by default.</li>
|
||||
<li><strong>Copy my recordings across</strong> — on by default.</li>
|
||||
<li><strong>Copy my logs across</strong> — off by default; you rarely need old logs in the installed copy.</li>
|
||||
</ul>
|
||||
|
||||
<p>When you select Install, RemSound copies itself into place and then <strong>closes and reopens from the installed location</strong>. A confirmation dialog tells you this is about to happen before it does. From then on your settings, profiles and recordings live inside the installed folder, exactly as they did in the portable copy, and RemSound still updates itself in place as usual.</p>
|
||||
|
||||
<p><strong>Uninstalling.</strong> Once RemSound is installed, that same menu item becomes <strong>Uninstall RemSound from this PC</strong> (Alt+O, U). You can also uninstall from the “Uninstall RemSound” shortcut in its Start-menu folder, or from Windows’ <em>Installed apps</em> list — all three do exactly the same thing. RemSound asks you to confirm first, with two tick-boxes — <strong>Remove profiles, config and logs</strong> and <strong>Remove recordings</strong> — both <strong>off by default</strong>, so unless you tick them your own files are kept even after the program itself is removed. A short message confirms once it's done.</p>
|
||||
|
||||
<h3>Help menu</h3>
|
||||
|
||||
<p>The Help menu opens this manual, checks for updates, and shows the About dialog.</p>
|
||||
|
||||
@@ -20,6 +20,14 @@ internal sealed class AboutDialog : Form
|
||||
/// updates" path.</summary>
|
||||
private const string ReleaseNotes =
|
||||
"""
|
||||
RemSound v5.1
|
||||
|
||||
Install RemSound as a proper Windows app.
|
||||
|
||||
A new "Install RemSound on this PC" in the Options menu turns the copy you're running into a properly installed app — on the Start menu, with a desktop shortcut, and listed in Windows' Installed apps. It installs just for you, so it never needs administrator rights, and you can bring your profiles, settings, recordings and logs across as you go. Once it's installed, that same menu item becomes Uninstall, which removes it again and asks first whether to keep or delete your own files. You never have to install — running RemSound straight from the unzipped folder works exactly as before.
|
||||
|
||||
Smaller download too, now the built-in sounds have been slimmed down.
|
||||
|
||||
RemSound v5.0
|
||||
|
||||
A big one. Shape each person, record everyone separately, name your peers, and make the window your own.
|
||||
|
||||
@@ -0,0 +1,759 @@
|
||||
using System.Diagnostics;
|
||||
using System.Drawing;
|
||||
using System.Text;
|
||||
using Microsoft.Win32;
|
||||
using RemSound.Core;
|
||||
|
||||
namespace RemSound.App;
|
||||
|
||||
/// <summary>
|
||||
/// "Install RemSound on this PC" — promotes a portable copy (a folder the user extracted from the
|
||||
/// zip and runs from Downloads / a USB stick / wherever) into a proper per-user install under
|
||||
/// <c>%LOCALAPPDATA%\Programs\RemSound</c>, with optional desktop and Start-menu shortcuts, a
|
||||
/// Windows "Installed apps" entry, and login auto-start. Modelled on Andre's Sensor Readout
|
||||
/// self-installer.
|
||||
///
|
||||
/// Design choices, all deliberate:
|
||||
/// * <b>Per-user location</b> (<c>%LOCALAPPDATA%\Programs</c>), never Program Files. No admin /
|
||||
/// UAC prompt, and the silent auto-updater keeps working because it can write its own folder.
|
||||
/// * <b>No separate installer .exe / script.</b> A screen-reader user never has to hunt down and
|
||||
/// run a loose file — it's a menu item inside the app they already have open.
|
||||
/// * <b>The whole folder is the install.</b> RemSound reads its settings/profiles/logs relative to
|
||||
/// its own exe (see <see cref="AppConfig.UserDataDirectory"/>), so once it's running from the
|
||||
/// install folder it finds everything there automatically — no AppData lookup, no code changes.
|
||||
/// The installer just copies the program files (always) plus, per the user's tick-boxes, their
|
||||
/// "user settings and logs" data and their recordings into the same layout inside the target.
|
||||
/// * <b>Batch uninstaller, not PowerShell.</b> A running exe can't delete its own folder, so a
|
||||
/// tiny <c>.cmd</c> waits for RemSound to close then removes the folder. Batch (not a .ps1) so a
|
||||
/// locked-down PowerShell execution policy can never block an uninstall.
|
||||
///
|
||||
/// Entry points: <see cref="RunInstall"/> (Options → Install…), <see cref="RunUninstallInProcess"/>
|
||||
/// (Options → Uninstall… while running the installed copy) and <see cref="RunUninstallStandalone"/>
|
||||
/// (the <c>--uninstall</c> switch behind the Start-menu / Installed-apps "Uninstall" entry).
|
||||
/// </summary>
|
||||
internal static class AppInstaller
|
||||
{
|
||||
private const string AppName = "RemSound";
|
||||
private const string ExeName = "RemSound.exe";
|
||||
private const string ManualName = "readme.html";
|
||||
|
||||
/// <summary>Windows "Installed apps" registration lives under the per-user hive (no admin).</summary>
|
||||
private const string UninstallRegistryPath =
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Uninstall\RemSound";
|
||||
|
||||
/// <summary>Subfolders/entries beside the exe that are USER DATA, not program files — copied only
|
||||
/// when the matching tick-box is set, never as part of the always-copied program files.</summary>
|
||||
private static readonly string[] UserDataEntries =
|
||||
{
|
||||
AppConfig.UserDataFolderName, // "user settings and logs" (config + profiles + logs)
|
||||
"recordings",
|
||||
};
|
||||
|
||||
/// <summary>Filename of the marker RemSound drops beside its exe when it installs itself. Its
|
||||
/// PRESENCE is the authority for "am I an installed copy" — an explicit token the installer writes,
|
||||
/// NOT a guess based on which folder the exe happens to sit in. Inferring install-state from the
|
||||
/// path (does my folder equal the computed install folder?) is fragile: it breaks if the install
|
||||
/// location ever moves, if the path resolves differently, or if the folder is relocated. A fresh
|
||||
/// copy unzipped from the release has no marker, so it correctly knows it's portable.</summary>
|
||||
private const string InstalledMarkerName = "installed.marker";
|
||||
|
||||
/// <summary>The fixed per-user install location: <c>%LOCALAPPDATA%\Programs\RemSound</c>.</summary>
|
||||
public static string InstallFolder
|
||||
{
|
||||
get
|
||||
{
|
||||
var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData);
|
||||
if (string.IsNullOrWhiteSpace(localAppData))
|
||||
{
|
||||
localAppData = Path.Combine(
|
||||
Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), "AppData", "Local");
|
||||
}
|
||||
return Path.Combine(localAppData, "Programs", AppName);
|
||||
}
|
||||
}
|
||||
|
||||
private static string MarkerPath(string folder) => Path.Combine(folder, InstalledMarkerName);
|
||||
|
||||
/// <summary>True when THIS running copy is an installed one — decided purely by whether the
|
||||
/// installer's marker file sits beside this exe, never by comparing folder paths. The Options menu
|
||||
/// uses it to show "Uninstall…" instead of "Install…".</summary>
|
||||
public static bool IsInstalledCopy => File.Exists(MarkerPath(AppContext.BaseDirectory));
|
||||
|
||||
/// <summary>True when an install already exists at the target location (a re-run becomes an
|
||||
/// update) — read from the destination's marker.</summary>
|
||||
private static bool InstallExistsAtTarget => File.Exists(MarkerPath(InstallFolder));
|
||||
|
||||
// ---------------- install ----------------
|
||||
|
||||
/// <summary>Options → "Install RemSound on this PC". Shows the tick-box dialog, copies the files,
|
||||
/// wires up shortcuts/registration/auto-start, then relaunches the installed copy and asks the
|
||||
/// portable one to exit. <paramref name="log"/> (may be null) records milestones to the diagnostic
|
||||
/// log when logging is on.</summary>
|
||||
public static void RunInstall(IWin32Window owner, Action<string>? log = null)
|
||||
{
|
||||
var source = NormalizeFolder(AppContext.BaseDirectory);
|
||||
var target = NormalizeFolder(InstallFolder);
|
||||
|
||||
if (IsInstalledCopy)
|
||||
{
|
||||
MessageBox.Show(owner,
|
||||
"This copy of RemSound is already installed, so there's nothing to do. " +
|
||||
"To remove it, use Uninstall RemSound from this PC.",
|
||||
"Already installed", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var updating = InstallExistsAtTarget;
|
||||
var options = ShowInstallOptionsDialog(owner, target, updating);
|
||||
if (options is null) return; // cancelled
|
||||
|
||||
var installedExe = Path.Combine(target, ExeName);
|
||||
try
|
||||
{
|
||||
log?.Invoke($"install: starting {(updating ? "update" : "fresh install")} to \"{target}\" " +
|
||||
$"(desktop={options.CreateDesktopShortcut}, startMenu={options.CreateStartMenuFolder}, " +
|
||||
$"startup={options.RunAtStartup}, profiles={options.CopyProfilesAndConfig}, " +
|
||||
$"recordings={options.CopyRecordings}, logs={options.CopyLogs})");
|
||||
|
||||
// The file copy is the part that must succeed — a failure here (disk full, permissions)
|
||||
// aborts with the copy untouched. Directory.CreateDirectory + copy.
|
||||
Directory.CreateDirectory(target);
|
||||
CopyProgramFiles(source, target);
|
||||
CopyUserData(source, target, options);
|
||||
|
||||
// Drop the marker that tells the installed copy it IS installed (so the Options menu shows
|
||||
// Uninstall). Part of the must-succeed path: without it the install wouldn't recognise
|
||||
// itself. Written last, after the files are safely in place.
|
||||
File.WriteAllText(MarkerPath(target),
|
||||
$"RemSound {CommandLine.AppVersion} was installed here by the in-app installer." + Environment.NewLine +
|
||||
"RemSound checks for this file to know it is installed rather than portable." + Environment.NewLine +
|
||||
"Removing this file makes RemSound treat this copy as portable again.", new UTF8Encoding(false));
|
||||
|
||||
// Everything below is best-effort desktop integration: a hiccup in one (e.g. a policy
|
||||
// blocking a shortcut) is logged but must NOT undo a good install — the app is already
|
||||
// sitting installed, and the user can add a shortcut by hand.
|
||||
TryStep(log, "desktop shortcut", () => SetDesktopShortcut(options.CreateDesktopShortcut, installedExe, target));
|
||||
TryStep(log, "Start menu folder", () => SetStartMenuFolder(options.CreateStartMenuFolder, installedExe, target));
|
||||
// Re-point (or clear) login auto-start so it launches the INSTALLED exe, not the portable
|
||||
// one the user may delete. Either way the entry now reflects the tick-box and the new path.
|
||||
TryStep(log, "run at startup", () =>
|
||||
{
|
||||
if (options.RunAtStartup) StartupAutoStart.TryEnable(installedExe);
|
||||
else StartupAutoStart.TryDisable();
|
||||
});
|
||||
TryStep(log, "register in Installed apps", () => RegisterInstalledApp(target, installedExe));
|
||||
|
||||
log?.Invoke("install: files copied and shortcuts written; relaunching installed copy");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke($"install: FAILED — {ex.GetType().Name}: {ex.Message}");
|
||||
MessageBox.Show(owner,
|
||||
"RemSound could not finish installing:" + Environment.NewLine + Environment.NewLine + ex.Message +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"Nothing was changed to the copy you're running now — you can keep using it.",
|
||||
"Install failed", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBox.Show(owner,
|
||||
$"RemSound is now installed on this PC, in:{Environment.NewLine}{target}{Environment.NewLine}{Environment.NewLine}" +
|
||||
"Press OK to finish. RemSound will now close and reopen from the new install location.",
|
||||
"RemSound installed", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
|
||||
// Hand over cleanly. We can't just launch the installed exe and exit: the single-instance
|
||||
// lock would still be held for the instant it takes us to shut down, and the new copy would
|
||||
// see "already running". So a tiny batch waits for THIS process to exit (lock released), then
|
||||
// starts the installed copy. Then we exit decisively.
|
||||
try { StartRelaunchAfterExit(installedExe, target); } catch { /* fall through — worst case the user starts it from the shortcut */ }
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
// ---------------- uninstall ----------------
|
||||
|
||||
/// <summary>Options → "Uninstall RemSound from this PC", chosen from inside the running installed
|
||||
/// copy. THIS process holds the files open, so we confirm, tear down shortcuts/registration/
|
||||
/// auto-start, launch the wait-then-delete batch, and exit so it can remove the folder.</summary>
|
||||
public static void RunUninstallInProcess(IWin32Window owner, Action<string>? log = null)
|
||||
{
|
||||
// The folder we're running from IS the install (the marker confirms it) — delete that, rather
|
||||
// than a separately computed path, so uninstall never targets the wrong place.
|
||||
var target = NormalizeFolder(AppContext.BaseDirectory);
|
||||
if (!IsInstalledCopy)
|
||||
{
|
||||
MessageBox.Show(owner,
|
||||
"This copy of RemSound isn't an installed one, so there's nothing to uninstall from here. " +
|
||||
"You can just delete this folder.",
|
||||
"Not an installed copy", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = ShowUninstallConfirmDialog(owner, target);
|
||||
if (options is null) return; // cancelled
|
||||
|
||||
log?.Invoke($"uninstall: starting (removeProfilesConfigLogs={options.RemoveProfilesConfigLogs}, " +
|
||||
$"removeRecordings={options.RemoveRecordings})");
|
||||
TearDownIntegration();
|
||||
try { StartDeleteAfterExit(target, options.RemoveProfilesConfigLogs, options.RemoveRecordings); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
log?.Invoke($"uninstall: could not start remover — {ex.Message}");
|
||||
MessageBox.Show(owner,
|
||||
"RemSound couldn't start the uninstaller:" + Environment.NewLine + Environment.NewLine + ex.Message,
|
||||
"Uninstall failed", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
// Uninstall doesn't relaunch — just tell the user it's done, then close (the remover finishes
|
||||
// deleting the folder the moment this process exits).
|
||||
MessageBox.Show(owner,
|
||||
"RemSound has been removed from this PC. Press OK to finish.",
|
||||
"RemSound uninstalled", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
Environment.Exit(0);
|
||||
}
|
||||
|
||||
/// <summary>The <c>--uninstall</c> switch: run from the Start-menu "Uninstall" shortcut or Windows
|
||||
/// "Installed apps". A separate installed copy may still be running, so we confirm, force-close any
|
||||
/// running RemSound, tear down integration, and start the wait-then-delete batch. Called very early
|
||||
/// in <see cref="Program"/>, before the single-instance guard, and always ends the process.</summary>
|
||||
public static void RunUninstallStandalone()
|
||||
{
|
||||
// Reached only from the installed exe (the Start-menu / Installed-apps Uninstall entries run
|
||||
// "<installed exe> --uninstall"), so the folder we're in is the real install — gated by the
|
||||
// marker so a stray "--uninstall" on a portable copy does nothing.
|
||||
var target = NormalizeFolder(AppContext.BaseDirectory);
|
||||
if (!IsInstalledCopy)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"This copy of RemSound isn't an installed one, so there's nothing to remove.",
|
||||
"Nothing to uninstall", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
return;
|
||||
}
|
||||
|
||||
var options = ShowUninstallConfirmDialog(null, target);
|
||||
if (options is null) return; // cancelled
|
||||
|
||||
// Close any running RemSound (the installed copy) so its files unlock; the remover also retries,
|
||||
// so exact timing doesn't matter.
|
||||
try { SingleInstanceCoordinator.ForceCloseOtherInstances(); } catch { /* remover retries regardless */ }
|
||||
TearDownIntegration();
|
||||
try { StartDeleteAfterExit(target, options.RemoveProfilesConfigLogs, options.RemoveRecordings); }
|
||||
catch (Exception ex)
|
||||
{
|
||||
MessageBox.Show(
|
||||
"RemSound couldn't start the uninstaller:" + Environment.NewLine + Environment.NewLine + ex.Message,
|
||||
"Uninstall failed", MessageBoxButtons.OK, MessageBoxIcon.Warning);
|
||||
return;
|
||||
}
|
||||
|
||||
MessageBox.Show(
|
||||
"RemSound has been removed from this PC. Press OK to finish.",
|
||||
"RemSound uninstalled", MessageBoxButtons.OK, MessageBoxIcon.Information);
|
||||
}
|
||||
|
||||
/// <summary>Remove everything the install put OUTSIDE its own folder — desktop + Start-menu
|
||||
/// shortcuts, the login auto-start entry, and the Windows "Installed apps" registration. The
|
||||
/// folder itself is deleted by the batch remover after RemSound closes.</summary>
|
||||
private static void TearDownIntegration()
|
||||
{
|
||||
try { SetDesktopShortcut(false, "", ""); } catch { }
|
||||
try { SetStartMenuFolder(false, "", ""); } catch { }
|
||||
try { StartupAutoStart.TryDisable(); } catch { }
|
||||
try { UnregisterInstalledApp(); } catch { }
|
||||
}
|
||||
|
||||
// ---------------- file copy ----------------
|
||||
|
||||
/// <summary>Copy the PROGRAM files (exe, DLLs, native runtimes, default sounds, manual, Tolk, …) —
|
||||
/// everything beside the exe EXCEPT the user-data entries, which are handled separately so the
|
||||
/// tick-boxes can include or exclude them. Overwrites, so a re-run cleanly updates/repairs.</summary>
|
||||
private static void CopyProgramFiles(string source, string target)
|
||||
{
|
||||
foreach (var file in Directory.GetFiles(source))
|
||||
{
|
||||
// Never carry a stray install marker across — the installer writes a fresh one into the
|
||||
// target itself, so a source copy's marker must not leak in and mislabel things.
|
||||
if (string.Equals(Path.GetFileName(file), InstalledMarkerName, StringComparison.OrdinalIgnoreCase))
|
||||
continue;
|
||||
File.Copy(file, Path.Combine(target, Path.GetFileName(file)), overwrite: true);
|
||||
}
|
||||
|
||||
foreach (var dir in Directory.GetDirectories(source))
|
||||
{
|
||||
var name = Path.GetFileName(dir);
|
||||
if (UserDataEntries.Any(e => string.Equals(e, name, StringComparison.OrdinalIgnoreCase)))
|
||||
continue; // user data — copied conditionally below
|
||||
CopyDirectory(dir, Path.Combine(target, name));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Copy the user's own data into the same layout inside the install folder, per the
|
||||
/// tick-boxes: profiles + the global config file, recordings, and logs — each independently.</summary>
|
||||
private static void CopyUserData(string source, string target, InstallOptions options)
|
||||
{
|
||||
var srcData = Path.Combine(source, AppConfig.UserDataFolderName);
|
||||
var dstData = Path.Combine(target, AppConfig.UserDataFolderName);
|
||||
|
||||
if (options.CopyProfilesAndConfig)
|
||||
{
|
||||
var srcProfiles = Path.Combine(srcData, "profiles");
|
||||
if (Directory.Exists(srcProfiles)) CopyDirectory(srcProfiles, Path.Combine(dstData, "profiles"));
|
||||
var srcConfig = Path.Combine(srcData, "global config.json");
|
||||
if (File.Exists(srcConfig))
|
||||
{
|
||||
Directory.CreateDirectory(dstData);
|
||||
File.Copy(srcConfig, Path.Combine(dstData, "global config.json"), overwrite: true);
|
||||
}
|
||||
}
|
||||
|
||||
if (options.CopyLogs)
|
||||
{
|
||||
var srcLogs = Path.Combine(srcData, "logs");
|
||||
if (Directory.Exists(srcLogs)) CopyDirectory(srcLogs, Path.Combine(dstData, "logs"));
|
||||
}
|
||||
|
||||
if (options.CopyRecordings)
|
||||
{
|
||||
var srcRec = Path.Combine(source, "recordings");
|
||||
if (Directory.Exists(srcRec)) CopyDirectory(srcRec, Path.Combine(target, "recordings"));
|
||||
}
|
||||
}
|
||||
|
||||
private static void CopyDirectory(string source, string dest)
|
||||
{
|
||||
Directory.CreateDirectory(dest);
|
||||
foreach (var file in Directory.GetFiles(source))
|
||||
File.Copy(file, Path.Combine(dest, Path.GetFileName(file)), overwrite: true);
|
||||
foreach (var dir in Directory.GetDirectories(source))
|
||||
CopyDirectory(dir, Path.Combine(dest, Path.GetFileName(dir)));
|
||||
}
|
||||
|
||||
// ---------------- shortcuts ----------------
|
||||
|
||||
private static string DesktopShortcutPath =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.DesktopDirectory), AppName + ".lnk");
|
||||
|
||||
/// <summary>The per-user Start-menu Programs folder that holds RemSound's shortcuts group.</summary>
|
||||
private static string StartMenuFolderPath =>
|
||||
Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Programs), AppName);
|
||||
|
||||
private static void SetDesktopShortcut(bool enabled, string targetExe, string workingDir)
|
||||
{
|
||||
if (!enabled)
|
||||
{
|
||||
if (File.Exists(DesktopShortcutPath)) File.Delete(DesktopShortcutPath);
|
||||
return;
|
||||
}
|
||||
CreateShortcut(DesktopShortcutPath, targetExe, "", workingDir, "RemSound low-latency audio", targetExe);
|
||||
}
|
||||
|
||||
/// <summary>Create (or remove) the Start-menu "RemSound" folder with three shortcuts: the program,
|
||||
/// the manual, and Uninstall — the classic "installed app" grouping the user asked for.</summary>
|
||||
private static void SetStartMenuFolder(bool enabled, string targetExe, string workingDir)
|
||||
{
|
||||
var folder = StartMenuFolderPath;
|
||||
if (!enabled)
|
||||
{
|
||||
try { if (Directory.Exists(folder)) Directory.Delete(folder, recursive: true); } catch { }
|
||||
return;
|
||||
}
|
||||
|
||||
Directory.CreateDirectory(folder);
|
||||
CreateShortcut(Path.Combine(folder, AppName + ".lnk"),
|
||||
targetExe, "", workingDir, "RemSound low-latency audio", targetExe);
|
||||
|
||||
var manual = Path.Combine(workingDir, ManualName);
|
||||
if (File.Exists(manual))
|
||||
CreateShortcut(Path.Combine(folder, "RemSound Manual.lnk"),
|
||||
manual, "", workingDir, "Open the RemSound manual", targetExe);
|
||||
|
||||
CreateShortcut(Path.Combine(folder, "Uninstall RemSound.lnk"),
|
||||
targetExe, "--uninstall", workingDir, "Uninstall RemSound from this PC", targetExe);
|
||||
}
|
||||
|
||||
/// <summary>Write a .lnk using the Windows Script Host shell object via late-bound COM, so there's
|
||||
/// no extra library to ship and no IShellLink P/Invoke to maintain. Best-effort per shortcut.</summary>
|
||||
private static void CreateShortcut(string linkPath, string targetPath, string arguments,
|
||||
string workingDir, string description, string iconSourceExe)
|
||||
{
|
||||
var dir = Path.GetDirectoryName(linkPath);
|
||||
if (!string.IsNullOrWhiteSpace(dir)) Directory.CreateDirectory(dir);
|
||||
if (File.Exists(linkPath)) File.Delete(linkPath);
|
||||
|
||||
var shellType = Type.GetTypeFromProgID("WScript.Shell");
|
||||
if (shellType is null) return;
|
||||
dynamic? shell = null;
|
||||
try
|
||||
{
|
||||
shell = Activator.CreateInstance(shellType);
|
||||
dynamic link = shell!.CreateShortcut(linkPath);
|
||||
link.TargetPath = targetPath;
|
||||
link.Arguments = arguments;
|
||||
link.WorkingDirectory = workingDir;
|
||||
link.Description = description;
|
||||
if (!string.IsNullOrWhiteSpace(iconSourceExe)) link.IconLocation = iconSourceExe + ",0";
|
||||
link.Save();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (shell is not null)
|
||||
{
|
||||
try { System.Runtime.InteropServices.Marshal.FinalReleaseComObject(shell); } catch { }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------- "Installed apps" registration ----------------
|
||||
|
||||
private static void RegisterInstalledApp(string installFolder, string installedExe)
|
||||
{
|
||||
using var key = Registry.CurrentUser.CreateSubKey(UninstallRegistryPath, writable: true);
|
||||
if (key is null) return;
|
||||
key.SetValue("DisplayName", "RemSound");
|
||||
key.SetValue("DisplayVersion", CommandLine.AppVersion);
|
||||
key.SetValue("Publisher", "RemSound");
|
||||
key.SetValue("InstallLocation", installFolder);
|
||||
key.SetValue("DisplayIcon", installedExe);
|
||||
key.SetValue("UninstallString", $"\"{installedExe}\" --uninstall");
|
||||
key.SetValue("QuietUninstallString", $"\"{installedExe}\" --uninstall");
|
||||
key.SetValue("NoModify", 1, RegistryValueKind.DWord);
|
||||
key.SetValue("NoRepair", 1, RegistryValueKind.DWord);
|
||||
try { key.SetValue("EstimatedSize", (int)(FolderSizeBytes(installFolder) / 1024), RegistryValueKind.DWord); }
|
||||
catch { /* size is cosmetic in the Apps list — skip if it can't be measured */ }
|
||||
}
|
||||
|
||||
private static void UnregisterInstalledApp()
|
||||
{
|
||||
try { Registry.CurrentUser.DeleteSubKeyTree(UninstallRegistryPath, throwOnMissingSubKey: false); }
|
||||
catch { }
|
||||
}
|
||||
|
||||
private static long FolderSizeBytes(string folder)
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var f in Directory.EnumerateFiles(folder, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
try { total += new FileInfo(f).Length; } catch { }
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
// ---------------- batch handoff / removal ----------------
|
||||
|
||||
/// <summary>Launch the installed copy and hand it our foreground right. We start it DIRECTLY (not
|
||||
/// via a detached helper) while THIS process is still alive and frontmost, so we can call
|
||||
/// <c>AllowSetForegroundWindow</c> to grant the child permission to pull itself to the front once
|
||||
/// it's up — the Windows-sanctioned way. A process launched from a background helper never gets
|
||||
/// that right, which is why the window kept opening behind everything. The child is told our PID
|
||||
/// (<c>--await-pid</c>) so it waits for us to exit — releasing the single-instance lock — before it
|
||||
/// starts for real, avoiding the "already running" race.</summary>
|
||||
private static void StartRelaunchAfterExit(string installedExe, string installFolder)
|
||||
{
|
||||
var psi = new ProcessStartInfo
|
||||
{
|
||||
FileName = installedExe,
|
||||
WorkingDirectory = installFolder,
|
||||
UseShellExecute = false,
|
||||
};
|
||||
psi.ArgumentList.Add("--foreground");
|
||||
psi.ArgumentList.Add("--await-pid");
|
||||
psi.ArgumentList.Add(Environment.ProcessId.ToString());
|
||||
|
||||
var child = Process.Start(psi);
|
||||
// Grant the just-launched child the right to take the foreground. Given now, while we still
|
||||
// hold it, it survives our imminent exit and lets the child's SetForegroundWindow succeed.
|
||||
if (child is not null)
|
||||
{
|
||||
try { AllowSetForegroundWindow(child.Id); } catch { /* the child also self-nudges as a fallback */ }
|
||||
}
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool AllowSetForegroundWindow(int dwProcessId);
|
||||
|
||||
/// <summary>Spawn a hidden batch that waits for RemSound to close, then removes the install folder,
|
||||
/// retrying a few times in case a copy is slow to release its files, then deletes itself. The two
|
||||
/// flags decide independently whether the "user settings and logs" folder (profiles + config +
|
||||
/// logs) and the "recordings" folder go too; whichever aren't removed are kept in place.</summary>
|
||||
private static void StartDeleteAfterExit(string installFolder, bool removeProfilesConfigLogs, bool removeRecordings)
|
||||
{
|
||||
var pid = Environment.ProcessId;
|
||||
|
||||
// Folders to KEEP: everything else (all program files, and any data folder not being removed)
|
||||
// is deleted. When nothing is kept, the whole folder goes in one shot.
|
||||
var keep = new List<string>();
|
||||
if (!removeProfilesConfigLogs) keep.Add(AppConfig.UserDataFolderName);
|
||||
if (!removeRecordings) keep.Add("recordings");
|
||||
|
||||
string deleteBody;
|
||||
if (keep.Count == 0)
|
||||
{
|
||||
deleteBody = $"rd /s /q \"{installFolder}\"\r\n";
|
||||
}
|
||||
else
|
||||
{
|
||||
// Delete every subfolder except the kept one(s), plus all files in the root. The kept
|
||||
// folders remain, so the install folder itself is left holding just the user's data.
|
||||
var guards = string.Concat(keep.Select(k => $"if /I not \"%%~nxD\"==\"{k}\" "));
|
||||
deleteBody =
|
||||
$"for /d %%D in (\"{installFolder}\\*\") do {guards}rd /s /q \"%%D\"\r\n" +
|
||||
$"del /f /q \"{installFolder}\\*\" >nul 2>&1\r\n";
|
||||
}
|
||||
|
||||
// Retry sentinel: the exe file is the last thing to unlock, so loop until it (or the whole
|
||||
// folder) is gone. Written without parenthesised blocks so %tries% expands fresh each pass.
|
||||
var script =
|
||||
"@echo off\r\n" +
|
||||
":wait\r\n" +
|
||||
$"tasklist /FI \"PID eq {pid}\" 2>nul | find \"{pid}\" >nul\r\n" +
|
||||
"if %errorlevel%==0 ( ping -n 2 127.0.0.1 >nul & goto wait )\r\n" +
|
||||
"set tries=0\r\n" +
|
||||
":del\r\n" +
|
||||
deleteBody +
|
||||
$"if not exist \"{installFolder}\\{ExeName}\" goto done\r\n" +
|
||||
"ping -n 2 127.0.0.1 >nul\r\n" +
|
||||
"set /a tries+=1\r\n" +
|
||||
"if %tries% lss 30 goto del\r\n" +
|
||||
":done\r\n" +
|
||||
"del \"%~f0\"\r\n";
|
||||
RunHiddenBatch(script, "RemSound-uninstall-");
|
||||
}
|
||||
|
||||
private static void RunHiddenBatch(string script, string namePrefix)
|
||||
{
|
||||
var path = Path.Combine(Path.GetTempPath(), namePrefix + Guid.NewGuid().ToString("N") + ".cmd");
|
||||
File.WriteAllText(path, script, new UTF8Encoding(false));
|
||||
Process.Start(new ProcessStartInfo
|
||||
{
|
||||
FileName = "cmd.exe",
|
||||
Arguments = $"/c \"{path}\"",
|
||||
WindowStyle = ProcessWindowStyle.Hidden,
|
||||
CreateNoWindow = true,
|
||||
UseShellExecute = false,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------- dialogs ----------------
|
||||
|
||||
private sealed class InstallOptions
|
||||
{
|
||||
public bool CreateDesktopShortcut;
|
||||
public bool CreateStartMenuFolder;
|
||||
public bool RunAtStartup;
|
||||
public bool CopyProfilesAndConfig;
|
||||
public bool CopyRecordings;
|
||||
public bool CopyLogs;
|
||||
}
|
||||
|
||||
private sealed class UninstallOptions
|
||||
{
|
||||
public bool RemoveProfilesConfigLogs;
|
||||
public bool RemoveRecordings;
|
||||
}
|
||||
|
||||
/// <summary>The install tick-box dialog. Tab order is: every check-box first, THEN Install, THEN
|
||||
/// Cancel — and there is deliberately no default button, so Enter never fires Install before the
|
||||
/// user has tabbed past the options. Escape cancels.</summary>
|
||||
private static InstallOptions? ShowInstallOptionsDialog(IWin32Window owner, string target, bool updating)
|
||||
{
|
||||
using var dialog = new Form
|
||||
{
|
||||
Text = updating ? "Update the RemSound install" : "Install RemSound on this PC",
|
||||
StartPosition = FormStartPosition.CenterParent,
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
MinimizeBox = false,
|
||||
MaximizeBox = false,
|
||||
AutoSize = true,
|
||||
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||
ShowInTaskbar = false,
|
||||
};
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoSize = true,
|
||||
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||
ColumnCount = 1,
|
||||
Padding = new Padding(14),
|
||||
};
|
||||
|
||||
var heading = Theme.Heading(dialog.Text);
|
||||
|
||||
var intro = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(520, 0),
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
Text = (updating
|
||||
? "RemSound is already installed. This will update the installed copy in:"
|
||||
: "RemSound will be installed for you in:")
|
||||
+ Environment.NewLine + target + Environment.NewLine + Environment.NewLine
|
||||
+ "No administrator rights are needed. Choose what to set up, then select Install.",
|
||||
};
|
||||
|
||||
var desktopBox = MakeCheck("Create a &desktop shortcut", "Create a desktop shortcut", true);
|
||||
var startMenuBox = MakeCheck("Add to the &Start menu (program, manual and uninstall)",
|
||||
"Add a Start menu folder containing the program, the manual and an uninstall shortcut", true);
|
||||
var startupBox = MakeCheck("&Run RemSound when I sign in to Windows",
|
||||
"Run RemSound automatically when you sign in to Windows", StartupAutoStart.IsEnabled);
|
||||
var profilesBox = MakeCheck("Copy my &profiles and settings across",
|
||||
"Copy your profiles and all your settings from this copy into the install folder", true);
|
||||
var recordingsBox = MakeCheck("Copy my re&cordings across",
|
||||
"Copy your recordings from this copy into the install folder", true);
|
||||
var logsBox = MakeCheck("Copy my &logs across",
|
||||
"Copy your diagnostic logs from this copy into the install folder", false);
|
||||
|
||||
var installButton = new Button
|
||||
{
|
||||
Text = updating ? "&Update" : "&Install",
|
||||
AccessibleName = updating ? "Update" : "Install",
|
||||
DialogResult = DialogResult.OK,
|
||||
AutoSize = true,
|
||||
Margin = new Padding(3, 12, 3, 3),
|
||||
};
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "&Cancel",
|
||||
AccessibleName = "Cancel",
|
||||
DialogResult = DialogResult.Cancel,
|
||||
AutoSize = true,
|
||||
Margin = new Padding(3, 12, 3, 3),
|
||||
};
|
||||
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
FlowDirection = FlowDirection.LeftToRight,
|
||||
AutoSize = true,
|
||||
Dock = DockStyle.Fill,
|
||||
Margin = Padding.Empty,
|
||||
};
|
||||
buttons.Controls.Add(installButton);
|
||||
buttons.Controls.Add(cancelButton);
|
||||
|
||||
foreach (var c in new Control[]
|
||||
{ heading, intro, desktopBox, startMenuBox, startupBox, profilesBox, recordingsBox, logsBox, buttons })
|
||||
{
|
||||
layout.Controls.Add(c);
|
||||
}
|
||||
dialog.Controls.Add(layout);
|
||||
|
||||
// Tab order: options top-to-bottom, then Install, then Cancel. No AcceptButton, so pressing
|
||||
// Enter on a check-box does nothing — the user must tab to Install and activate it.
|
||||
var order = new Control[]
|
||||
{ desktopBox, startMenuBox, startupBox, profilesBox, recordingsBox, logsBox, installButton, cancelButton };
|
||||
for (var i = 0; i < order.Length; i++) order[i].TabIndex = i;
|
||||
dialog.CancelButton = cancelButton; // Escape cancels
|
||||
dialog.AcceptButton = null;
|
||||
|
||||
if (dialog.ShowDialog(owner) != DialogResult.OK) return null;
|
||||
return new InstallOptions
|
||||
{
|
||||
CreateDesktopShortcut = desktopBox.Checked,
|
||||
CreateStartMenuFolder = startMenuBox.Checked,
|
||||
RunAtStartup = startupBox.Checked,
|
||||
CopyProfilesAndConfig = profilesBox.Checked,
|
||||
CopyRecordings = recordingsBox.Checked,
|
||||
CopyLogs = logsBox.Checked,
|
||||
};
|
||||
}
|
||||
|
||||
// AccessibleCheckBox (not a plain CheckBox) — it re-fires the MSAA focus event on every toggle so
|
||||
// NVDA reliably announces "checked" / "not checked", including for a spacebar toggle while the box
|
||||
// already has focus, which is the state plain WinForms checkboxes leave silent on .NET 10.
|
||||
private static AccessibleCheckBox MakeCheck(string text, string accessibleName, bool @checked) => new()
|
||||
{
|
||||
Text = text,
|
||||
AccessibleName = accessibleName,
|
||||
Checked = @checked,
|
||||
AutoSize = true,
|
||||
Margin = new Padding(3, 2, 3, 2),
|
||||
};
|
||||
|
||||
/// <summary>The uninstall confirmation. Two independent opt-ins — remove profiles/config/logs, and
|
||||
/// remove recordings — both unticked by default, so the safe outcome (keep my data) is the default.
|
||||
/// OK to go ahead, Cancel to back out.</summary>
|
||||
private static UninstallOptions? ShowUninstallConfirmDialog(IWin32Window? owner, string target)
|
||||
{
|
||||
using var dialog = new Form
|
||||
{
|
||||
Text = "Uninstall RemSound from this PC",
|
||||
StartPosition = owner is null ? FormStartPosition.CenterScreen : FormStartPosition.CenterParent,
|
||||
FormBorderStyle = FormBorderStyle.FixedDialog,
|
||||
MinimizeBox = false,
|
||||
MaximizeBox = false,
|
||||
AutoSize = true,
|
||||
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||
ShowInTaskbar = false,
|
||||
TopMost = owner is null, // launched with no parent (Start menu) — make sure it's seen
|
||||
};
|
||||
|
||||
var layout = new TableLayoutPanel
|
||||
{
|
||||
Dock = DockStyle.Fill,
|
||||
AutoSize = true,
|
||||
AutoSizeMode = AutoSizeMode.GrowAndShrink,
|
||||
ColumnCount = 1,
|
||||
Padding = new Padding(14),
|
||||
};
|
||||
|
||||
var heading = Theme.Heading("Uninstall RemSound from this PC");
|
||||
|
||||
var intro = new Label
|
||||
{
|
||||
AutoSize = true,
|
||||
MaximumSize = new Size(520, 0),
|
||||
Margin = new Padding(3, 3, 3, 10),
|
||||
Text = "This will remove the installed RemSound from this PC:" + Environment.NewLine + target +
|
||||
Environment.NewLine + Environment.NewLine +
|
||||
"RemSound will close when the uninstall starts.",
|
||||
};
|
||||
var removeDataBox = MakeCheck("Remove &profiles, config and logs", "Remove profiles, config and logs", false);
|
||||
var removeRecBox = MakeCheck("Remove re&cordings", "Remove recordings", false);
|
||||
|
||||
var okButton = new Button
|
||||
{
|
||||
Text = "&OK", AccessibleName = "OK",
|
||||
DialogResult = DialogResult.OK, AutoSize = true, Margin = new Padding(3, 12, 3, 3),
|
||||
};
|
||||
var cancelButton = new Button
|
||||
{
|
||||
Text = "&Cancel", AccessibleName = "Cancel",
|
||||
DialogResult = DialogResult.Cancel, AutoSize = true, Margin = new Padding(3, 12, 3, 3),
|
||||
};
|
||||
var buttons = new FlowLayoutPanel
|
||||
{
|
||||
FlowDirection = FlowDirection.LeftToRight, AutoSize = true, Dock = DockStyle.Fill, Margin = Padding.Empty,
|
||||
};
|
||||
buttons.Controls.Add(okButton);
|
||||
buttons.Controls.Add(cancelButton);
|
||||
|
||||
foreach (var c in new Control[] { heading, intro, removeDataBox, removeRecBox, buttons }) layout.Controls.Add(c);
|
||||
dialog.Controls.Add(layout);
|
||||
|
||||
// Tab order: the two check-boxes, then OK, then Cancel. No default button, so Enter on a box
|
||||
// doesn't fire OK — the user tabs to OK deliberately. Escape cancels.
|
||||
removeDataBox.TabIndex = 0;
|
||||
removeRecBox.TabIndex = 1;
|
||||
okButton.TabIndex = 2;
|
||||
cancelButton.TabIndex = 3;
|
||||
dialog.CancelButton = cancelButton;
|
||||
dialog.AcceptButton = null;
|
||||
|
||||
var result = owner is null ? dialog.ShowDialog() : dialog.ShowDialog(owner);
|
||||
return result == DialogResult.OK
|
||||
? new UninstallOptions { RemoveProfilesConfigLogs = removeDataBox.Checked, RemoveRecordings = removeRecBox.Checked }
|
||||
: null;
|
||||
}
|
||||
|
||||
/// <summary>Run a best-effort integration step, logging (never throwing) on failure — so a blocked
|
||||
/// shortcut or registry write doesn't undo an otherwise-good install.</summary>
|
||||
private static void TryStep(Action<string>? log, string what, Action step)
|
||||
{
|
||||
try { step(); }
|
||||
catch (Exception ex) { log?.Invoke($"install: could not set up {what} — {ex.GetType().Name}: {ex.Message}"); }
|
||||
}
|
||||
|
||||
private static string NormalizeFolder(string path) =>
|
||||
Path.GetFullPath(path ?? "").TrimEnd(Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar);
|
||||
}
|
||||
@@ -200,6 +200,7 @@ internal static class CommandLine
|
||||
Console.WriteLine("Settings and control (these act, then exit):");
|
||||
Console.WriteLine(" --log on|off Turn the diagnostic log on or off.");
|
||||
Console.WriteLine(" --close Close a running copy of RemSound.");
|
||||
Console.WriteLine(" --uninstall Uninstall an installed copy of RemSound (asks first).");
|
||||
Console.WriteLine();
|
||||
Console.WriteLine("Start-up options (these change how RemSound launches):");
|
||||
Console.WriteLine(" --profile \"<name>\" Start straight into the named profile (skip the picker).");
|
||||
|
||||
@@ -1409,7 +1409,10 @@ public sealed class MainForm : Form
|
||||
// wired up before we hide the window.
|
||||
var coldStart = isFirstLaunch;
|
||||
isFirstLaunch = false;
|
||||
var minimizeThisInstance = startNextInstanceMinimized || (coldStart && AppConfig.Load().StartMinimised);
|
||||
// A post-install relaunch (--foreground) overrides any start-minimised preference — the
|
||||
// user just ran an interactive install and expects to see the installed copy come up.
|
||||
var minimizeThisInstance = !forceForegroundOnStart
|
||||
&& (startNextInstanceMinimized || (coldStart && AppConfig.Load().StartMinimised));
|
||||
startNextInstanceMinimized = false;
|
||||
if (minimizeThisInstance)
|
||||
{
|
||||
@@ -1417,6 +1420,36 @@ public sealed class MainForm : Form
|
||||
// user choosing to minimise, so it must not sound the "minimise" cue.
|
||||
BeginInvoke(() => trayController.Minimize(playCue: false));
|
||||
}
|
||||
else if (forceForegroundOnStart)
|
||||
{
|
||||
forceForegroundOnStart = false;
|
||||
logFile.Event("installer: post-install relaunch — bringing the window to the foreground");
|
||||
// Deferred so it runs after Shown settles, then yanks the window to the front so the
|
||||
// just-installed copy isn't left hiding behind other windows. Try again a moment later:
|
||||
// a freshly-launched process routinely loses the very first foreground race (the OS is
|
||||
// still settling which window owns the foreground just after the old copy exited).
|
||||
BeginInvoke(() =>
|
||||
{
|
||||
ForceWindowToForeground();
|
||||
var attempts = 0;
|
||||
var retry = new System.Windows.Forms.Timer { Interval = 250 };
|
||||
retry.Tick += (_, _) =>
|
||||
{
|
||||
attempts++;
|
||||
// Stop once we genuinely own the foreground, or after a few tries.
|
||||
var won = !IsDisposed && GetForegroundWindow() == Handle;
|
||||
if (IsDisposed || attempts >= 5 || won)
|
||||
{
|
||||
retry.Stop();
|
||||
retry.Dispose();
|
||||
logFile.Event($"installer: foreground grab done after {attempts} retries, gotForeground={won}");
|
||||
return;
|
||||
}
|
||||
ForceWindowToForeground();
|
||||
};
|
||||
retry.Start();
|
||||
});
|
||||
}
|
||||
|
||||
// Kick off UPnP discovery if the user has the box ticked. Off by default; the
|
||||
// mapper itself coalesces redundant Start() calls so a re-enter via Shown after
|
||||
@@ -1987,6 +2020,27 @@ public sealed class MainForm : Form
|
||||
UpdateRealtekAsioMenuItemText();
|
||||
}
|
||||
|
||||
// Install / uninstall RemSound as a proper per-user Windows app. The single item flips to
|
||||
// "Uninstall…" when this copy IS the installed one. Copies files to %LOCALAPPDATA%\Programs
|
||||
// (no admin), with optional shortcuts and login auto-start. Modelled on Andre's Sensor Readout.
|
||||
ToolStripMenuItem installItem;
|
||||
if (AppInstaller.IsInstalledCopy)
|
||||
{
|
||||
installItem = new ToolStripMenuItem("&Uninstall RemSound from this PC...")
|
||||
{
|
||||
AccessibleName = "Uninstall RemSound from this PC",
|
||||
};
|
||||
installItem.Click += (_, _) => AppInstaller.RunUninstallInProcess(this, msg => logFile.Event($"installer: {msg}"));
|
||||
}
|
||||
else
|
||||
{
|
||||
installItem = new ToolStripMenuItem("&Install RemSound on this PC...")
|
||||
{
|
||||
AccessibleName = "Install RemSound on this PC",
|
||||
};
|
||||
installItem.Click += (_, _) => AppInstaller.RunInstall(this, msg => logFile.Event($"installer: {msg}"));
|
||||
}
|
||||
|
||||
var optionItems = new List<ToolStripItem>
|
||||
{
|
||||
recordingSettingsItem,
|
||||
@@ -1997,6 +2051,7 @@ public sealed class MainForm : Form
|
||||
};
|
||||
if (realtekToggle is not null) optionItems.Add(realtekToggle);
|
||||
optionItems.Add(new ToolStripSeparator());
|
||||
optionItems.Add(installItem);
|
||||
optionItems.Add(prefsItem);
|
||||
optionsMenu.DropDownItems.AddRange(optionItems.ToArray());
|
||||
|
||||
@@ -2104,6 +2159,12 @@ public sealed class MainForm : Form
|
||||
// "loading audio driver" splash in that case.
|
||||
internal static bool startNextInstanceMinimized;
|
||||
|
||||
// Set from the --foreground switch, which the post-install relaunch passes. Makes the next
|
||||
// MainForm pull itself to the front and take focus even past Windows' foreground lock, so the
|
||||
// freshly-installed copy doesn't open behind other windows and leave the user hunting for it.
|
||||
// One-shot: consumed (cleared) the first time a window honours it.
|
||||
internal static bool forceForegroundOnStart;
|
||||
|
||||
// True until the first MainForm of the process has shown its window. Distinguishes a genuine
|
||||
// cold launch (where the "Start minimised" preference applies) from an in-session new-profile or
|
||||
// profile-switch relaunch (where it must NOT — those would otherwise hide the window and look
|
||||
@@ -7304,6 +7365,57 @@ public sealed class MainForm : Form
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern IntPtr GetForegroundWindow();
|
||||
|
||||
/// <summary>Bring this window to the very front and give it focus, robust against Windows'
|
||||
/// foreground lock. A freshly-launched process (the post-install relaunch) has no recent user
|
||||
/// input to its name, so a bare <c>SetForegroundWindow</c> is refused and the window opens behind
|
||||
/// whatever's on top. Briefly attaching our input queue to the current foreground thread lets the
|
||||
/// call through; the TopMost blip nudges the Z-order too. Best-effort — never throws.</summary>
|
||||
private void ForceWindowToForeground()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (IsDisposed) return;
|
||||
// Capture who's in front FIRST. If we called Show()/Activate() before reading this,
|
||||
// GetForegroundWindow could already return our own handle — and then we'd skip the
|
||||
// thread-attach below, which is the part that actually lets SetForegroundWindow win.
|
||||
var fgWnd = GetForegroundWindow();
|
||||
var myThread = GetCurrentThreadId();
|
||||
var fgThread = fgWnd == IntPtr.Zero ? myThread : GetWindowThreadProcessId(fgWnd, out _);
|
||||
|
||||
// Share input state with whatever currently owns the foreground, so Windows treats our
|
||||
// SetForegroundWindow as coming from the active thread and allows it past the lock.
|
||||
var attached = fgThread != myThread && AttachThreadInput(fgThread, myThread, true);
|
||||
try
|
||||
{
|
||||
if (WindowState == FormWindowState.Minimized) WindowState = FormWindowState.Normal;
|
||||
Show();
|
||||
BringToFront();
|
||||
SetForegroundWindow(Handle);
|
||||
Activate();
|
||||
Focus();
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (attached) AttachThreadInput(fgThread, myThread, false);
|
||||
}
|
||||
}
|
||||
catch { /* best-effort — worst case the window is visible but not focused */ }
|
||||
}
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool SetForegroundWindow(IntPtr hWnd);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(IntPtr hWnd, out uint processId);
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
[System.Runtime.InteropServices.DllImport("user32.dll")]
|
||||
[return: System.Runtime.InteropServices.MarshalAs(System.Runtime.InteropServices.UnmanagedType.Bool)]
|
||||
private static extern bool AttachThreadInput(uint idAttach, uint idAttachTo, bool fAttach);
|
||||
|
||||
|
||||
// ===================== Profile system =====================
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
using System.Diagnostics;
|
||||
using System.Runtime;
|
||||
using System.Windows.Forms;
|
||||
using RemSound.Core;
|
||||
@@ -102,6 +103,17 @@ internal static class Program
|
||||
}
|
||||
catch { /* older runtime or API change — stay on the classic light theme */ }
|
||||
|
||||
// --uninstall: launched from the Start-menu "Uninstall RemSound" shortcut or Windows
|
||||
// "Installed apps". Runs its own confirmation dialog and the wait-then-delete remover, then
|
||||
// exits. Handled here — after visual styles/theme are set so the dialog renders correctly, but
|
||||
// BEFORE the single-instance guard, because the installed copy it's removing may itself be
|
||||
// running (the remover force-closes it and retries the delete).
|
||||
if (Array.Exists(args, a => string.Equals(a, "--uninstall", StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
AppInstaller.RunUninstallStandalone();
|
||||
return;
|
||||
}
|
||||
|
||||
// Consolidate every older layout (loose files, or the interim config\ folder) into the single
|
||||
// "user settings and logs" folder before anything reads config/profiles/logs. Idempotent +
|
||||
// best-effort; upgrades users from any older build. Shown to the user once if files moved.
|
||||
@@ -128,6 +140,18 @@ internal static class Program
|
||||
if (cliExit is { } cliCode) Environment.Exit(cliCode);
|
||||
if (cli.StartMinimized) MainForm.startNextInstanceMinimized = true;
|
||||
|
||||
// --foreground: the post-install relaunch passes this so the freshly-installed copy pulls
|
||||
// itself to the front and takes focus, instead of opening behind whatever's on top (a new
|
||||
// process has no "recent user input" credit, so Windows' foreground lock blocks it otherwise).
|
||||
if (Array.Exists(args, a => string.Equals(a, "--foreground", StringComparison.OrdinalIgnoreCase)))
|
||||
MainForm.forceForegroundOnStart = true;
|
||||
|
||||
// --await-pid <pid>: the post-install relaunch passes the OLD (portable) copy's process id.
|
||||
// Wait for it to exit — releasing the single-instance lock — BEFORE we try to take that lock
|
||||
// just below, so the hand-over doesn't trip the "already running" guard. Bounded so a stale or
|
||||
// wrong pid can never hang startup.
|
||||
WaitForAwaitedProcess(args);
|
||||
|
||||
// Single-instance guard. RemSound must never run as two copies at once: with the
|
||||
// auto-updater relaunching the app, a copy that didn't exit cleanly used to leave two
|
||||
// (then more) copies running, each playing received audio — Andre's "stacked and
|
||||
@@ -408,6 +432,22 @@ internal static class Program
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>If <c>--await-pid <pid></c> is present (the post-install relaunch passes the old
|
||||
/// copy's id), block until that process exits so its single-instance lock is released before this
|
||||
/// copy tries to take it. Bounded to a few seconds so a stale/wrong pid can never hang startup.</summary>
|
||||
private static void WaitForAwaitedProcess(string[] args)
|
||||
{
|
||||
var idx = Array.FindIndex(args, a => string.Equals(a, "--await-pid", StringComparison.OrdinalIgnoreCase));
|
||||
if (idx < 0 || idx + 1 >= args.Length) return;
|
||||
if (!int.TryParse(args[idx + 1], out var pid)) return;
|
||||
try
|
||||
{
|
||||
using var p = Process.GetProcessById(pid);
|
||||
p.WaitForExit(10000);
|
||||
}
|
||||
catch { /* already gone / invalid pid — nothing to wait for */ }
|
||||
}
|
||||
|
||||
/// <summary>One-time, Windows-native notice telling the user their config/profiles were moved
|
||||
/// into the new "user settings and logs" folder. Only called when a real migration happened. TaskDialog
|
||||
/// (not a hand-rolled Form) so a screen reader reads the whole message automatically.</summary>
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
tag_name on the latest GitHub release; bump it on every public release. The
|
||||
AssemblyVersion / FileVersion default to this value, and Assembly.GetName().Version
|
||||
is what the About dialog and the updater both read. -->
|
||||
<Version>5.0</Version>
|
||||
<Version>5.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
|
||||
@@ -45,20 +45,30 @@ internal static class StartupAutoStart
|
||||
/// <summary>Add or update the Run-key entry to point at the currently-running exe.
|
||||
/// Quotes the path so spaces work. Returns true on success.</summary>
|
||||
public static bool TryEnable()
|
||||
{
|
||||
var exePath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
// Fallback: use AppContext.BaseDirectory. .NET hosting produces a
|
||||
// different process path for self-contained vs framework-dependent
|
||||
// publish, but BaseDirectory is reliable.
|
||||
exePath = System.IO.Path.Combine(AppContext.BaseDirectory, "RemSound.exe");
|
||||
}
|
||||
return TryEnable(exePath);
|
||||
}
|
||||
|
||||
/// <summary>Add or update the Run-key entry to point at a specific exe path. Used by the
|
||||
/// self-installer to re-point login-launch at the freshly INSTALLED copy while it's still
|
||||
/// running from the portable one, so auto-start doesn't keep launching the old folder.
|
||||
/// Quotes the path so spaces work. Returns true on success.</summary>
|
||||
public static bool TryEnable(string exePath)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exePath)) return false;
|
||||
using var key = Registry.CurrentUser.OpenSubKey(RunKeyPath, writable: true)
|
||||
?? Registry.CurrentUser.CreateSubKey(RunKeyPath, writable: true);
|
||||
if (key is null) return false;
|
||||
var exePath = Environment.ProcessPath;
|
||||
if (string.IsNullOrEmpty(exePath))
|
||||
{
|
||||
// Fallback: use AppContext.BaseDirectory. .NET hosting produces a
|
||||
// different process path for self-contained vs framework-dependent
|
||||
// publish, but BaseDirectory is reliable.
|
||||
exePath = System.IO.Path.Combine(AppContext.BaseDirectory, "RemSound.exe");
|
||||
}
|
||||
// Wrap in double-quotes so a path containing spaces (e.g. C:\Program Files\)
|
||||
// parses correctly when Windows launches it.
|
||||
key.SetValue(ValueName, $"\"{exePath}\"");
|
||||
|
||||
Reference in New Issue
Block a user