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

C# 開發 Windows Service 實戰 (下):客戶端控制介面、系統配置持久化、服務安裝部署全流程

     編輯:關於C#

承接上集服務核心實現,本文完善 **WinForm 客戶端控制介面**、**系統配置物件 (MyConfig) 與 Access 資料庫持久化**、**服務安裝/解除安裝/啟動/停止/暫停/繼續** 完整生命週期管理,並詳細說明 **命令列參數路由**、**InstallUtil 部署**、**除錯模式** 等生產級部署細節。

客戶端主視窗 (frmClient) UI 佈局

  • ToolStrip:重新整理、刪除記錄、系統設定、啟動服務、停止服務
  • ListView (Groups):顯示檔案系統操作日誌,依事件類型分組 (Created/Changed/Deleted/Renamed),圖示區分
  • StatusStrip:服務安裝狀態、記錄筆數
  • Timer (2秒):輪詢 ServiceController.Status 更新按鈕啟用狀態

服務控制按鈕實現


// 啟動服務
private void btnStartService_Click(object sender, EventArgs e)
{
    if (!_serviceInstalled) return;
    using (var sc = new ServiceController("MyFileSystemWatcher"))
    {
        if (sc.Status == ServiceControllerStatus.Stopped)
            sc.Start(); // 非阻塞,發送啟動命令即返回
    }
}

// 停止服務
private void btnStopService_Click(object sender, EventArgs e)
{
    if (!_serviceInstalled) return;
    using (var sc = new ServiceController("MyFileSystemWatcher"))
    {
        if (sc.Status == ServiceControllerStatus.Running)
            sc.Stop(); // 非阻塞
    }
}

// Timer 輪詢更新 UI
private void timer_Tick(object sender, EventArgs e)
{
    if (!_serviceInstalled) return;
    using (var sc = new ServiceController("MyFileSystemWatcher"))
    {
        btnStartService.Enabled = (sc.Status == ServiceControllerStatus.Stopped);
        btnStopService.Enabled = (sc.Status == ServiceControllerStatus.Running);
    }
}

系統設定對話框 (dlgConfig)

對應 MyConfig 物件屬性:

  • 監視事件勾選:重命名、修改、建立、刪除 (四個 CheckBox)
  • 監視路徑清單:ListBox + 新增/移除按鈕,支援通配符 (如 C:\Data\*.txt、D:\)
  • 確定按鈕 → MyConfig.Instance.Save() 寫入資料庫

MyConfig:單例配置物件與資料庫存取


public sealed class MyConfig
{
    private static readonly Lazy _instance = new(() => new MyConfig());
    public static MyConfig Instance => _instance.Value;

    // 屬性
    public bool LogRenamed { get; set; } = true;
    public bool LogChanged { get; set; } = true;
    public bool LogCreated { get; set; } = true;
    public bool LogDeleted { get; set; } = true;
    public string[] WatchedPaths { get; set; } = Array.Empty();

    private MyConfig() { }

    // 從 SystemConfig 表載入
    public void Load()
    {
        WatchedPaths = Array.Empty();
        var paths = new ArrayList();

        using (var cmd = Util.DBConnection.CreateCommand())
        {
            cmd.CommandText = "SELECT ConfigName, ConfigValue FROM SystemConfig";
            using (var reader = cmd.ExecuteReader())
            {
                while (reader.Read())
                {
                    string name = reader.GetString(0).Trim().ToLower();
                    string value = reader.GetString(1);
                    switch (name)
                    {
                        case "logrenamed": LogRenamed = Convert.ToBoolean(value); break;
                        case "logchanged": LogChanged = Convert.ToBoolean(value); break;
                        case "logdeleted": LogDeleted = Convert.ToBoolean(value); break;
                        case "logcreated": LogCreated = Convert.ToBoolean(value); break;
                        default:
                            if (name.StartsWith("path")) paths.Add(value.Trim());
                            break;
                    }
                }
            }
        }
        WatchedPaths = (string[])paths.ToArray(typeof(string));
    }

