【C#】分享基於Win32 API的服務操作類(解決ManagedInstallerClass.InstallHelper不能帶參數安裝的問題)


注:這里的服務是指Windows 服務

------------------201508250915更新------------------

剛剛得知TransactedInstaller類是支持帶參數安裝服務的,在此感謝猿友KOFIP的指教和代碼,詳情請見回復。

------------------201506182056原文------------------

市面上常見的安裝一個服務的方法大概有這么幾種:

  • 用Process類調用sc.exe、Installutil.exe等外部工具進行安裝。極不推薦,須知創建一個進程開銷不小,並且依賴外部工具有損程序健壯性
  • 使用TransactedInstaller和AssemblyInstaller安裝類進行安裝。不推薦,既然都用托管方法,何不用更好的方法呢
  • 用ManagedInstallerClass.InstallHelper進行安裝。這個我認為是托管方法中首選的方法,事實上它就是對上述兩個安裝類的封裝。另外,Installutil.exe也是用的這個方法

此前我一直用的就是InstallHelper法,但最近需要安裝一個服務時卻遇到問題,就是承載該服務的程序文件(exe),同時又是個帶用戶界面的桌面程序,它通過傳入main方法的參數決定是以服務運行,還是以桌面程序運行(這里不討論為什么不把服務和桌面程序分成兩個exe。另外有關如何讓一個exe即是服務又是桌面程序的問題,請參看園子里其它猿友的文章,或者有閑心我也會寫一篇),這就需要安裝該服務時,給映像文件路徑帶上參數,但InstallHelper不支持帶參數,勉強帶上參數的話,不是報文件不存在,就是報路徑不能有非法字符。看了InstallHelper的源碼,發現它會把路徑和參數整個套進一對雙引號,這樣在傳遞給更底層的安裝方法時,底層方法會將該字串視為一個路徑,自然不是一個合法的路徑。但要我去用sc.exe,那是一萬個不願意,倫家可是一個有追求的碼屌好不好。所以好好研究了一下InstallHelper的實現,大概套路是這樣:

為exe所屬程序集創建AssemblyInstaller,AssemblyInstaller會負責創建程序集中所有標記有RunInstallerAttribute特性的類的實例,並將它們加入一個installer集合,最后由TransactedInstaller埃個執行這些installer,實際上就是各個installer實例執行各自的Install方法。對於服務(ServiceBase類)來說,用VS添加安裝程序后,便會自動生成一個叫ProjectInstaller的類,這個類就標有RunInstallerAttribute特性。ProjectInstaller類本身會攜帶兩個installer實例,分別屬於ServiceProcessInstaller和ServiceInstaller類,ProjectInstaller得到執行的時候會把這倆實例也加入上述installer集合,所以最終執行的是這倆實例的Install方法。其中ServiceProcessInstaller的Install並不真正執行啥玩意兒,它只是攜帶一些信息(比如運行服務的帳戶),供ServiceInstaller的Install取用,真正執行安裝服務這個事的是ServiceInstaller的Install方法。

——然而上面這段話並沒有什么卵用,不懂的童鞋還得自己看源碼推敲才能弄懂。記住一點就好,服務的安裝是System.ServiceProcess.ServiceInstaller的Install方法起作用,上面一堆XXXInstaller都是浮雲。而ServiceInstaller.Install內部正是調用CreateService這個系統API來執行服務的安裝。所以歸根結底,溯本追源,CreateService才是正宗,當然它內部是啥玩意兒就不知道了。題外,一個exe中多個服務時,ServiceProcessInstaller須只有一個,而ServiceInstaller則是每個服務對應一個,看名字就知道,前者與服務承載進程有關,大家都在一個exe里,當然要共用進程設置,后者則是存儲每個服務的設置,自然要一一對應。

回到正題,弄清InstallHelper最終是調用CreateService后,直接看后者支不支持帶參數安裝就行了,答案顯然是支持的(該API文檔在此),遂寫了個基於API的操作類,問題解決。

該操作類目前僅提供Install和Uninstall倆方法,用以替代InstallHelper,至於對服務的其它操作(啟動/停止等),System.ServiceProcess.ServiceController類已經有完備的實現,時間倉促,無暇造輪,后面有閑心再說。

