added statup with Windows feature
This commit is contained in:
@@ -0,0 +1,120 @@
|
||||
using System.IO;
|
||||
using System.Security;
|
||||
using CursorLang.Models;
|
||||
using Microsoft.Win32;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Startup for a build that is not a package: a value under the Run key.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// A package declares its startup task in the manifest and asks Windows to switch
|
||||
/// it on. A build unpacked into a folder has no manifest, so it registers itself
|
||||
/// the way desktop programs always have — under the Run key of the current user.
|
||||
/// Administrator rights are not needed for that: the key belongs to the user.
|
||||
///
|
||||
/// Windows keeps the user's own verdict apart from the entry itself. Turning the
|
||||
/// app off in Settings — Apps — Startup leaves the Run value where it is and marks
|
||||
/// it disabled under StartupApproved. The mark is obeyed here the same way a
|
||||
/// package obeys DisabledByUser: the app does not argue with the user.
|
||||
/// </remarks>
|
||||
internal sealed class RegistryStartup
|
||||
{
|
||||
private const string RunPath = @"Software\Microsoft\Windows\CurrentVersion\Run";
|
||||
|
||||
private const string ApprovedPath =
|
||||
@"Software\Microsoft\Windows\CurrentVersion\Explorer\StartupApproved\Run";
|
||||
|
||||
/// <summary>The name of the value — Windows shows it in the startup list.</summary>
|
||||
private const string ValueName = "CursorLang";
|
||||
|
||||
private readonly RegistryKey _root;
|
||||
private readonly string? _command;
|
||||
|
||||
internal RegistryStartup()
|
||||
: this(Registry.CurrentUser, GetCommand())
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>A root of the test's own, so that the real startup list is left alone.</summary>
|
||||
internal RegistryStartup(RegistryKey root, string? command)
|
||||
{
|
||||
_root = root;
|
||||
_command = command;
|
||||
}
|
||||
|
||||
internal StartupState GetState()
|
||||
{
|
||||
if (_command is null)
|
||||
{
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using RegistryKey? run = _root.OpenSubKey(RunPath);
|
||||
|
||||
if (run?.GetValue(ValueName) is null)
|
||||
{
|
||||
return StartupState.Disabled;
|
||||
}
|
||||
|
||||
return IsApprovedByUser() ? StartupState.Enabled : StartupState.DisabledByUser;
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException)
|
||||
{
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
internal StartupState SetEnabled(bool enabled)
|
||||
{
|
||||
if (_command is null)
|
||||
{
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using RegistryKey run = _root.CreateSubKey(RunPath);
|
||||
|
||||
if (enabled)
|
||||
{
|
||||
// The path is written afresh every time: the app may have been moved
|
||||
run.SetValue(ValueName, _command, RegistryValueKind.String);
|
||||
}
|
||||
else
|
||||
{
|
||||
run.DeleteValue(ValueName, throwOnMissingValue: false);
|
||||
}
|
||||
}
|
||||
catch (Exception e) when (e is SecurityException or UnauthorizedAccessException or IOException)
|
||||
{
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
|
||||
// The answer is read back rather than assumed: an entry the user has
|
||||
// banned stays banned no matter what was just written next to it
|
||||
return GetState();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Whether the user has left the entry alone. The verdict is a blob whose
|
||||
/// lowest bit of the first byte stands for the ban; no value means untouched.
|
||||
/// </summary>
|
||||
private bool IsApprovedByUser()
|
||||
{
|
||||
using RegistryKey? approved = _root.OpenSubKey(ApprovedPath);
|
||||
|
||||
return approved?.GetValue(ValueName) is not byte[] { Length: > 0 } verdict
|
||||
|| (verdict[0] & 1) == 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// What Windows is to run. <c>null</c> — the path of the running program is
|
||||
/// unknown, and there is nothing to write down.
|
||||
/// </summary>
|
||||
private static string? GetCommand() =>
|
||||
Environment.ProcessPath is { Length: > 0 } path ? $"\"{path}\"" : null;
|
||||
}
|
||||
Reference in New Issue
Block a user