Systemセッションから、Userセッションでアプリを起動する②

もくじ

目次(WPF/xaml/C#/C++関連メモ) - tera1707’s blog

やりたいこと

前回の記事で、システムセッションで動いているWindowsサービスから、ユーザーセッションでexeを起動するということをC++でした。

ただ、notepad.exeなら起動してくれるが、自作の、トーストを起動してくれるexeをそこから動かすと、トーストは起動してくれなかった。

pinvokeを使ってC++のコードをC#に直しつつ、その辺をうまく動くように直したい。

C#でやってみる

結論、下記のような流れにすると、うまく動くようになった。

  • ユーザーセッションのトークンを作成し複製する
    • WTSQueryUserToken()
    • DuplicateTokenEx()
  • ログインユーザーのセッションの設定をする
    • SetTokenInformation()
  • 環境変数の設定
    • CreateEnvironmentBlock()
  • 起動するプロセスの情報をセットし、ユーザーセッションでプロセスを起動
    • CreateProcessAsUser()

C++のときとは、最初の手順が違う。  (winlogonのトークンを複製するのではなく、WTSQueryUserToken()で取ってきたトークンを複製)

※下記コードは、NativeMethodsクラスに、pinvokeで呼ぶWin32APIの関数を一式定義して、読んでいる。  その部分のコードは、pinvoke参考サイトと、win32のヘッダファイルからもらってきただけ&量が多いので割愛。

using System.Diagnostics;
using System.Runtime.InteropServices;

namespace MyUtilily
{
    static internal class CreateProcessAsUserOnCSharp
    {
        /// <summary>
        /// ユーザーセッションでコマンドを実行する
        /// </summary>
        /// <param name="commandline">実行したいコマンド</param>
        /// <exception cref="InvalidOperationException"></exception>
        public static void CreateProcessAsUser(string commandline)
        {
            var sessionId = NativeMethods.WTSGetActiveConsoleSessionId();

            IntPtr hPToken = IntPtr.Zero;
            var hUserTokenDup = IntPtr.Zero;

            var ret = NativeMethods.WTSQueryUserToken(sessionId, out hPToken);

            var sa = new NativeMethods.SECURITY_ATTRIBUTES();
            sa.nLength = Marshal.SizeOf(sa);

            if (!NativeMethods.DuplicateTokenEx(hPToken, NativeMethods.TOKEN_ALL_ACCESS, ref sa, NativeMethods.SECURITY_IMPERSONATION_LEVEL.SecurityDelegation, NativeMethods.TOKEN_TYPE.TokenPrimary, out hUserTokenDup))
            {
                NativeMethods.CloseHandle(hPToken);
                throw new InvalidOperationException();
            }

            var si = new NativeMethods.STARTUPINFO()
            {
                cb = Marshal.SizeOf(sa),
                lpDesktop = @"winsta0\default",
                wShowWindow = 0,//SW_HIDE
                dwFlags = NativeMethods.STARTF_USESHOWWINDOW,
            };

            var creationFlags = NativeMethods.CREATE_UNICODE_ENVIRONMENT;
            var env = IntPtr.Zero;

            // アクティブユーザのセッションを設定します
            var ret2 = NativeMethods.SetTokenInformation(hUserTokenDup, NativeMethods.TOKEN_INFORMATION_CLASS.TokenSessionId, ref sessionId, sizeof(NativeMethods.TOKEN_INFORMATION_CLASS));

            // 環境変数を設定
            if (!NativeMethods.CreateEnvironmentBlock(out env, hUserTokenDup, true))
            {
                env = IntPtr.Zero;
            }

            NativeMethods.PROCESS_INFORMATION pi = new NativeMethods.PROCESS_INFORMATION();

            NativeMethods.CreateProcessAsUser(hUserTokenDup, IntPtr.Zero, commandline, IntPtr.Zero, IntPtr.Zero, false, creationFlags, env, IntPtr.Zero, ref si, out pi);

            NativeMethods.DestroyEnvironmentBlock(env);
        }
    }
}

コード

github.com

結果

Windowsサービスのコードの方から、CreateProcessAsUserOnCSharpクラスのCreateProcessAsUser()メソッドの引数に、呼び出したい「トーストを出してくれるexe」のパスを入れてやれば、トーストを表示させることができた。

備考

はっきり言って、コードの意味をあまり理解できてない。 C++からC#に直してうまく動くようになったのも、適当にいろいろ試していたらたまたま動いた、レベル。

今は、C++で試した方の記事に書いたように「システムセッションで動くサービスから、ユーザーセッションで動かさないといけないexeを起動するのは不自然なのかも」「であれば最初からユーザー権限で動くバックグラウンドアプリから起動したほうがよいのかも」と考え、そちらを採用しようとしているので、いったんこっちを深く調べるのはおいておこうと思う。 (ほんとに必要になったときに、しっかり調べる。)

参考

前回のC++で試したときの記事 tera1707.com

pinvokeの参考 www.pinvoke.net