Windowsサービスを自動起動にする方法|ServiceBaseでFileSystemWatcherを安定稼働&Console入出力を置き換える

Windowsサービス(ServiceBase)でファイル監視・コピー処理を動かしたいのに、OS起動時に自動開始しない/Console.WriteLine・ReadLine が原因で止まる――この2つは「サービスの仕組み」を押さえるとスッキリ解決できます。本記事では、自動起動の正しい設定場所、コンソール入出力の置き換え、FileSystemWatcher をサービスで安定稼働させる実装パターンとインストール手順をまとめて解説します。

目次

Windowsサービスを「OS起動時に自動開始」する結論:コードではなくサービス設定で決まる

まず大前提として、Windowsサービスの「OS起動時に自動開始するかどうか」は、アプリ側のコードで毎回判断するものではなく、Windowsのサービス制御マネージャー(SCM)が持つサービス設定(Startup type / StartType)で決まります。

つまり、サービスの実装(ServiceBase)をどれだけ頑張っても、StartType が「手動」になっていればOS起動時には自動開始されません。逆に、StartType が「自動」になっていれば、OS起動時にSCMがサービスを起動します。

起動種類(Startup type)の種類と選び方

起動種類意味おすすめ用途注意点
Automatic(自動)OS起動時に自動開始常駐監視(ファイル監視・キュー処理など)起動直後の依存先(ネットワーク/DB等)が未準備だと失敗しやすい
Automatic (Delayed Start)(自動(遅延開始))起動直後の負荷を避けて少し後に開始OS起動直後に必須ではない常駐処理起動が遅れる前提で設計(最初の処理遅延を許容)
Manual(手動)手動開始/依存サービスから開始常時は不要、必要時のみ開始する処理「自動起動したい」要件には不向き
Disabled(無効)開始不可一時停止・廃止当然起動しない

設定方法1:ServiceInstaller で StartType を Automatic にする(.NET Framework の定番)

Visual Studio の「Windowsサービス(ServiceBase)」で作った場合、インストーラー(Installer)を用意して、ServiceInstaller.StartType に ServiceStartMode.Automatic を設定するのが王道です。

using System.ComponentModel;
using System.Configuration.Install;
using System.ServiceProcess;

[RunInstaller(true)]
public class ProjectInstaller : Installer
{
    public ProjectInstaller()
    {
        var processInstaller = new ServiceProcessInstaller
        {
            // まずは LocalSystem で動く例(実運用では専用アカウント推奨)
            Account = ServiceAccount.LocalSystem
        };

        var serviceInstaller = new ServiceInstaller
        {
            ServiceName = "FileCopyWatcherService",
            DisplayName = "File Copy Watcher Service",
            Description = "Monitors a folder and copies new files to destination.",
            StartType = ServiceStartMode.Automatic
        };

        Installers.Add(processInstaller);
        Installers.Add(serviceInstaller);
    }
}

インストールは環境により、InstallUtil.exe や MSI(セットアッププロジェクト)、あるいは管理ツールで行います。重要なのは「StartType を自動にした状態でサービスとして登録する」ことです。

設定方法2:sc.exe で作成/変更する(手早く確実)

インストーラーを作らずに、まずは動作確認したい場合は sc.exe が便利です。

REM サービス作成(binPath= の後ろは必ずスペースを入れる)
sc.exe create FileCopyWatcherService binPath= "C:\Services\FileCopyWatcherService.exe" start= auto

REM 既存サービスを自動起動へ変更
sc.exe config FileCopyWatcherService start= auto

REM 遅延開始にしたい場合(必要に応じて)
sc.exe config FileCopyWatcherService start= delayed-auto

なお、実行ファイルのパス(binPath)は絶対パスを推奨します。相対パスはサービスの実行カレント(多くの場合 C:\Windows\System32)に依存して事故の元です。

設定方法3:PowerShell(New-Service / Set-Service)

# 新規作成
New-Service -Name "FileCopyWatcherService" `
  -BinaryPathName "C:\Services\FileCopyWatcherService.exe" `
  -DisplayName "File Copy Watcher Service" `
  -Description "Monitors a folder and copies new files to destination." `
  -StartupType Automatic

