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

もくじ

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

やりたいこと

サービスから、なにかのアプリを起動しようと、サービス(C#/.NET6)のコードの中に下記のようなコードを書いたが、なぜか起動してくれなかった。

Process.Start("C:\Windows\System32\notepad.exe");

なんでなのか?を調べつつ、どうにかしてサービスからアプリを起動させたい。

なぜ起動しないのか?

普通にあるユーザーでログインして、なにかのアプリを起動したときは「ユーザーセッション」というものでログインしているらしい。 →以前調べた、ログインしたときのセッション番号(>=1)がそれにあたるっぽい。

それとは違い、サービスは特別なセッション(セッション番号=0)で動いており、通常のアプリを起動したりはできないらしい。(ユーザーやデスクトップとのインタラクションは禁止されており、UIのあるようなアプリは起動できない)

なので、アプリの「自然」な起動をしようと思うと、ユーザーセッションで動いてほしいアプリを、サービスから起動するようなことはせず、ユーザーセッションで動くバックグラウンドアプリから起動してあげた方がよいのかもしれない。

→バックグラウンドアプリの作り方

ただ今回は、それでもサービスからアプリを起動したいので、そういうことができるやり方を調べる。

起動のしかた

いろいろ調べた結果、C++でWin32APIを使ったやり方はある様子。

まずC++&Win32APIでやってみてから、 そのコードをp/invokeC#に直す方向でやってみる。

C++でやってみる

流れとしては、

  • winlogon.exeが動いているセッションIDが、そのときログインしているユーザーのセッションIDだから、それを取得
    • OpenProcess()
  • そのIDから、Winlogonのプロセストークンをコピー
    • OpenProcessToken()
    • DuplicateTokenEx()
  • ログインユーザーのセッションの設定をする
    • SetTokenInformation()
  • 環境変数の設定
    • CreateEnvironmentBlock()
  • 起動するプロセスの情報をセットし、ユーザーセッションでプロセスを起動
    • CreateProcessAsUser()

という流れになる。コードとしては下記。

BOOL createProcessAsUser(const std::wstring& app, const std::wstring& param, HANDLE token, DWORD creationFlags, LPVOID env)
{
    wchar_t arg[MAX_PATH] = L"";

    wcscpy_s(arg, (param.empty() ? app.c_str() : (app + L" " + param).c_str()));

    WCHAR tmp[256] = L"winsta0\\default";
    STARTUPINFO         si = { sizeof(STARTUPINFO), nullptr, tmp };
    PROCESS_INFORMATION pi = {};
    const BOOL          retval = CreateProcessAsUser(token, nullptr, arg, nullptr, nullptr, FALSE, creationFlags, env, nullptr, &si, &pi);

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

    return retval;
}

void MyCreateProcessAsUser(std::wstring appFullPath)
{
    auto dwSesId = ::WTSGetActiveConsoleSessionId();
    auto winlogonPid = GetProcessIdByName(L"winlogon.exe");

    auto hProcess = OpenProcess(MAXIMUM_ALLOWED, false, winlogonPid);

    HANDLE hPToken = NULL;

    if (!OpenProcessToken(hProcess, TOKEN_DUPLICATE, &hPToken))
    {
        auto err = GetLastError();
        CloseHandle(hProcess);
        return;
    }

    SECURITY_ATTRIBUTES sa = {0};
    sa.nLength = sizeof(sa);

    HANDLE hUserTokenDup = NULL;

    if (!DuplicateTokenEx(hPToken, MAXIMUM_ALLOWED, &sa, SecurityIdentification, TokenPrimary, &hUserTokenDup))
    {
        CloseHandle(hProcess);
        CloseHandle(hPToken);
        return;
    }

    //-------------

    STARTUPINFO si = {0};
    si.cb = sizeof(si);

    DWORD  creationFlags = CREATE_NEW_CONSOLE | NORMAL_PRIORITY_CLASS;
    LPVOID env = nullptr;

    // アクティブユーザのセッションを設定します
    auto ret = SetTokenInformation(hUserTokenDup, TokenSessionId, &dwSesId, sizeof(DWORD));

    // 環境変数を設定します
    if (CreateEnvironmentBlock(&env, hUserTokenDup, TRUE)) {
        creationFlags |= CREATE_UNICODE_ENVIRONMENT;
    }
    else {
        env = nullptr;
    }

    auto retval = createProcessAsUser(appFullPath, L"", hUserTokenDup, creationFlags, env);

    DestroyEnvironmentBlock(env);
}

結果

上のコードのMyCreateProcessAsUser()に、起動したいアプリのexeのフルパスを入れると、そのアプリを起動することができた。(今回は、C:\\Windows\\System32\\notepad.exeで実験した。)

ただ、今回の狙いは、以前の記事でやったような、トーストを起動してくれるexeをサービスから叩いて、トーストを表示するということだったのだが、なぜかトーストは出てくれなかった。(トーストのためのexeは起動しているのだが、実際にトーストが表示はされない。)

一旦C++のコードはここまでにして、次はC#で同じことをやりつつ、トーストがでるように改良する。

参考

コードの参考① www.webdevqa.jp.net

コードの参考② yano.hatenadiary.jp

その他もろもろ

social.msdn.microsoft.com

以前のトーストの記事

qiita.com