Skip to content

Commit

Permalink
feat: 新增WindowsAutorun,用于Windows桌面登录自启动。close NewLifeX/Stardust#31
Browse files Browse the repository at this point in the history
  • Loading branch information
nnhy committed Aug 8, 2023
1 parent 3296daf commit b20a671
Show file tree
Hide file tree
Showing 4 changed files with 252 additions and 5 deletions.
16 changes: 16 additions & 0 deletions NewLife.Agent/IHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,14 @@ public interface IHost
/// <param name="serviceName">服务名</param>
Boolean Restart(String serviceName);

///// <summary>安装为自启动。登录进入桌面后启动</summary>
///// <param name="serviceName">服务名</param>
///// <param name="displayName">显示名</param>
///// <param name="binPath">文件路径</param>
///// <param name="description">描述信息</param>
///// <returns></returns>
//Boolean InstallAutorun(String serviceName, String displayName, String binPath, String description);

/// <summary>开始执行服务</summary>
/// <param name="service"></param>
void Run(ServiceBase service);
Expand Down Expand Up @@ -99,6 +107,14 @@ public abstract class Host : DisposeBase, IHost
/// <param name="serviceName">服务名</param>
public virtual Boolean Restart(String serviceName) => false;

///// <summary>安装为自启动。登录进入桌面后启动</summary>
///// <param name="serviceName">服务名</param>
///// <param name="displayName">显示名</param>
///// <param name="binPath">文件路径</param>
///// <param name="description">描述信息</param>
///// <returns></returns>
//public virtual Boolean InstallAutorun(String serviceName, String displayName, String binPath, String description) => false;

/// <summary>开始执行服务</summary>
/// <param name="service"></param>
public abstract void Run(ServiceBase service);
Expand Down
27 changes: 22 additions & 5 deletions NewLife.Agent/ServiceBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,9 @@ public abstract class ServiceBase : DisposeBase

/// <summary>描述</summary>
public String Description { get; set; }

/// <summary>是否使用自启动。自启动需要用户登录桌面,默认false使用系统服务</summary>
public Boolean UseAutorun { get; set; }
#endregion

#region 构造
Expand Down Expand Up @@ -109,12 +112,18 @@ protected virtual void Init()
if (Host == null)
{
if (Runtime.Windows)
Host = new WindowsService { Service = this };
{
if (UseAutorun)
Host = new WindowsAutorun { Service = this };
else
Host = new WindowsService { Service = this };
}
else if (Systemd.Available)
Host = new Systemd { Service = this };
else Host = RcInit.Available ?
(IHost)new RcInit { Service = this } :
throw new NotSupportedException($"不支持该操作系统!");
else if (RcInit.Available)
Host = new RcInit { Service = this };
else
throw new NotSupportedException($"不支持该操作系统!");
}

Log = XTrace.Log;
Expand Down Expand Up @@ -259,6 +268,9 @@ protected virtual void ProcessMenu()
}
#endregion
break;
//case '6':
// InstallAutorun();
// break;
case '7':
if (WatchDogs.Length > 0) CheckWatchDog();
break;
Expand Down Expand Up @@ -312,6 +324,11 @@ protected virtual void ShowMenu()
Console.WriteLine("5 模拟运行 -run");
}

//if (Runtime.Windows)
//{
// Console.WriteLine("6 安装开机自启 -autorun");
//}

var dogs = WatchDogs;
if (dogs.Length > 0)
{
Expand Down Expand Up @@ -594,7 +611,7 @@ private void Install()
exe = args[0].GetFullPath();
}

var bin = $"{exe} -s";
var bin = UseAutorun ? $"{exe} -run" : $"{exe} -s";

// 兼容更多参数做为服务启动,譬如:--urls
if (args.Length > 2)
Expand Down
212 changes: 212 additions & 0 deletions NewLife.Agent/WindowsAutorun.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
using System.Diagnostics;
using System.Reflection;
using System.Runtime.InteropServices;
using System.Runtime.Versioning;
using System.Security.Principal;
using System.Text;
using Microsoft.Win32;
using NewLife.Log;

namespace NewLife.Agent;

