XFE Git
XFE Studio Git
Git 首页 全局搜索
XFE 主站 文档 NuGet

XFEExtension.NetCore.InputSimulator

【DLL】XFE各类拓展的模拟键盘输入

公开
关注 0 Fork 0 Star 0
UTF-8
using System.ComponentModel;
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json;
using XFEExtension.NetCore.InputSimulator.Native;

namespace XFEExtension.NetCore.InputSimulator;

/// <summary>部署 DLL 内嵌的驱动包。使用方不需要 WDK、单独下载驱动或第三方输入环境。</summary>
public static class DriverDeployment
{
    private const string Prefix = "XFE.InputDriver.";
    private static readonly string[] Files = ["XfeInputDriver.dll", "XfeInput.inf", "XfeInput.cat", "XfeInputSetup.exe", "publisher.cer"];
    private static readonly object InstallLock = new();

    /// <summary>检测已启动的驱动控制设备及协议;不安装、不请求管理员授权、不发送输入或占用会话。</summary>
    /// <remarks>未检测到设备也可能是设备被禁用或尚未启动。Ready 不代表已安装最新驱动或输入会话空闲。</remarks>
    public static DriverCheckResult CheckDriver() => CheckDriver(DriverTransport.QueryDevice);

    internal static DriverCheckResult CheckDriver(Func<DriverInfo?> queryDevice)
    {
        try
        {
            var info = queryDevice();
            if (info is null)
                return new(DriverStatus.NotDetected, "未检测到已启动的 XFE HID 控制设备。可显式调用 DriverDeployment.InstallEmbeddedDriver() 安装,或检查设备管理器中的设备状态。");
            if (!DriverProtocol.IsCompatible(info.Value))
                return new(DriverStatus.Incompatible, "已安装的驱动协议与本 DLL 不兼容,请显式调用 DriverDeployment.InstallEmbeddedDriver() 更新驱动。");
            return new(DriverStatus.Ready, "驱动控制设备可访问且协议兼容;连接时仍需申请独占输入会话。");
        }
        catch (PlatformNotSupportedException error) { return new(DriverStatus.UnsupportedPlatform, error.Message); }
        catch (InvalidDataException error) { return new(DriverStatus.Incompatible, error.Message); }
        catch (Exception error) when (error is Win32Exception or IOException or UnauthorizedAccessException)
        {
            return new(DriverStatus.Unavailable, error.Message);
        }
    }

    /// <summary>此 DLL 是否包含完整驱动负载。</summary>
    public static bool HasEmbeddedDriver
    {
        get
        {
            var assembly = Assembly.GetExecutingAssembly();
            if (!Files.Take(4).All(file => assembly.GetManifestResourceInfo(Prefix + file) is not null) ||
                assembly.GetManifestResourceInfo(Prefix + "payload.json") is null) return false;
            using var manifest = ReadManifest();
            return !manifest.RootElement.GetProperty("signed").GetBoolean() ||
                assembly.GetManifestResourceInfo(Prefix + "publisher.cer") is not null;
        }
    }

    /// <summary>打包时驱动目录文件和安装器是否通过签名校验;最终能否加载仍由 Windows 决定。</summary>
    public static bool IsEmbeddedDriverSigned
    {
        get
        {
            if (!HasEmbeddedDriver) return false;
            using var manifest = ReadManifest();
            return manifest.RootElement.GetProperty("signed").GetBoolean();
        }
    }

    /// <summary>内嵌签名类型:unsigned、self-signed 或 trusted-publisher。自签名包需用户信任签名者。</summary>
    public static string EmbeddedDriverSigningKind
    {
        get
        {
            if (!HasEmbeddedDriver) return "unsigned";
            using var manifest = ReadManifest();
            return manifest.RootElement.GetProperty("signingKind").GetString()!;
        }
    }

    /// <summary>发布者证书到期时间;未签名时为空。安装器会再次检查证书有效期。</summary>
    public static DateTimeOffset? EmbeddedDriverCertificateExpires
    {
        get
        {
            if (!IsEmbeddedDriverSigned) return null;
            using var manifest = ReadManifest();
            return manifest.RootElement.GetProperty("certificateExpires").GetDateTimeOffset();
        }
    }

    /// <summary>打包时所有签名负载是否带有通过校验的时间戳;不表示包可永久安装。</summary>
    public static bool IsEmbeddedDriverTimestamped
    {
        get
        {
            if (!IsEmbeddedDriverSigned) return false;
            using var manifest = ReadManifest();
            return manifest.RootElement.TryGetProperty("timestamped", out var value) && value.GetBoolean();
        }
    }