方案源碼:

代碼不少,如果只是實現Install會很少,這主要是搞Uninstall帶來的,因為卸載前要考慮先停止,停止就要考慮先停止從屬服務,所以環環相扣,API一個接一個封裝,就成這樣了。

注:只支持安裝自有進程服務,不支持共享進程服務。即只支持一個exe里只承載一個服務的情況,不支持多服務共享一個exe的情況。這是由CreateService的dwServiceType參數指定的,Install已寫死為SERVICE_WIN32_OWN_PROCESS常量,即自有進程類服務。

using System;
using System.ComponentModel;
using System.Runtime.ConstrainedExecution;
using System.Runtime.InteropServices;
using System.Text;
using System.Threading;

namespace AhDung
{
    #region 異常定義

    /// <summary>
    /// 服務不存在異常
    /// </summary>
    public class ServiceNotExistException : ApplicationException
    {
        public ServiceNotExistException() : base("服務不存在!") { }

        public ServiceNotExistException(string message) : base(message) { }
    }

    #endregion

    #region 枚舉定義

    /// <summary>
    /// 服務啟動類型
    /// </summary>
    public enum ServiceStartType
    {
        Boot,
        System,
        Auto,
        Manual,
        Disabled
    }

    /// <summary>
    /// 服務運行帳戶
    /// </summary>
    public enum ServiceAccount
    {
        LocalSystem,
        LocalService,
        NetworkService,
    }

    #endregion

    /// <summary>
    /// Windows 服務輔助類
    /// </summary>
    public static class ServiceHelper
    {
        #region 公開方法

        /// <summary>
        /// 安裝服務
        /// </summary>
        /// <param name="serviceName">服務名</param>
        /// <param name="displayName">友好名稱</param>
        /// <param name="binaryFilePath">映像文件路徑,可帶參數</param>
        /// <param name="description">服務描述</param>
        /// <param name="startType">啟動類型</param>
        /// <param name="account">啟動賬戶</param>
        /// <param name="dependencies">依賴服務</param>
        public static void Install(string serviceName, string displayName, string binaryFilePath, string description, ServiceStartType startType, ServiceAccount account = ServiceAccount.LocalSystem, string[] dependencies = null)
        {
            IntPtr scm = OpenSCManager();

            IntPtr service = IntPtr.Zero;
            try
            {
                service = Win32Class.CreateService(scm, serviceName, displayName, Win32Class.SERVICE_ALL_ACCESS, Win32Class.SERVICE_WIN32_OWN_PROCESS, startType, Win32Class.SERVICE_ERROR_NORMAL, binaryFilePath, null, IntPtr.Zero, ProcessDependencies(dependencies), GetServiceAccountName(account), null);

                if (service == IntPtr.Zero)
                {
                    if (Marshal.GetLastWin32Error() == 0x431)//ERROR_SERVICE_EXISTS
                    { throw new ApplicationException("服務已存在!"); }

                    throw new ApplicationException("服務安裝失敗!");
                }

                //設置服務描述
                Win32Class.SERVICE_DESCRIPTION sd = new Win32Class.SERVICE_DESCRIPTION();
                try
                {
                    sd.description = Marshal.StringToHGlobalUni(description);
                    Win32Class.ChangeServiceConfig2(service, 1, ref sd);
                }
                finally
                {
                    Marshal.FreeHGlobal(sd.description); //釋放
                }
            }
            finally
            {
                if (service != IntPtr.Zero)
                {
                    Win32Class.CloseServiceHandle(service);
                }
                Win32Class.CloseServiceHandle(scm);
            }
        }

