single.php

C#でWindowsサービスからユーザーセッションで別プロセスを起動する方法

Windowsサービスから、ユーザーセッションで別の実行可能ファイルを起動する場合の手順についてを備忘録的に投稿します。

Windowsサービスのアプリ起動制限

[サービス]を利用して開始したアプリから次のようなコードで[メモ帳]を起動する場合に、アプリが表示されない場合があります。

public void Execute()
{
  ProcessStartInfo pi = new ProcessStartInfo();
  pi.UseShellExecute = true;
  pi.FileName = "notepad.exe";
  pi.Arguments = option;
  Process.Start(pi);
}

これは[サービス]で動作している起動したアプリが[System]セッションで動作しています。

そのため、[Process.Start]メソッドで起動する別アプリのセッションも同一になるためユーザーインタフェイスを伴うアプリケーションの起動に制限があるためです。

ユーザーセッションで起動

Win32APIの[CreateProcessAsUser]関数を利用するとログインしているユーザーセッションでプロセスを作成して別アプリの実行ができます。

例えばC#の場合は、こんな感じで構造体とWin32APIのインポートを行います。

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct STARTUPINFO
{
  public int cb;
  public IntPtr lpReserved;
  public IntPtr lpDesktop;
  public IntPtr lpTitle;
  public uint dwX;
  public uint dwY;
  public uint dwXSize;
  public uint dwYSize;
  public uint dwXCountChars;
  public uint dwYCountChars;
  public uint dwFillAttribute;
  public uint dwFlags;
  public ushort wShowWindow;
  public ushort cbReserved2;
  public IntPtr lpReserved2;
  public IntPtr hStdInput;
  public IntPtr hStdOutput;
  public IntPtr hStdError;
}

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct PROCESS_INFORMATION
{
  public IntPtr hProcess;
  public IntPtr hThread;
  public uint dwProcessId;
  public uint dwThreadId;
}

[DllImport(@"ADVAPI32.dll", CharSet = CharSet.Unicode)]
public extern static Int32 CreateProcessAsUser(IntPtr hToken, IntPtr lpApplicationName, string lpCommandLine, IntPtr lpProcessAttributes, IntPtr lpThreadAttributes, Int32 bInheritHandles, UInt32 dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation);

[DllImport(@"Kernel32.dll")]
public extern static Int32 CloseHandle(IntPtr hObject);

[DllImport(@"Kernel32.dll")]
public extern static UInt32 WTSGetActiveConsoleSessionId();

[DllImport(@"WTSAPI32.dll")]
public extern static Int32 WTSQueryUserToken(UInt32 SessionId, out IntPtr phToken);

実際にアプリを起動する場合は、こんな感じで呼び出します。

public void Execute(string filename)
{
  uint sessionId = WTSGetActiveConsoleSessionId();
  IntPtr userToken;

  if (WTSQueryUserToken(sessionId, out userToken) == 0)
  {
    return;
  }

  STARTUPINFO startupInfo = new STARTUPINFO();
  startupInfo.cb = Marshal.SizeOf(startupInfo);

  PROCESS_INFORMATION pi;
  int result = CreateProcessAsUser(
        userToken,
        IntPtr.Zero,
        filename,
        IntPtr.Zero,
        IntPtr.Zero,
        0,
        0,
        IntPtr.Zero,
        System.IO.Directory.GetCurrentDirectory(),
        ref startupInfo,
        out pi);

    CloseHandle(pi.hThread);
    CloseHandle(pi.hProcess);
    CloseHandle(userToken);
}

実行するとログインしているユーザーのプロセスでアプリが実行されるので、[Windows サービス]などシステムアカウントが実行しているプロセス内からユーザーセッションでアプリの起動が可能になります。

まとめ

Windowsサービスから、ユーザーセッションで別の実行可能ファイルを起動する場合の手順について書きました。

[サービス]で動作している起動したアプリが[System]セッションで動作します。

また[Process.Start]メソッドで別アプリを起動した場合のセッションも同一になるためユーザーインタフェイスを伴うアプリケーションの起動に制限があります。

ユーザーセッションで起動する場合には[WTSGetActiveConsoleSessionId]関数で取得した値を使ってアクティブなユーザーのTokenを利用して、プロセスを作成する必要があります。

Windowsサービスなどからメモ帳などの別アプリを起動したい人の参考になれば幸いです。

スポンサーリンク

最後までご覧いただき、ありがとうございます。

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です