# 変更

Set-Service -Name "FileCopyWatcherService" -StartupType Automatic

インストール方法の比較(どれを選ぶべきか)

方法メリットデメリット向いている場面
ServiceInstaller(InstallUtil/MSI)配布・運用がしやすい/StartType なども一括管理用意する手間がある社内配布・運用に載せる/継続運用前提
sc.exe最短で試せる/サーバー上でも確実配布の仕組みとしては弱い検証・暫定対応・現場の復旧
PowerShell自動化しやすい/スクリプト運用に強い権限や実行ポリシーの考慮が必要IaC的にサーバー構築/大量展開

Windowsサービスで Console が使えない理由:画面も入力も存在しない

Windowsサービスは基本的にユーザーのデスクトップ(対話セッション)とは切り離された環境で動きます。サービスに「コンソール画面」はなく、ユーザーがキー入力する場もありません。

この状態で Console.WriteLine() を呼んでも出力先がありません。さらに危険なのが Console.ReadLine() で、入力待ち状態になり、サービスは処理が止まります。サービスが起動中に止まってしまうと、ファイル監視やコピーも当然動きません。

Console を捨てて「ログ」と「設定」に分離するのが正攻法

サービスは無人運用が前提なので、Console の代わりに以下の2つへ置き換えると運用が安定します。

  • ログ出力:EventLog / ファイルログ / ETW / 外部ログ基盤
  • 設定入力:app.config / JSON / 環境変数 / レジストリ / 引数(ただしサービス起動時)

ログ出力の選択肢(おすすめ順の目安)

方式メリットデメリット向いているケース
Windowsイベントログ(EventLog)標準機能で管理しやすい/監視ツールと相性が良いソース登録に管理者権限が必要な場合があるサーバー運用・障害解析・監視連携
ファイルログ(例:ProgramData 配下)実装が容易/詳細ログを残しやすい権限・ローテーション・肥大化対策が必要詳細トレースが必要/イベントログが足りない
外部ログ(SIEM/クラウド監視)横断検索・可視化が強い導入コスト/ネットワーク依存大規模運用・監視が重要

EventLog を使う最小実装(サービスでの定番)

イベントログは「サービスがいつ起動し、どこで失敗したか」を追うのに強力です。特に OnStart で例外が出るとサービス起動自体が失敗するため、起動直後のログは重要になります。

using System;
using System.Diagnostics;
using System.ServiceProcess;

public partial class FileCopyWatcherService : ServiceBase
{
    private readonly EventLog _eventLog;

    public FileCopyWatcherService()
    {
        ServiceName = "FileCopyWatcherService";

        _eventLog = new EventLog();
        _eventLog.Source = "FileCopyWatcherService";
        _eventLog.Log = "Application";
    }

    protected override void OnStart(string[] args)
    {
        try
        {
            _eventLog.WriteEntry("Service starting...", EventLogEntryType.Information);
            // ここで監視開始などを行う(後述)
        }
        catch (Exception ex)
        {
            _eventLog.WriteEntry("Service failed to start: " + ex, EventLogEntryType.Error);
            throw; // 起動失敗としてSCMへ返す(監視しているなら再起動設定が効く)
        }
    }

    protected override void OnStop()
    {
        _eventLog.WriteEntry("Service stopping...", EventLogEntryType.Information);
    }
}

注意点として、イベントログの Source(上の例では FileCopyWatcherService)を初回登録するタイミングによっては管理者権限が必要です。配布するなら「インストール時に Source を作る」運用に寄せるのが安全です。

ファイルログを書くなら「書き込み先」と「権限」を最優先で設計する

サービスの実行ユーザー(LocalSystem、NetworkService、専用ユーザーなど)により、書ける場所が変わります。ありがちな事故は「デスクトップやユーザープロファイルに書こうとして失敗する」「C:\ 直下に書こうとして権限エラー」です。

迷ったら、ProgramData(例:C:\ProgramData\アプリ名)配下に作るのが運用上安全です。

using System;
using System.IO;
using System.Text;

