[C#] Windowsサービスインストール時に別アプリケーション.exeファイルをログインユーザで実行する方法


はじめに

Windowsサービス単体の作成ではなく、Windowsサービスと対話的な処理を行うデスクトップアプリケーションとセットで作成し、インストール時にはどちらも起動したいとなった場合、Local Systemとして起動してしまいます。

今回は、デスクトップアプリケーション側の方をログインしているユーザ(インストールユーザ)として起動する方法の備忘録となります。

環境

検証した環境は下記となります。

項目 説明
OS Windows 10 20H2 Pro 64bit
IDE Visual Studio 2019 Professional

プロジェクトの作成と準備

まず、事前準備として、Windowsサービスアプリケーションプロジェクトを作成します。

  1. Visual Studioを起動し、新しいプロジェクトの作成を行います。プロジェクトは「Windows サービス (.NET Framework)」を選択します。

    画像1
  2. プロジェクト名やソリューション名を入力し、「作成」をクリックし作成します。

    画像2
  3. デザイナ画面が表示されるので、右クリックし「インストーラーの追加」を選択しインストーラを追加します。

    画像3
  4. すると、下記のように「ProjectInstaller.cs」が作成され、serviceProcessInstaller1とserviceInstaller1がデザイナ画面に作成されます。

    画像4

まずは、上記まで説明しますが、以降必要な詳細設定(サービス名など)については割愛します。

サービスのコミット時にデスクトップアプリケーションを起動する

アプリケーションを起動するには「Process.Start」よく利用するかと思います。

しかし、インストール時にはSystemユーザでサービスが起動するためデスクトップ側も自動的にSystemユーザで起動してしまいます。

この起動するユーザをインストールしたユーザ(ログインユーザ)で起動するようにしたいと思います。

まずは、ProjectInstallerのコードを表示し下記のように記載します。


〜 public override void Commit (System.Collections.IDictionary savedState) { ProcessAsUser.Launch(@"C:¥Work¥test.exe"); } 〜

ProcessAsuser.Launchは以降で紹介するユーザ権限で実行する処理メソッドとなります。

ログインユーザで実行する場合は、System側からはわからないので、起動しているプロセスを利用してそのプロセスユーザでデスクトップアプリケーションを起動するようにしていきます。

Reference

この処理ではWin32APIを利用します。


// PROCESS_INFORMATION構造体定義 [StructLayout(LayoutKind.Sequential)] internal struct PROCESS_INFORMATION { public IntPtr hProcess; public IntPtr hThread; public uint dwProcessId; public uint dwThreadId; } // SECURITY_ATTRIBUTES構造体定義 [StructLayout(LayoutKind.Sequential)] internal struct SECURITY_ATTRIBUTES { public uint nLength; public IntPtr lpSecurityDescriptor; public bool bInheritHandle; } // STARTUPINFO構造体定義 [StructLayout(LayoutKind.Sequential)] public struct STARTUPINFO { public uint cb; public string lpReserved; public string lpDesktop; public string 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 short wShowWindow; public short cbReserved2; public IntPtr lpReserved2; public IntPtr hStdInput; public IntPtr hStdOutput; public IntPtr hStdError; } // SECURITY_IMPERSONATION_LEVEL Enumeration internal enum SECURITY_IMPERSONATION_LEVEL { SecurityAnonymous, SecurityIdentification, SecurityImpersonation, SecurityDelegation } // TOKEN_TYPE Enumeration internal enum TOKEN_TYPE { TokenPrimary = 1, TokenImpersonation } // Main Class class ProcessAsUser { // DLL Import [DllImport("advapi32.dll", SetLastError = true)] private static extern bool CreateProcessAsUser( IntPtr hToken, string lpApplicationName, string lpCommandLine, ref SECURITY_ATTRIBUTES lpProcessAttributes, ref SECURITY_ATTRIBUTES lpThreadAttributes, bool bInheritHandles, uint dwCreationFlags, IntPtr lpEnvironment, string lpCurrentDirectory, ref STARTUPINFO lpStartupInfo, out PROCESS_INFORMATION lpProcessInformation); [DllImport("advapi32.dll", EntryPoint = "DuplicateTokenEx", SetLastError = true)] private static extern bool DuplicateTokenEx( IntPtr hExistingToken, uint dwDesiredAccess, ref SECURITY_ATTRIBUTES lpThreadAttributes, Int32 ImpersonationLevel, Int32 dwTokenType, ref IntPtr phNewToken); [DllImport("advapi32.dll", SetLastError = true)] private static extern bool OpenProcessToken( IntPtr ProcessHandle, UInt32 DesiredAccess, ref IntPtr TokenHandle); [DllImport("userenv.dll", SetLastError = true)] private static extern bool CreateEnvironmentBlock( ref IntPtr lpEnvironment, IntPtr hToken, bool bInherit); [DllImport("userenv.dll", SetLastError = true)] private static extern bool DestroyEnvironmentBlock( IntPtr lpEnvironment); [DllImport("kernel32.dll", SetLastError = true)] private static extern bool CloseHandle( IntPtr hObject); // constants private const short SW_SHOW = 5; private const uint TOKEN_QUERY = 0x0008; private const uint TOKEN_DUPLICATE = 0x0002; private const uint TOKEN_ASSIGN_PRIMARY = 0x0001; private const int GENERIC_ALL_ACCESS = 0x10000000; private const int STARTF_USESHOWWINDOW = 0x00000001; private const int STARTF_FORCEONFEEDBACK = 0x00000040; private const uint CREATE_UNICODE_ENVIRONMENT = 0x00000400; public static bool Launch(string appCmdLine /*,int processId*/) { bool ret = false; //Either specify the processID explicitly //Or try to get it from a process owned by the user. //In this case assuming there is only one explorer.exe Process[] ps = Process.GetProcessesByName("explorer"); int processId = -1;//=processId if (ps.Length > 0) { processId = ps[0].Id; } if (processId > 1) { IntPtr token = GetPrimaryToken(processId); if (token != IntPtr.Zero) { IntPtr envBlock = GetEnvironmentBlock(token); ret = LaunchProcessAsUser(appCmdLine, token, envBlock); if (envBlock != IntPtr.Zero) DestroyEnvironmentBlock(envBlock); CloseHandle(token); } } return ret; } private static bool LaunchProcessAsUser(string cmdLine, IntPtr token, IntPtr envBlock) { bool result = false; PROCESS_INFORMATION pi = new PROCESS_INFORMATION(); SECURITY_ATTRIBUTES saProcess = new SECURITY_ATTRIBUTES(); SECURITY_ATTRIBUTES saThread = new SECURITY_ATTRIBUTES(); saProcess.nLength = (uint)Marshal.SizeOf(saProcess); saThread.nLength = (uint)Marshal.SizeOf(saThread); STARTUPINFO si = new STARTUPINFO(); si.cb = (uint)Marshal.SizeOf(si); si.lpDesktop = @"WinSta0\Default"; //Modify as needed si.dwFlags = STARTF_USESHOWWINDOW | STARTF_FORCEONFEEDBACK; si.wShowWindow = SW_SHOW; result = CreateProcessAsUser( token, null, cmdLine, ref saProcess, ref saThread, false, CREATE_UNICODE_ENVIRONMENT, envBlock, null, ref si, out pi); if (result == false) { int error = Marshal.GetLastWin32Error(); string message = String.Format("CreateProcessAsUser Error: {0}", error); Debug.WriteLine(message); } return result; } private static IntPtr GetPrimaryToken(int processId) { IntPtr token = IntPtr.Zero; IntPtr primaryToken = IntPtr.Zero; bool retVal = false; Process p = null; try { p = Process.GetProcessById(processId); } catch (ArgumentException) { string details = String.Format("ProcessID {0} Not Available", processId); Debug.WriteLine(details); throw; } //Gets impersonation token retVal = OpenProcessToken(p.Handle, TOKEN_DUPLICATE, ref token); if (retVal == true) { SECURITY_ATTRIBUTES sa = new SECURITY_ATTRIBUTES(); sa.nLength = (uint)Marshal.SizeOf(sa); //Convert the impersonation token into Primary token retVal = DuplicateTokenEx( token, TOKEN_ASSIGN_PRIMARY | TOKEN_DUPLICATE | TOKEN_QUERY, ref sa, (int)SECURITY_IMPERSONATION_LEVEL.SecurityIdentification, (int)TOKEN_TYPE.TokenPrimary, ref primaryToken); //Close the Token that was previously opened. CloseHandle(token); if (retVal == false) { string message = String.Format("DuplicateTokenEx Error: {0}", Marshal.GetLastWin32Error()); Debug.WriteLine(message); } } else { string message = String.Format("OpenProcessToken Error: {0}", Marshal.GetLastWin32Error()); Debug.WriteLine(message); } //We'll Close this token after it is used. return primaryToken; } private static IntPtr GetEnvironmentBlock(IntPtr token) { IntPtr envBlock = IntPtr.Zero; bool retVal = CreateEnvironmentBlock(ref envBlock, token, false); if (retVal == false) { string message = String.Format("CreateEnvironmentBlock Error: {0}", Marshal.GetLastWin32Error()); Debug.WriteLine(message); } return envBlock; } }

最後に

本記事ではWindowsサービスからログインユーザとして別アプリケーションを起動する方法の備忘録を掲載しました。

Win32APIを利用する方法しかわかりませんが、Windows Server環境でも動作したので、お試しください。