    // 存入 SystemConfig 表 (全刪重寫)
    public void Save()
    {
        using (var cmd = Util.DBConnection.CreateCommand())
        {
            cmd.CommandText = "DELETE FROM SystemConfig";
            cmd.ExecuteNonQuery();

            cmd.CommandText = "INSERT INTO SystemConfig (ConfigName, ConfigValue) VALUES (?, ?)";
            var pName = cmd.CreateParameter(); cmd.Parameters.Add(pName);
            var pValue = cmd.CreateParameter(); cmd.Parameters.Add(pValue);

            void Set(string n, string v) { pName.Value = n; pValue.Value = v; cmd.ExecuteNonQuery(); }

            Set("LogRenamed", LogRenamed.ToString());
            Set("LogChanged", LogChanged.ToString());
            Set("LogDeleted", LogDeleted.ToString());
            Set("LogCreated", LogCreated.ToString());

            for (int i = 0; i < WatchedPaths.Length; i++)
            {
                string p = WatchedPaths[i]?.Trim();
                if (!string.IsNullOrEmpty(p)) Set($"path{i}", p);
            }
        }
    }
}

檔案系統監視服務核心 (MyFileSystemWatcherService)


public partial class MyFileSystemWatcherService : ServiceBase
{
    private ArrayList _watchers;
    private EventLog _eventLog;

    public MyFileSystemWatcherService()
    {
        InitializeComponent();
        ServiceName = "MyFileSystemWatcher";
        _eventLog = new EventLog { Source = ServiceName };
        if (!EventLog.SourceExists(ServiceName))
            EventLog.CreateEventSource(ServiceName, "Application");
    }

    // 核心啟動邏輯
    internal bool StartFileSystemWatching()
    {
        _watchers = new ArrayList();
        MyConfig.Instance.Load();
        var paths = MyConfig.Instance.WatchedPaths;
        var sb = new StringBuilder();

        foreach (string path in paths)
        {
            if (!Path.IsPathRooted(path)) continue;

            string basePath, filter;
            if (Directory.Exists(path)) { basePath = path; filter = "*.*"; }
            else { basePath = Path.GetDirectoryName(path); filter = Path.GetFileName(path); }

            if (string.IsNullOrEmpty(basePath) || !Directory.Exists(basePath)) continue;
            // 避免監視自身程式目錄 (防止遞迴觸發)
            if (basePath.ToUpper().StartsWith(Application.StartupPath.ToUpper())) continue;

            if (sb.Length > 0) sb.Append(";");
            sb.Append(path);

            var watcher = new FileSystemWatcher(basePath, filter)
            {
                EnableRaisingEvents = true,
                IncludeSubdirectories = false // 需遞迴請設 true
            };

            // 匿名委託綁定事件 (C# 2.0 語法)
            if (MyConfig.Instance.LogChanged)
                watcher.Changed += (s, a) => WriteLog(a.FullPath, a.ChangeType.ToString());
            if (MyConfig.Instance.LogCreated)
                watcher.Created += (s, a) => WriteLog(a.FullPath, a.ChangeType.ToString());
            if (MyConfig.Instance.LogDeleted)
                watcher.Deleted += (s, a) => WriteLog(a.FullPath, a.ChangeType.ToString());
            if (MyConfig.Instance.LogRenamed)
                watcher.Renamed += (s, a) => WriteLog(a.FullPath, a.ChangeType.ToString());

            _watchers.Add(watcher);
        }

        _eventLog.WriteEntry($"開始監視: {sb}", EventLogEntryType.Information);
        return true;
    }

    private void WriteLog(string objectName, string eventStyle)
    {
        using (var cmd = Util.DBConnection.CreateCommand())
        {
            cmd.CommandText = "INSERT INTO FileSystemLog (RecordID, WatchTime, ObjectName, EventStyle) VALUES (?, ?, ?, ?)";
            cmd.Parameters.Add(cmd.CreateParameter()).Value = Guid.NewGuid().ToString();
            cmd.Parameters.Add(cmd.CreateParameter()).Value = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
            cmd.Parameters.Add(cmd.CreateParameter()).Value = objectName;
            cmd.Parameters.Add(cmd.CreateParameter()).Value = eventStyle;
            cmd.ExecuteNonQuery();
        }
    }

    // ServiceBase 覆寫
    protected override void OnStart(string[] args) => StartFileSystemWatching();

    protected override void OnStop()
    {
        if (_watchers != null)
        {
            foreach (FileSystemWatcher w in _watchers) { w.EnableRaisingEvents = false; w.Dispose(); }
            _watchers = null;
        }
        Util.CloseDBConnection();
        base.OnStop();
    }

    protected override void OnPause()
    {
        if (_watchers != null)
            foreach (FileSystemWatcher w in _watchers) w.EnableRaisingEvents = false;
        base.OnPause();
    }

    protected override void OnContinue()
    {
        if (_watchers != null)
            foreach (FileSystemWatcher w in _watchers) w.EnableRaisingEvents = true;
        base.OnContinue();
    }
}

資料庫連線管理