public static class SimpleFileLogger
{
    private static readonly object _lock = new object();
    private static readonly string _logDir =
        Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
                     "FileCopyWatcherService", "logs");

    public static void Info(string message) => Write("INFO", message);
    public static void Error(string message) => Write("ERROR", message);

    private static void Write(string level, string message)
    {
        try
        {
            Directory.CreateDirectory(_logDir);

            var path = Path.Combine(_logDir, DateTime.UtcNow.ToString("yyyyMMdd") + ".log");
            var line = $"{DateTime.Now:yyyy-MM-dd HH:mm:ss.fff} [{level}] {message}";

            lock (_lock)
            {
                File.AppendAllText(path, line + Environment.NewLine, Encoding.UTF8);
            }
        }
        catch
        {
            // ここで例外を投げるとサービス全体が落ちうるので握りつぶすのも選択肢
            // (ただし、重要度に応じて EventLog へフォールバックするなどの設計も可)
        }
    }
}

FileSystemWatcher をサービスで動かす実装の要点:OnStart で開始、OnStop で確実に破棄

Windowsサービスでの基本パターンは次の通りです。

  • OnStart:監視・バックグラウンド処理を開始する
  • OnStop:監視を止め、リソースを Dispose する(後片付け)

このパターンを守ると、サービスの再起動や停止がきれいに行えます。特に FileSystemWatcher は OS リソースを使うため、停止時に Dispose しないと不具合の温床になります。

重要:OnStart は「すぐ戻る」ように作る

Service Control Manager はサービス起動時にタイムアウトを持っており、OnStart が長くブロックすると「起動失敗」と判断されることがあります。つまり、OnStart の中で重い処理(大量コピーや待機)を直接やらないのが鉄則です。

  • OnStart:監視開始、ワーカースレッド開始、タイマー開始など「開始の合図」だけ
  • 実処理:バックグラウンド(Task/Thread/Queue)で動かす

サービス向け FileSystemWatcher の実装例(キュー+リトライ+停止対応)

ファイル作成を検知してコピーする処理で頻出する問題は、次の2つです。

  • ファイルが「作成された」通知が来た時点では、まだ書き込み中でコピーすると壊れる
  • イベントが連打されたり、取りこぼしが起きたりする(Changed 多発/バッファ溢れ)

そこで、イベントハンドラは軽くしてキューへ積み、ワーカー側で「ファイルが安定するまで待ってからコピー」するのが堅い実装です。

using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.IO;
using System.ServiceProcess;
using System.Threading;
using System.Threading.Tasks;

public partial class FileCopyWatcherService : ServiceBase
{
    private FileSystemWatcher _watcher;
    private CancellationTokenSource _cts;
    private Task _worker;

    private readonly BlockingCollection<string> _queue = new BlockingCollection<string>();

    private readonly EventLog _eventLog;

    // 監視元・コピー先(本来は設定ファイルから読む)
    private readonly string _sourceDir = @"C:\Watch";
    private readonly string _destDir   = @"C:\Dest";

    public FileCopyWatcherService()
    {
        ServiceName = "FileCopyWatcherService";

        _eventLog = new EventLog
        {
            Source = "FileCopyWatcherService",
            Log = "Application"
        };
    }

    protected override void OnStart(string[] args)
    {
        try
        {
            _eventLog.WriteEntry("Service starting...", EventLogEntryType.Information);

            Directory.CreateDirectory(_sourceDir);
            Directory.CreateDirectory(_destDir);

            _cts = new CancellationTokenSource();

            // ワーカー開始(OnStart はすぐ戻る)
            _worker = Task.Run(() => WorkerLoop(_cts.Token), _cts.Token);

            // FileSystemWatcher セットアップ
            _watcher = new FileSystemWatcher(_sourceDir)
            {
                Filter = "*.*",
                IncludeSubdirectories = false,
                NotifyFilter = NotifyFilters.FileName | NotifyFilters.CreationTime | NotifyFilters.Size,
                InternalBufferSize = 64 * 1024 // 必要に応じて調整(上限あり)
            };

            _watcher.Created += OnCreated;
            _watcher.Error += OnWatcherError;

            _watcher.EnableRaisingEvents = true;

            _eventLog.WriteEntry($"Watching: {_sourceDir}", EventLogEntryType.Information);
        }
        catch (Exception ex)
        {
            _eventLog.WriteEntry("Service failed to start: " + ex, EventLogEntryType.Error);
            throw;
        }
    }

