131 lines
4.5 KiB
C#
131 lines
4.5 KiB
C#
using System.Security;
|
|
using CursorLang.Core.Models;
|
|
using Microsoft.Win32;
|
|
|
|
namespace CursorLang.Core.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 agent is not where it should be, and
|
|
/// there is nothing to write down.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The agent by name rather than <c>Environment.ProcessPath</c>: this setting is
|
|
/// switched from the settings window, and its own path would put the wrong process
|
|
/// into the startup list — one that shows a window and exits.
|
|
///
|
|
/// The argument is how the agent recognises a launch of this kind and goes straight
|
|
/// to the tray without the settings window: see <see cref="StartupLaunch"/>. The
|
|
/// user starting the application themselves passes no such thing and gets the window.
|
|
/// </remarks>
|
|
internal static string? GetCommand() =>
|
|
AgentExecutable.AgentPath is { Length: > 0 } path
|
|
? $"\"{path}\" {StartupLaunch.Argument}"
|
|
: null;
|
|
}
|