        /// <summary>
        /// 卸載服務
        /// </summary>
        /// <param name="serviceName">服務名</param>
        public static void Uninstall(string serviceName)
        {
            IntPtr scmHandle = IntPtr.Zero;
            IntPtr service = IntPtr.Zero;

            try
            {
                service = OpenService(serviceName, out scmHandle);

                StopService(service); //停止服務。里面會遞歸停止從屬服務

                if (!Win32Class.DeleteService(service) && Marshal.GetLastWin32Error() != 0x430) //忽略已標記為刪除的服務。ERROR_SERVICE_MARKED_FOR_DELETE
                {
                    throw new ApplicationException("刪除服務失敗!");
                }
            }
            catch (ServiceNotExistException) { } //忽略服務不存在的情況
            finally
            {
                if (service != IntPtr.Zero)
                {
                    Win32Class.CloseServiceHandle(service);
                    Win32Class.CloseServiceHandle(scmHandle);//放if里面是因為如果服務打開失敗,在OpenService里就已釋放SCM
                }
            }
        }

        #endregion

        #region 輔助方法

        /// <summary>
        /// 轉換帳戶枚舉為有效參數
        /// </summary>
        private static string GetServiceAccountName(ServiceAccount account)
        {
            if (account == ServiceAccount.LocalService)
            {
                return @"NT AUTHORITY\LocalService";
            }
            if (account == ServiceAccount.NetworkService)
            {
                return @"NT AUTHORITY\NetworkService";
            }
            return null;
        }

        /// <summary>
        /// 處理依賴服務參數
        /// </summary>
        private static string ProcessDependencies(string[] dependencies)
        {
            if (dependencies == null || dependencies.Length == 0)
            {
                return null;
            }

            StringBuilder sb = new StringBuilder();
            foreach (string s in dependencies)
            {
                sb.Append(s).Append('\0');
            }
            sb.Append('\0');

            return sb.ToString();
        }

        #endregion

        #region API 封裝方法

        /// <summary>
        /// 打開服務管理器
        /// </summary>
        private static IntPtr OpenSCManager()
        {
            IntPtr scm = Win32Class.OpenSCManager(null, null, Win32Class.SC_MANAGER_ALL_ACCESS);

            if (scm == IntPtr.Zero)
            {
                throw new ApplicationException("打開服務管理器失敗!");
            }

            return scm;
        }

        /// <summary>
        /// 打開服務
        /// </summary>
        /// <param name="serviceName">服務名稱</param>
        /// <param name="scmHandle">服務管理器句柄。供調用者釋放</param>
        private static IntPtr OpenService(string serviceName, out IntPtr scmHandle)
        {
            scmHandle = OpenSCManager();

            IntPtr service = Win32Class.OpenService(scmHandle, serviceName, Win32Class.SERVICE_ALL_ACCESS);

            if (service == IntPtr.Zero)
            {
                int errCode = Marshal.GetLastWin32Error();

                Win32Class.CloseServiceHandle(scmHandle); //關閉SCM

                if (errCode == 0x424) //ERROR_SERVICE_DOES_NOT_EXIST
                {
                    throw new ServiceNotExistException();
                }

                throw new Win32Exception();
            }

            return service;
        }

        /// <summary>
        /// 停止服務
        /// </summary>
        private static void StopService(IntPtr service)
        {
            ServiceState currState = GetServiceStatus(service);

            if (currState == ServiceState.Stopped)
            {
                return;
            }

            if (currState != ServiceState.StopPending)
            {
                //遞歸停止從屬服務
                string[] childSvs = EnumDependentServices(service, EnumServiceState.Active);
                if (childSvs.Length != 0)
                {
                    IntPtr scm = OpenSCManager();
                    try
                    {
                        foreach (string childSv in childSvs)
                        {
                            StopService(Win32Class.OpenService(scm, childSv, Win32Class.SERVICE_STOP));
                        }
                    }
                    finally
                    {
                        Win32Class.CloseServiceHandle(scm);
                    }
                }

                Win32Class.SERVICE_STATUS status = new Win32Class.SERVICE_STATUS();
                Win32Class.ControlService(service, Win32Class.SERVICE_CONTROL_STOP, ref status); //發送停止指令
            }

            if (!WaitForStatus(service, ServiceState.Stopped, new TimeSpan(0, 0, 30)))
            {
                throw new ApplicationException("停止服務失敗!");
            }
        }