    private void OnCreated(object sender, FileSystemEventArgs e)
    {
        // ここは軽く:重い処理はしない
        if (string.IsNullOrWhiteSpace(e.FullPath)) return;

        // ディレクトリ作成通知などを弾きたい場合は存在チェック
        // (Created はファイルでもディレクトリでも飛ぶケースがある)
        _queue.Add(e.FullPath);
    }

    private void OnWatcherError(object sender, ErrorEventArgs e)
    {
        _eventLog.WriteEntry("FileSystemWatcher error: " + e.GetException(), EventLogEntryType.Warning);

        // 取りこぼしの可能性があるため、必要に応じてフォルダ全スキャンで補完する設計も検討
        // 例:タイマーで定期的に未処理ファイルを拾う等
    }

    private void WorkerLoop(CancellationToken ct)
    {
        foreach (var path in _queue.GetConsumingEnumerable(ct))
        {
            try
            {
                ProcessOne(path, ct);
            }
            catch (OperationCanceledException)
            {
                // 停止要求
                break;
            }
            catch (Exception ex)
            {
                _eventLog.WriteEntry("Processing failed: " + ex, EventLogEntryType.Error);
            }
        }
    }

    private void ProcessOne(string path, CancellationToken ct)
    {
        // ファイルが書き込み中の可能性があるので、安定するまで待ってからコピー
        WaitForFileReady(path, ct);

        var fileName = Path.GetFileName(path);
        var destPath = Path.Combine(_destDir, fileName);

        CopyWithRetry(path, destPath, ct);

        _eventLog.WriteEntry($"Copied: {path} -> {destPath}", EventLogEntryType.Information);
    }

    private static void WaitForFileReady(string path, CancellationToken ct)
    {
        // 「開ける」「サイズが一定」などで簡易判定
        const int maxWaitMs = 60_000;
        const int intervalMs = 500;

        var sw = Stopwatch.StartNew();
        long lastSize = -1;

        while (true)
        {
            ct.ThrowIfCancellationRequested();

            if (!File.Exists(path))
            {
                // 作成→即削除などもあり得る
                throw new FileNotFoundException("File not found (maybe removed before copy).", path);
            }

            try
            {
                var info = new FileInfo(path);
                var size = info.Length;

                // 連続してサイズが変わらないなら安定とみなす(必要なら回数を増やす)
                if (size == lastSize)
                {
                    // 排他ロックが外れているかも確認
                    using (File.Open(path, FileMode.Open, FileAccess.Read, FileShare.Read))
                    {
                        return;
                    }
                }

                lastSize = size;
            }
            catch (IOException)
            {
                // まだロック中の可能性
            }
            catch (UnauthorizedAccessException)
            {
                // 一時的にアクセスできない可能性(作成直後やACL)
            }

            if (sw.ElapsedMilliseconds > maxWaitMs)
            {
                throw new TimeoutException("Timed out waiting for file to become ready: " + path);
            }

            Thread.Sleep(intervalMs);
        }
    }

    private static void CopyWithRetry(string src, string dest, CancellationToken ct)
    {
        const int maxRetry = 10;
        const int delayMs = 1000;

        for (int i = 1; i <= maxRetry; i++)
        {
            ct.ThrowIfCancellationRequested();

            try
            {
                // 上書き可にする場合は true
                File.Copy(src, dest, overwrite: true);
                return;
            }
            catch (IOException) when (i < maxRetry)
            {
                Thread.Sleep(delayMs);
            }
        }

        // 最後まで失敗したら例外を上へ
        File.Copy(src, dest, overwrite: true);
    }