/// <summary>Windows自启动</summary>
/// <remarks>
/// 桌面登录自启动
/// </remarks>
public class WindowsAutorun : Host
{
/// <summary>开始执行服务</summary>
/// <param name="service"></param>
public override void Run(ServiceBase service) => throw new NotSupportedException();

/// <summary>服务是否已安装</summary>
/// <param name="serviceName">服务名</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public override Boolean IsInstalled(String serviceName)
{
#if NET40_OR_GREATER || NET5_0_OR_GREATER
// 在注册表中写入启动配置
using var key = Registry.LocalMachine.OpenSubKey("""SOFTWARE\Microsoft\Windows\CurrentVersion\Run""", false);

var v = key.GetValue(serviceName)?.ToString();
return !v.IsNullOrEmpty();
#else
return false;
#endif
}

/// <summary>服务是否已启动</summary>
/// <param name="serviceName">服务名</param>
/// <returns></returns>
public override unsafe Boolean IsRunning(String serviceName) => false;

/// <summary>安装服务</summary>
/// <param name="serviceName">服务名</param>
/// <param name="displayName"></param>
/// <param name="binPath"></param>
/// <param name="description"></param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public override Boolean Install(String serviceName, String displayName, String binPath, String description)
{
XTrace.WriteLine("{0}.Install {1}, {2}, {3}, {4}", GetType().Name, serviceName, displayName, binPath, description);

#if !NETSTANDARD
if (!IsAdministrator()) return RunAsAdministrator("-i");
#endif

//// 当前用户的启动目录
//var dir = Environment.GetFolderPath(Environment.SpecialFolder.Startup);
//if (dir.IsNullOrEmpty()) return false;

//dir.EnsureDirectory(false);

#if NET40_OR_GREATER || NET5_0_OR_GREATER
// 在注册表中写入启动配置
using var key = Registry.LocalMachine.OpenSubKey("""SOFTWARE\Microsoft\Windows\CurrentVersion\Run""", true);

// 添加应用程序到自启动项
key.SetValue(serviceName, binPath);
#endif

return true;
}

/// <summary>卸载服务</summary>
/// <param name="serviceName">服务名</param>
/// <returns></returns>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public override unsafe Boolean Remove(String serviceName)
{
XTrace.WriteLine("{0}.Remove {1}", GetType().Name, serviceName);

#if !NETSTANDARD
if (!IsAdministrator()) return RunAsAdministrator("-uninstall");
#endif

#if NET40_OR_GREATER || NET5_0_OR_GREATER
// 在注册表中写入启动配置
using var key = Registry.LocalMachine.OpenSubKey("""SOFTWARE\Microsoft\Windows\CurrentVersion\Run""", true);
key.DeleteValue(serviceName, false);
#endif

return true;
}

/// <summary>查询服务配置</summary>
/// <param name="serviceName">服务名</param>
#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
public override unsafe ServiceConfig QueryConfig(String serviceName)
{
#if NET40_OR_GREATER || NET5_0_OR_GREATER
using var key = Registry.LocalMachine.OpenSubKey("""SOFTWARE\Microsoft\Windows\CurrentVersion\Run""", false);

var v = key.GetValue(serviceName)?.ToString();
if (v.IsNullOrEmpty()) return null;

return new ServiceConfig
{
Name = serviceName,
FilePath = v.TrimEnd(" -run")
};
#else
return null;
#endif
}

#if !NETSTANDARD
static Boolean RunAsAdministrator(String argument)
{
var exe = ExecutablePath;
if (exe.IsNullOrEmpty()) return false;

if (exe.EndsWithIgnoreCase(".dll"))
{
var exe2 = Path.ChangeExtension(exe, ".exe");
if (File.Exists(exe2)) exe = exe2;
}

var startInfo = exe.EndsWithIgnoreCase(".dll") ?
new ProcessStartInfo
{
FileName = "dotnet",
Arguments = $"{Path.GetFileName(exe)} {argument}",
WorkingDirectory = Path.GetDirectoryName(exe),
Verb = "runas",
UseShellExecute = true,
} :
new ProcessStartInfo
{
FileName = exe,
Arguments = argument,
Verb = "runas",
UseShellExecute = true,
};

var p = Process.Start(startInfo);
return !p.WaitForExit(5_000) || p.ExitCode == 0;
}

static String _executablePath;
static String ExecutablePath
{
get
{
if (_executablePath == null)
{
var entryAssembly = Assembly.GetEntryAssembly();
if (entryAssembly != null)
{
var codeBase = entryAssembly.CodeBase;

Check warning on line 169 in NewLife.Agent/WindowsAutorun.cs

View workflow job for this annotation

GitHub Actions / build-publish

'Assembly.CodeBase' is obsolete: 'Assembly.CodeBase and Assembly.EscapedCodeBase are only included for .NET Framework compatibility. Use Assembly.Location.' (https://aka.ms/dotnet-warnings/SYSLIB0012)

Check warning on line 169 in NewLife.Agent/WindowsAutorun.cs

View workflow job for this annotation

GitHub Actions / test

'Assembly.CodeBase' is obsolete: 'Assembly.CodeBase and Assembly.EscapedCodeBase are only included for .NET Framework compatibility. Use Assembly.Location.' (https://aka.ms/dotnet-warnings/SYSLIB0012)

Check warning on line 169 in NewLife.Agent/WindowsAutorun.cs

View workflow job for this annotation

GitHub Actions / test

'Assembly.CodeBase' is obsolete: 'Assembly.CodeBase and Assembly.EscapedCodeBase are only included for .NET Framework compatibility. Use Assembly.Location.' (https://aka.ms/dotnet-warnings/SYSLIB0012)
var uri = new Uri(codeBase);
_executablePath = uri.IsFile ? uri.LocalPath + Uri.UnescapeDataString(uri.Fragment) : uri.ToString();
}
else
{
var moduleFileNameLongPath = GetModuleFileNameLongPath(new HandleRef(null, IntPtr.Zero));
_executablePath = moduleFileNameLongPath.ToString().GetFullPath();
}
}

return _executablePath;
}
}

static StringBuilder GetModuleFileNameLongPath(HandleRef hModule)
{
var sb = new StringBuilder(260);
var num = 1;
var num2 = 0;
while ((num2 = GetModuleFileName(hModule, sb, sb.Capacity)) == sb.Capacity && Marshal.GetLastWin32Error() == 122 && sb.Capacity < 32767)
{
num += 2;
var capacity = (num * 260 < 32767) ? (num * 260) : 32767;
sb.EnsureCapacity(capacity);
}
sb.Length = num2;
return sb;
}

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern Int32 GetModuleFileName(HandleRef hModule, StringBuilder buffer, Int32 length);

#if NET5_0_OR_GREATER
[SupportedOSPlatform("windows")]
#endif
private Boolean IsAdministrator()
{
var current = WindowsIdentity.GetCurrent();
var windowsPrincipal = new WindowsPrincipal(current);
return windowsPrincipal.IsInRole(WindowsBuiltInRole.Administrator);
}
#endif
}
2 changes: 2 additions & 0 deletions Test/MyServices.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public MyServices()

DisplayName = "新生命服务代理";
Description = "用于承载各种服务的服务代理!";

UseAutorun = true;
}
#endregion

Expand Down

0 comments on commit b20a671

Please sign in to comment.