        /// <summary>
        /// 遍歷從屬服務
        /// </summary>
        /// <param name="serviceHandle"></param>
        /// <param name="state">選擇性遍歷(活動、非活動、全部)</param>
        private static string[] EnumDependentServices(IntPtr serviceHandle, EnumServiceState state)
        {
            int bytesNeeded = 0; //存放從屬服務的空間大小,由API返回
            int numEnumerated = 0; //從屬服務數,由API返回

            //先嘗試以空結構獲取,如獲取成功說明從屬服務為空,否則拿到上述倆值
            if (Win32Class.EnumDependentServices(serviceHandle, state, IntPtr.Zero, 0, ref bytesNeeded, ref numEnumerated))
            {
                return new string[0];
            }
            if (Marshal.GetLastWin32Error() != 0xEA) //僅當錯誤值不是大小不夠(ERROR_MORE_DATA)時才拋異常
            {
                throw new Win32Exception();
            }

            //在非托管區域創建指針
            IntPtr structsStart = Marshal.AllocHGlobal(new IntPtr(bytesNeeded));
            try
            {
                //往上述指針處塞存放從屬服務的結構組,每個從屬服務是一個結構
                if (!Win32Class.EnumDependentServices(serviceHandle, state, structsStart, bytesNeeded, ref bytesNeeded, ref numEnumerated))
                {
                    throw new Win32Exception();
                }

                string[] dependentServices = new string[numEnumerated];
                int sizeOfStruct = Marshal.SizeOf(typeof(Win32Class.ENUM_SERVICE_STATUS)); //每個結構的大小
                long structsStartAsInt64 = structsStart.ToInt64();
                for (int i = 0; i < numEnumerated; i++)
                {
                    Win32Class.ENUM_SERVICE_STATUS structure = new Win32Class.ENUM_SERVICE_STATUS();
                    IntPtr ptr = new IntPtr(structsStartAsInt64 + i * sizeOfStruct); //根據起始指針、結構次序和結構大小推算各結構起始指針
                    Marshal.PtrToStructure(ptr, structure); //根據指針拿到結構
                    dependentServices[i] = structure.serviceName; //從結構中拿到服務名
                }

                return dependentServices;
            }
            finally
            {
                Marshal.FreeHGlobal(structsStart);
            }
        }

        /// <summary>
        /// 獲取服務狀態
        /// </summary>
        private static ServiceState GetServiceStatus(IntPtr service)
        {
            Win32Class.SERVICE_STATUS status = new Win32Class.SERVICE_STATUS();

            if (!Win32Class.QueryServiceStatus(service, ref status))
            {
                throw new ApplicationException("獲取服務狀態出錯!");
            }

            return status.currentState;
        }

        /// <summary>
        /// 等候服務至目標狀態
        /// </summary>
        private static bool WaitForStatus(IntPtr serviceHandle, ServiceState desiredStatus, TimeSpan timeout)
        {
            DateTime startTime = DateTime.Now;

            while (GetServiceStatus(serviceHandle) != desiredStatus)
            {
                if (DateTime.Now - startTime > timeout) { return false; }

                Thread.Sleep(200);
            }

            return true;
        }

        #endregion

        #region 嵌套類

        /// <summary>
        /// Win32 API相關
        /// </summary>
        private static class Win32Class
        {
            #region 常量定義

            /// <summary>
            /// 打開服務管理器時請求的權限:全部
            /// </summary>
            public const int SC_MANAGER_ALL_ACCESS = 0xF003F;

            /// <summary>
            /// 服務類型:自有進程類服務
            /// </summary>
            public const int SERVICE_WIN32_OWN_PROCESS = 0x10;

            /// <summary>
            /// 打開服務時請求的權限:全部
            /// </summary>
            public const int SERVICE_ALL_ACCESS = 0xF01FF;

            /// <summary>
            /// 打開服務時請求的權限:停止
            /// </summary>
            public const int SERVICE_STOP = 0x20;

            /// <summary>
            /// 服務操作標記:停止
            /// </summary>
            public const int SERVICE_CONTROL_STOP = 0x1;

            /// <summary>
            /// 服務出錯行為標記
            /// </summary>
            public const int SERVICE_ERROR_NORMAL = 0x1;

            #endregion

            #region API所需類和結構定義