internal static class Util
{
    private static IDbConnection _conn;
    public static IDbConnection DBConnection
    {
        get
        {
            if (_conn == null)
            {
                string dbPath = Path.Combine(Application.StartupPath, "FileSystemWatcher.mdb");
                _conn = new OleDbConnection($@"Provider=Microsoft.Jet.OLEDB.4.0;Data Source=""{dbPath}""");
                _conn.Open();
            }
            return _conn;
        }
    }
    public static void CloseDBConnection()
    {
        _conn?.Close();
        _conn = null;
    }
}
  • 單一連線物件,**服務運行期間保持開啟**,避免頻繁開關效能損耗。
  • Access (Jet OLEDB) 適合輕量級本地服務;生產環境建議改用 SQLite / SQL Server Express / PostgreSQL。

程式進入點與命令列路由 (Program.cs)


[STAThread]
static void Main(string[] args)
{
    try
    {
        // 定位 InstallUtil.exe (與 mscorlib.dll 同目錄)
        string runtimeDir = Path.GetDirectoryName(typeof(string).Assembly.CodeBase.Replace("file:///", ""));
        string installUtil = Path.Combine(runtimeDir, "InstallUtil.exe");

        foreach (string arg in args)
        {
            switch (arg.ToLower())
            {
                case "/install":
                    Process.Start(installUtil, $"\"{Application.ExecutablePath}\"");
                    return;
                case "/uninstall":
                    Process.Start(installUtil, $"/u \"{Application.ExecutablePath}\"");
                    return;
                case "/client":
                    Application.EnableVisualStyles();
                    Application.SetCompatibleTextRenderingDefault(false);
                    using (var frm = new frmClient())
                    {
                        Application.Run(frm);
                        Util.CloseDBConnection();
                    }
                    return;
                case "/debug":
                    // 直接在主執行緒啟動監視,方便 VS 斷點調試 OnStart 邏輯
                    var svc = new MyFileSystemWatcherService();
                    svc.StartFileSystemWatching();
                    Thread.Sleep(Timeout.Infinite); // 阻塞主執行緒
                    return;
            }
        }
    }
    catch (Exception ex) { Console.WriteLine(ex); return; }

    // 無參數 → 服務模式
    ServiceBase.Run(new MyFileSystemWatcherService());
}

部署與運行指令

操作指令
安裝服務MyWindowsService.exe /install 或 InstallUtil.exe MyWindowsService.exe
解除安裝MyWindowsService.exe /uninstall 或 InstallUtil.exe /u MyWindowsService.exe
啟動客戶端MyWindowsService.exe /client
除錯服務邏輯VS 設定專案啟動參數 /debug,直接 F5 斷點調試
服務管理器啟動服務 MMC (services.msc) → 「MyFileSystemWatcher」→ 啟動

資料庫結構 (Access MDB)

  • FileSystemLog:RecordID (PK, Text 50)、WatchTime (Text 20)、ObjectName (Text 250)、EventStyle (Text 50: Created/Changed/Deleted/Renamed)
  • SystemConfig:ConfigName (PK, Text 50)、ConfigValue (Text 250) — 範例:LogChanged=True, path0=C:\Data\*.txt

常見問題與避坑

  1. 服務無法存取網路路徑:LocalSystem 帳號無網路權限 → 服務屬性「登入」改為具網路權限的網域帳號。
  2. FileSystemWatcher 事件漏失/重複:內部緩衝區溢位 (預設 8KB) → 設 InternalBufferSize = 64*1024;網路磁碟機事件不可靠 → 改用輪詢/USN Journal。
  3. Access 並發鎖死:服務單連線寫入、客戶端讀取共用連線 → 改用連線池或 SQLite (WAL 模式)。
  4. 安裝需管理員權限:InstallUtil 需以系統管理員身分執行命令提示字元。

總結

本系列完整實現了:

  1. 服務核心:ServiceBase 衍生、FileSystemWatcher 多目錄監視、匿名委託事件綁定、生命週期 (Start/Stop/Pause/Continue) 處理。
  2. 持久化配置:MyConfig 單例、Access 參數化 SQL、全刪重寫簡化邏輯。
  3. 客戶端控制:ServiceController 非阻塞操作、Timer 輪詢狀態同步 UI、ListView 分組顯示日誌。
  4. 單一 EXE 多模式:命令列參數路由服務/客戶端/安裝/除錯,InstallUtil 自動定位部署。

.NET 封裝了底層 SCM (Service Control Manager) 複雜 API,讓開發者專注於 **業務邏輯 (檔案監視、日誌寫入、配置管理)**,以極低成本構建穩健的 Windows 後台服務。

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