    protected override void OnStop()
    {
        _eventLog.WriteEntry("Service stopping...", EventLogEntryType.Information);

        try
        {
            _watcher?.Dispose();
            _watcher = null;

            if (_cts != null)
            {
                _cts.Cancel();

                // キュー完了を通知してワーカーを終わらせる
                _queue.CompleteAdding();

                try { _worker?.Wait(TimeSpan.FromSeconds(10)); } catch { /* ignore */ }

                _cts.Dispose();
                _cts = null;
            }
        }
        finally
        {
            _eventLog.WriteEntry("Service stopped.", EventLogEntryType.Information);
        }
    }
}

FileSystemWatcher の設定で差が出るポイント

項目推奨理由注意点
イベントハンドラ内の処理極力軽く(キュー投入のみ)連打に耐え、バッファ溢れを防ぐ重いコピーをハンドラでやると取りこぼし・遅延の原因
InternalBufferSize必要に応じて増やすイベントが多いと内部バッファ溢れが起きる無限に増やせない(上限あり)。増やしても根本は「処理を軽く」
NotifyFilter / Filter必要なものだけに絞るイベント量を減らせる絞りすぎると検知漏れになる
Error イベント必ずハンドルしてログ取りこぼしの兆候を検知できるError が出たら補完スキャンを検討

Console.ReadLine の代替:サービス停止は「OnStop」で受ける

コンソールアプリでは「Enterで終了」などを Console.ReadLine() で実装しがちですが、サービスでは終了トリガーはSCMからの停止要求です。つまり、停止処理は OnStop に実装し、サービスの停止ボタン/net stop/sc stop で止めます。

設定値を対話的に入力する設計もサービスとは相性が悪いので、次のように方針転換すると運用しやすくなります。

  • 監視フォルダ・コピー先:設定ファイル(app.config / JSON)
  • ログレベル:設定ファイル
  • 手動実行の操作:別の管理ツール(GUI/CLI)を用意してサービスに指示(ファイル/名前付きパイプ/HTTP等)

インストール後の確認:自動起動・ログ・権限の3点セット

services.msc で「自動」になっているか確認

Windowsのサービス管理(services.msc)で対象サービスを開き、スタートアップの種類が「自動」または「自動(遅延開始)」になっているかを確認します。

イベントビューアーで起動ログを見る

イベントログに書く設計にしておくと、起動時の状況を追いやすくなります。

  • イベントビューアー → Windowsログ → アプリケーション
  • ソース:FileCopyWatcherService(例)

サービス実行アカウントの権限を見直す

ファイル監視とコピーは「監視元フォルダの読み取り」「コピー先フォルダの書き込み」が必要です。ここが不足すると、サービスは起動しても実処理が失敗します。

実行アカウント特徴メリット注意点
LocalSystemローカルで強い権限ローカルディスクなら動きやすいネットワーク先(UNC等)へのアクセスで詰まりやすい
NetworkServiceネットワークアクセスでマシンアカウント扱いドメイン環境での共有アクセスが比較的やりやすい共有側で「コンピュータアカウント」に権限付与が必要な場合
専用ユーザー権限を最小化しやすい最も運用が安定しやすいパスワード管理、権限設定が必要

運用目線では、必要権限だけ付与した専用ユーザーでサービスを動かす設計が無難です(セキュリティ的にもおすすめ)。

よくあるトラブルと対策(サービス化でつまずくポイントを先回り)

症状原因の典型対策
サービスが開始できない(起動タイムアウト)OnStart で重い処理/待機をしているOnStart は即復帰、重い処理は Task/Queue へ。必要なら RequestAdditionalTime を検討
Console.ReadLine で止まるサービスにコンソール入力が存在しない対話入力を廃止し、設定ファイル+ログ出力へ。停止は OnStop で
コピーされたファイルが壊れる/サイズが0作成通知時点で書き込み中「ファイルが開ける」「サイズが安定」を確認してからコピー。リトライを入れる
監視イベントを取りこぼすイベント処理が重い/InternalBufferSize 溢れイベントはキュー投入のみにして軽量化。InternalBufferSize 調整+Error ハンドル+補完スキャン
相対パスでファイルが見つからないサービスのカレントが System32 になりがちパスは絶対パスに統一。設定ファイルもフルパスで
アクセス拒否実行アカウントに権限がない監視元/コピー先のACLを確認。専用ユーザー運用を検討

落ちたら再起動する「回復」設定も入れると運用が安定する

ファイル監視は長期稼働が前提です。想定外の例外や外部要因で止まったときに備え、サービスの「回復(Recovery)」を設定しておくと復旧が早くなります。

  • services.msc → 対象サービス → プロパティ → 回復 タブ
  • 「最初のエラー」「2回目のエラー」:サービスの再起動

コマンドで行う場合は sc.exe でも設定できます(例)。

REM 失敗時に 60秒後再起動(例)
sc.exe failure FileCopyWatcherService reset= 0 actions= restart/60000/restart/60000/restart/60000

デバッグの現実解:サービスを「コンソールでも動く形」にしておくと楽になる

Windowsサービスはデバッグしづらいのが弱点です。そこで開発時は「ユーザー対話セッションで動くときはコンソール風に実行」できる逃げ道を用意すると効率が上がります。

using System;
using System.ServiceProcess;
using System.Threading;

static class Program
{
    static void Main(string[] args)
    {
        // デバッグ時、対話環境ならサービスロジックを直接起動
        if (Environment.UserInteractive)
        {
            var svc = new FileCopyWatcherService();
            svc.DebugStart(args);

            Console.WriteLine("Running... press Enter to stop.");
            Console.ReadLine();

            svc.DebugStop();
            return;
        }

        // 通常はサービスとして起動
        ServiceBase.Run(new ServiceBase[] { new FileCopyWatcherService() });
    }
}

// ServiceBase の OnStart/OnStop を呼ぶためのデバッグ用メソッド
public partial class FileCopyWatcherService
{
    public void DebugStart(string[] args) => OnStart(args);
    public void DebugStop() => OnStop();
}

この形にしておくと、普段はコンソールで挙動を確認しつつ、最終的にはサービスとしてインストールして運用に載せられます。もちろん運用時のサービス本体には Console 依存を残さないのが前提です。

補足:.NET 6+ なら Worker Service テンプレートが現代的(ただし考え方は同じ)

最近の .NET(.NET 6 以降)では Worker Service テンプレートを使い、ホスティング(Generic Host)で Windowsサービス化するのが一般的になっています。とはいえ、要点は本記事で説明したものと同じで、

  • 自動起動はサービス設定(StartupType)
  • コンソールは前提にしない
  • 起動と停止のライフサイクルを守る
  • ログと例外処理、回復設定を整える

という軸は変わりません。

using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;

Host.CreateDefaultBuilder(args)
    .UseWindowsService()
    .ConfigureServices(services =>
    {
        services.AddHostedService<Worker>();
    })
    .Build()
    .Run();

サービス化で押さえるべきチェックリスト

チェック項目OKの状態理由
StartTypeAutomatic / Delayed-AutomaticOS起動時に自動で動かすため
Console依存WriteLine/ReadLine を排除サービスにはUI/入力がない
OnStart軽量で即復帰起動タイムアウト回避
監視処理キュー+ワーカーで処理イベント連打や取りこぼしに強い
停止処理OnStop で Dispose / Cancel再起動・停止が安定
ログEventLog やファイルログで記録障害解析ができる
権限監視元/コピー先へ必要な権限起動しても処理が失敗しない

まとめ:自動起動設定とコンソール排除が最初の分岐点

  • Windowsサービスの自動起動は、コードではなくサービスの起動種類(StartType)で決まる
  • サービスにはコンソールがないため、Console.WriteLine/ReadLine は捨ててログと設定へ分離する
  • FileSystemWatcher はOnStart で開始、OnStop で Dispose、イベント処理はキュー化して安定運用
  • 「書き込み中ファイル」「取りこぼし」「権限」「相対パス」など、サービス特有の落とし穴を先に潰す

この方針で組み直すと、OS起動時に安定して動く監視サービスに仕上がります。運用を見据えるなら、回復設定とログ整備まで含めて一気に整えてしまうのがおすすめです。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次