            /// <summary>
            /// 服務狀態結構體
            /// </summary>
            [StructLayout(LayoutKind.Sequential)]
            public struct SERVICE_STATUS
            {
                public int serviceType;
                public ServiceState currentState;
                public int controlsAccepted;
                public int win32ExitCode;
                public int serviceSpecificExitCode;
                public int checkPoint;
                public int waitHint;
            }

            /// <summary>
            /// 服務描述結構體
            /// </summary>
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public struct SERVICE_DESCRIPTION
            {
                public IntPtr description;
            }

            /// <summary>
            /// 服務狀態結構體。遍歷API會用到
            /// </summary>
            [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
            public class ENUM_SERVICE_STATUS
            {
                public string serviceName;
                public string displayName;
                public int serviceType;
                public int currentState;
                public int controlsAccepted;
                public int win32ExitCode;
                public int serviceSpecificExitCode;
                public int checkPoint;
                public int waitHint;
            }

            #endregion

            #region API定義

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool ChangeServiceConfig2(IntPtr serviceHandle, uint infoLevel, ref SERVICE_DESCRIPTION serviceDesc);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern IntPtr OpenSCManager(string machineName, string databaseName, int dwDesiredAccess);

            [DllImport("advapi32.dll", SetLastError = true, CharSet = CharSet.Auto)]
            public static extern IntPtr OpenService(IntPtr hSCManager, string lpServiceName, int dwDesiredAccess);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern IntPtr CreateService(IntPtr hSCManager, string lpServiceName, string lpDisplayName, int dwDesiredAccess, int dwServiceType, ServiceStartType dwStartType, int dwErrorControl, string lpBinaryPathName, string lpLoadOrderGroup, IntPtr lpdwTagId, string lpDependencies, string lpServiceStartName, string lpPassword);

            [ReliabilityContract(Consistency.WillNotCorruptState, Cer.Success), DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool CloseServiceHandle(IntPtr handle);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool QueryServiceStatus(IntPtr hService, ref SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool DeleteService(IntPtr serviceHandle);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool ControlService(IntPtr hService, int dwControl, ref SERVICE_STATUS lpServiceStatus);

            [DllImport("advapi32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
            public static extern bool EnumDependentServices(IntPtr serviceHandle, EnumServiceState serviceState, IntPtr bufferOfENUM_SERVICE_STATUS, int bufSize, ref int bytesNeeded, ref int numEnumerated);

            #endregion
        }

        #endregion

        /// <summary>
        /// 服務狀態枚舉。用於遍歷從屬服務API
        /// </summary>
        private enum EnumServiceState
        {
            Active = 1,
            //InActive = 2,
            //All = 3
        }

        /// <summary>
        /// 服務狀態
        /// </summary>
        private enum ServiceState
        {
            Stopped = 1,
            //StartPending = 2,
            StopPending = 3,
            //Running = 4,
            //ContinuePending = 5,
            //PausePending = 6,
            //Paused = 7
        }
    }
}
ServiceHelper.cs

使用示例:

由於是直接用的API安裝,等於已經繞過了托管方法的一堆邏輯,所以不再需要在VS中為服務添加安裝程序(即VS自動生成的ProjectInstaller類和它攜帶的ServiceProcessInstaller和ServiceInstaller都可以省了),添加了也沒用。直接如下,傳入各個信息即可:

//安裝
ServiceHelper.Install(
    "test",                                // 服務名
    "Test Sv",                             // 顯示名稱
    @"""C:\新建 文件夾\test.exe"" /s",      // 映像路徑,可帶參數,若路徑有空格,需給路徑(不含參數)套上雙引號
    "描述描述描述",                         // 服務描述
    ServiceStartType.Auto,                 // 啟動類型
    ServiceAccount.LocalService,           // 運行帳戶,可選,默認是LocalSystem,即至尊帳戶
    new[] { "AudioSrv", "WebClient" }      // 依賴服務,要填服務名稱,沒有則為null或空數組,可選
    );

//卸載
ServiceHelper.Uninstall("test");

最后通過一張圖說明一下服務的各個概念:

-文畢-


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM