首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> .NET 網頁程式設計 >> ASP.NET 基礎 >> ASP.NET 基礎

.NET 實作可互動的 Windows 服務完整範例

     編輯:ASP.NET 基礎

前言:為什麼需要互動式服務?

近期需要開發一個檔案監控服務,查閱大量網路資料發現,大多數文章僅說明如何建立基本的 Windows 服務,卻鮮少提及如何與登入的使用者進行互動。根據 MSDN 文件說明,Windows 服務運行於非互動的視窗站與桌面環境中,若直接在服務中彈出視窗或對話框,將會不可見甚至導致程式停止回應。.NET Framework 內建的 ServiceBase 類別不支援與互動桌面溝通,若必須實現互動功能(如運行時參數設定、托盤圖示顯示),必須透過 P/Invoke 呼叫非受控的 Windows API。

核心原理:切換視窗站與桌面

要在服務中顯示 UI,關鍵在於將執行緒的桌面環境切換至互動使用者的 WinSta0\\Default 桌面。主要步驟如下:

  1. 在 OnStart 中啟動獨立執行緒(不可直接在主執行緒建立表單)。
  2. 執行緒內呼叫 OpenWindowStation 開啟互動視窗站 WinSta0。
  3. 以 SetProcessWindowStation 將程序關聯至該視窗站。
  4. 呼叫 OpenDesktop 取得 Default 桌面控制代碼。
  5. 以 SetThreadDesktop 將當前執行緒繫結至互動桌面。
  6. 在該執行緒上啟動 Application.Run(new Form1()) 進入訊息迴圈。
  7. 服務停止時,務必還原原始視窗站與桌面,並正確釋放控制代碼。

完整程式碼實作

using System;
using System.ComponentModel;
using System.Diagnostics;
using System.ServiceProcess;
using System.Threading;
using System.Runtime.InteropServices;

namespace FileWatchService
{
    public class FileWatchService : ServiceBase
    {
        private Thread _uiThread;
        private IContainer components;

        public FileWatchService()
        {
            InitializeComponent();
        }

        private void InitializeComponent()
        {
            this.ServiceName = "JadeWatchService";
        }

        [STAThread]
        static void Main()
        {
            ServiceBase.Run(new FileWatchService());
        }

        protected override void Dispose(bool disposing)
        {
            if (disposing && components != null)
                components.Dispose();
            base.Dispose(disposing);
        }

        protected override void OnStart(string[] args)
        {
            _uiThread = new Thread(ShowInteractiveForm);
            _uiThread.SetApartmentState(ApartmentState.STA);
            _uiThread.Start();
        }

        protected override void OnStop()
        {
            if (_uiThread != null && _uiThread.IsAlive)
            {
                _uiThread.Abort();
                _uiThread = null;
            }
        }

        private void ShowInteractiveForm()
        {
            // 取得目前程序與執行緒的視窗站/桌面控制代碼,稍後需還原
            IntPtr hWinStaSave = GetProcessWindowStation();
            IntPtr hThreadId = GetCurrentThreadId();
            IntPtr hDeskSave = GetThreadDesktop(hThreadId);

            // 開啟互動視窗站 WinSta0
            IntPtr hWinStaUser = OpenWindowStation("WinSta0", false, 0x033F); // MAXIMUM_ALLOWED
            if (hWinStaUser == IntPtr.Zero)
            {
                RpcRevertToSelf();
                return;
            }

            if (!SetProcessWindowStation(hWinStaUser))
            {
                CloseWindowStation(hWinStaUser);
                return;
            }

            // 開啟互動桌面 Default
            IntPtr hDeskUser = OpenDesktop("Default", 0, false, 0x033F);
            RpcRevertToSelf(); // 還原模擬權限

            if (hDeskUser == IntPtr.Zero)
            {
                SetProcessWindowStation(hWinStaSave);
                CloseWindowStation(hWinStaUser);
                return;
            }

            if (!SetThreadDesktop(hDeskUser))
            {
                CloseDesktop(hDeskUser);
                SetProcessWindowStation(hWinStaSave);
                CloseWindowStation(hWinStaUser);
                return;
            }

            try
            {
                // 此處可建立帶有 NotifyIcon 的表單,實現托盤互動
                using (var form = new Form1())
                {
                    Application.Run(form);
                }
            }
            finally
            {
                // 還原環境並釋放資源
                SetThreadDesktop(hDeskSave);
                SetProcessWindowStation(hWinStaSave);
                CloseDesktop(hDeskUser);
                CloseWindowStation(hWinStaUser);
            }
        }

        #region P/Invoke 宣告
        [DllImport("user32.dll")] private static extern int GetDesktopWindow();
        [DllImport("user32.dll")] private static extern IntPtr GetProcessWindowStation();
        [DllImport("kernel32.dll")] private static extern IntPtr GetCurrentThreadId();
        [DllImport("user32.dll")] private static extern IntPtr GetThreadDesktop(IntPtr dwThread);
        [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr OpenWindowStation(string lpszWinSta, bool fInherit, int dwDesiredAccess);
        [DllImport("user32.dll", CharSet = CharSet.Auto)] private static extern IntPtr OpenDesktop(string lpszDesktop, int dwFlags, bool fInherit, int dwDesiredAccess);
        [DllImport("user32.dll")] private static extern bool CloseDesktop(IntPtr hDesktop);
        [DllImport("rpcrt4.dll", SetLastError = true)] private static extern int RpcImpersonateClient(IntPtr bindingHandle);
        [DllImport("rpcrt4.dll", SetLastError = true)] private static extern int RpcRevertToSelf();
        [DllImport("user32.dll")] private static extern bool SetThreadDesktop(IntPtr hDesktop);
        [DllImport("user32.dll")] private static extern bool SetProcessWindowStation(IntPtr hWinSta);
        [DllImport("user32.dll")] private static extern bool CloseWindowStation(IntPtr hWinSta);
        #endregion
    }
}

關鍵細節與常見坑點

  • 權限問題:服務帳戶需具備「與桌面互動」權限,或以 LocalSystem 身分執行。
  • 執行緒模型:UI 執行緒必須設為 STA ([STAThread] 或 SetApartmentState),否則 Windows Forms 會拋出例外。
  • 資源釋放:finally 區塊務必還原視窗站與桌面,否則後續服務啟動可能失敗,甚至影響系統穩定。
  • Vista/Win7 後的 Session 0 隔離:服務運行於 Session 0,使用者桌面位於 Session 1+,直接呼叫 OpenWindowStation("WinSta0") 僅適用於舊版 Windows;新版建議改用 IPC 機制(Named Pipe、WCF、TCP)讓獨立 GUI 應用程式與服務通訊,較為穩健且符合現代架構。

現代替代方案建議

考慮到 Session 0 隔離與 UAC 限制,微軟官方建議採用「服務 + 獨立桌面應用程式」架構:服務專注背景邏輯,透過 Named Pipe / WCF / gRPC 與啟動於使用者會話的 WPF/WinForms 托盤程式雙向通訊。此法相容性最佳,亦便於單元測試與維護。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved