added statup with Windows feature
This commit is contained in:
@@ -0,0 +1,38 @@
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace CursorLang.Interop;
|
||||
|
||||
/// <summary>
|
||||
/// Answers whether the application runs from an MSIX package.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The same application runs both installed from the Store and simply unpacked
|
||||
/// into a folder. Some Windows features — startup through <c>StartupTask</c>, for
|
||||
/// instance — are available only to a package, and reaching for them without a
|
||||
/// check is not allowed: outside a package they throw.
|
||||
/// </remarks>
|
||||
internal static class PackageIdentityNative
|
||||
{
|
||||
/// <summary>APPMODEL_ERROR_NO_PACKAGE — the process runs outside a package.</summary>
|
||||
private const int NoPackage = 15700;
|
||||
|
||||
/// <summary>
|
||||
/// The application runs from an MSIX package.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// The value is computed once: it cannot change during the lifetime of
|
||||
/// the process.
|
||||
/// </remarks>
|
||||
internal static bool IsPackaged { get; } = DetectPackage();
|
||||
|
||||
private static bool DetectPackage()
|
||||
{
|
||||
// The answer comes from the return code rather than from the name itself, so
|
||||
// no buffer is needed: with zero length a package replies complaining about space
|
||||
uint length = 0;
|
||||
return GetCurrentPackageFullName(ref length, IntPtr.Zero) != NoPackage;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
|
||||
private static extern int GetCurrentPackageFullName(ref uint packageFullNameLength, IntPtr packageFullName);
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
using CursorLang.Models;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Starting the app together with Windows.
|
||||
/// </summary>
|
||||
public interface IStartupService
|
||||
{
|
||||
/// <summary>Finds out the current state of startup.</summary>
|
||||
Task<StartupState> GetStateAsync();
|
||||
|
||||
/// <summary>
|
||||
/// Asks for startup to be switched on or off and answers with the state that
|
||||
/// came of it: the request to switch it on may well be turned down.
|
||||
/// </summary>
|
||||
Task<StartupState> SetEnabledAsync(bool enabled);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using CursorLang.Interop;
|
||||
using CursorLang.Models;
|
||||
using Windows.ApplicationModel;
|
||||
|
||||
namespace CursorLang.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Startup, arranged by whatever means the current build has.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// Startup used to be a scheduled task with the highest rights — otherwise an app
|
||||
/// that wanted administrator rights would not start from the startup folder. The
|
||||
/// app needs no such rights any more, and the two ways left are simpler.
|
||||
///
|
||||
/// A package declares the task in its manifest, and Windows lists it for the user
|
||||
/// next to the rest under Settings — Apps — Startup. Turned off there, it can no
|
||||
/// longer be turned back on by the app. Outside a package the same setting is kept
|
||||
/// in the registry: see <see cref="RegistryStartup"/>.
|
||||
/// </remarks>
|
||||
public sealed class StartupService : IStartupService
|
||||
{
|
||||
/// <summary>Matches TaskId in the package manifest.</summary>
|
||||
private const string TaskId = "CursorLangStartup";
|
||||
|
||||
private readonly RegistryStartup _registry = new();
|
||||
|
||||
public async Task<StartupState> GetStateAsync()
|
||||
{
|
||||
if (!PackageIdentityNative.IsPackaged)
|
||||
{
|
||||
return _registry.GetState();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StartupTask task = await StartupTask.GetAsync(TaskId);
|
||||
return Translate(task.State);
|
||||
}
|
||||
catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
|
||||
{
|
||||
// No task by that name in the manifest: that happens to a package put
|
||||
// together by hand. The setting simply will not show
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<StartupState> SetEnabledAsync(bool enabled)
|
||||
{
|
||||
if (!PackageIdentityNative.IsPackaged)
|
||||
{
|
||||
return _registry.SetEnabled(enabled);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
StartupTask task = await StartupTask.GetAsync(TaskId);
|
||||
|
||||
if (!enabled)
|
||||
{
|
||||
task.Disable();
|
||||
return Translate(task.State);
|
||||
}
|
||||
|
||||
// Windows answers with a state rather than with success: once the user
|
||||
// has forbidden startup, the ban stays
|
||||
return Translate(await task.RequestEnableAsync());
|
||||
}
|
||||
catch (Exception e) when (e is COMException or ArgumentException or InvalidOperationException)
|
||||
{
|
||||
return StartupState.Unavailable;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>The state of a Windows task in the app's own terms.</summary>
|
||||
internal static StartupState Translate(StartupTaskState state) => state switch
|
||||
{
|
||||
StartupTaskState.Enabled => StartupState.Enabled,
|
||||
StartupTaskState.EnabledByPolicy => StartupState.EnabledByPolicy,
|
||||
StartupTaskState.Disabled => StartupState.Disabled,
|
||||
StartupTaskState.DisabledByUser => StartupState.DisabledByUser,
|
||||
StartupTaskState.DisabledByPolicy => StartupState.DisabledByPolicy,
|
||||
_ => StartupState.Unavailable,
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user