    /// <summary>显式安装或更新内嵌驱动,请求 UAC 管理员授权;不会修改启动安全策略或自动重启。</summary>
    /// <remarks>仅在调用方主动调用本方法时安装。创建控制器、发送输入及 CheckDriver 均不会自动安装。</remarks>
    public static void InstallEmbeddedDriver()
    {
        if (!OperatingSystem.IsWindowsVersionAtLeast(10, 0, 22000) || RuntimeInformation.OSArchitecture != Architecture.X64)
            throw new PlatformNotSupportedException("驱动安装支持 Windows 11 x64 系统。");
        lock (InstallLock)
        {
            if (!HasEmbeddedDriver) throw new InvalidOperationException("此 DLL 未内嵌驱动包;请使用完成驱动打包的发行版。");
            using var manifest = ReadManifest();
            var root = manifest.RootElement;
            if (root.GetProperty("protocol").GetUInt32() != Native.DriverProtocol.Version)
                throw new InvalidDataException("内嵌驱动包协议版本不匹配。");
            if (!root.GetProperty("signed").GetBoolean())
                throw new InvalidOperationException("此 DLL 属于未签名开发版,不能安装。请由发布者签名 UMDF 驱动包并重新打包;调用方无需申请证书或安装 WDK。");
            if (root.GetProperty("certificateExpires").GetDateTimeOffset() <= DateTimeOffset.UtcNow)
                throw new InvalidOperationException("内嵌驱动的签名证书已过期,请向发布者获取使用有效证书签名的新版本。");

            var directory = Path.Combine(Path.GetTempPath(), "XfeInput-" + Guid.NewGuid().ToString("N"));
            Directory.CreateDirectory(directory);
            var cleanup = true;
            try
            {
                foreach (var name in Files)
                {
                    using var resource = Assembly.GetExecutingAssembly().GetManifestResourceStream(Prefix + name)
                        ?? throw new InvalidDataException($"驱动资源缺失:{name}");
                    using var contents = new MemoryStream();
                    resource.CopyTo(contents);
                    var data = contents.ToArray();
                    var expected = root.GetProperty("files").GetProperty(name).GetString();
                    if (!Convert.ToHexString(SHA256.HashData(data)).Equals(expected, StringComparison.OrdinalIgnoreCase))
                        throw new InvalidDataException($"内嵌驱动资源校验失败:{name}");
                    File.WriteAllBytes(Path.Combine(directory, name), data);
                }
                var start = new ProcessStartInfo(Path.Combine(directory, "XfeInputSetup.exe"))
                {
                    UseShellExecute = true,
                    Verb = "runas",
                    WindowStyle = ProcessWindowStyle.Hidden,
                    WorkingDirectory = directory
                };
                start.ArgumentList.Add("install");
                start.ArgumentList.Add(Path.Combine(directory, "XfeInput.inf"));
                start.ArgumentList.Add(Path.Combine(directory, "publisher.cer"));
                start.ArgumentList.Add(root.GetProperty("files").GetProperty("publisher.cer").GetString()!);
                using var installer = Process.Start(start) ?? throw new IOException("无法启动驱动安装器。");
                if (!installer.WaitForExit(120000))
                {
                    cleanup = false; // An active Windows installer may still need these files.
                    throw new TimeoutException($"驱动安装尚未结束;请等待安装器完成。临时文件保留于 {directory}");
                }
                if (installer.ExitCode == 3010) throw new InvalidOperationException("驱动已安装,Windows 要求重启后才能使用;未自动重启。");
                if (installer.ExitCode != 0) throw new Win32Exception(installer.ExitCode, $"Windows 驱动安装失败({installer.ExitCode}),请检查签名和系统安装日志。");
                // Device enumeration may lag behind a successful installer exit.
                var readyTimer = Stopwatch.StartNew();
                while (true)
                {
                    var result = CheckDriver();
                    if (result.IsAvailable) break;
                    if (readyTimer.Elapsed >= TimeSpan.FromSeconds(10))
                        throw new IOException($"驱动安装器已完成,但控制设备尚未就绪:{result.Message}");
                    Thread.Sleep(250);
                }
            }
            finally
            {
                if (cleanup)
                {
                    // Delete only files extracted by this invocation; never recursively clean user paths.
                    foreach (var name in Files)
                        try { File.Delete(Path.Combine(directory, name)); } catch (IOException) { } catch (UnauthorizedAccessException) { }
                    try { Directory.Delete(directory); } catch (IOException) { } catch (UnauthorizedAccessException) { }
                }
            }
        }
    }

    private static JsonDocument ReadManifest()
    {
        using var stream = Assembly.GetExecutingAssembly().GetManifestResourceStream(Prefix + "payload.json")
            ?? throw new InvalidDataException("内嵌驱动清单缺失。");
        return JsonDocument.Parse(stream);
    }
}