DataContractJsonSerializerクラスで使うjson用クラスをrecordでつくる

もくじ qiita.com

やりたいこと

jsonファイルを読み込むときに、以前調べたDataContractJsonSerializerクラスをよく使うがjsonを読み込んだときにデータを保存するためのクラスをいちいち書くのがめんどくさい。あと長い。なにか短くかける方法がないか調べたい。

やったこと

C #9.0で追加されたrecord型を使う。

前提

下記で実験。

VisualStudio2022/.NET6/C#10

コード

classでつくる

以前DataContractJsonSerializerの実験で作ったjsonデータを格納するためのクラスは、下記の通り。

public class Rootobject
{
    public int data1 { get; set; }
    public string data2 { get; set; }
    public bool data3 { get; set; }
    public Arraydata[] arraydata { get; set; }
}

public class Arraydata
{
    public int data11 { get; set; }
    public float[] small { get; set; }
    public float[] large { get; set; }
}

recordでつくる

これと同じものをrecordで作ると下記のようになる。

こうすれば、前回作ったジェネリクスでjsonを読み込むコードも、そのまま使える。

[DataContract]
record Rootobject(
    [property: DataMember] int data1,
    [property: DataMember] string data2,
    [property: DataMember] bool data3,
    [property: DataMember] Arraydata[] arraydata
);

[DataContract]
record Arraydata(
    [property: DataMember] int data11,
    [property: DataMember] float[] small,
    [property: DataMember] float[] large
);

注意

試した限り、classだと、[DataContract]のアトリビュートをつけなくてもうまく動いたが、recordだとそれらを付けてやらないとdata = (T?)serializer.ReadObject(stream);でデシリアライズするときに例外が起きた。([property: DataMember]は、つけなくても動くのは動いた。) f:id:tera1707:20220117231200p:plain

注意点

但し、このままだと、例えば

  • jsonファイルを読み込んで、
  • data1の値だけ書き換えて
  • それを別のjsonファイルに保存しなおす

ということができない。

これは、上のようにrecordでRootobjectを作成すると、data1について、コンパイラが裏で

    [property: DataMember]
    public int data1 { get; init; }

のようにしてくれていて、data1の値をコンストラクタでしか入れることができないようになっているため。 これを、

[DataContract]
record Rootobject( [property: DataMember] string data2, [property: DataMember] bool data3, [property: DataMember] Arraydata[] arraydata)
{
    [property: DataMember]
    public int data1 { get; set; }
}

と書いてあげると、data1だけ編集することができるようにできるが、 そういうややこしいことをするなら、全部classで作った方がよいような気がする。

考察

「短くかけたかどうか?」という視点だけだと「{get;set}がなくなったくらい?」と思ってしまうが、 こちらのページにあるように、「jsonのデータ」を扱うためのものとしてこのrecordを見ると、比較したりするうえではrecordの方が、だいぶ扱いやすくなっていると思った。

参考

record型の詳しい説明。 ここを見ればすべてわかる。

ufcpp.net

レコードのプロパティへのアトリビュートの設定の仕方

docs.microsoft.com

キーボードのキーを押したことにする

もくじ

qiita.com

やりたいこと

Windowsアプリの中から、キーボードのキーを押したことにしたい。

やり方

keybd_event()関数を使う。

下記のコードでは、こんな感じの処理をしている。

#include <windows.h>
#include <winuser.h>
#include <iostream>

#define KEY_M (0x4E)

int main()
{
    std::wcout << L"Win+D press..." << std::endl;
    keybd_event(VK_LWIN, 0, 0, NULL);
    keybd_event(KEY_M, 0, 0, NULL);
    Sleep(500);

    std::wcout << L"Win+D Key Release..." << std::endl;
    keybd_event(KEY_M, 0, 2, NULL);
    keybd_event(VK_LWIN, 0, 2, NULL);
}

参考

使うAPI

docs.microsoft.com

キーコード

docs.microsoft.com

docs.microsoft.com

元にした記事

detail.chiebukuro.yahoo.co.jp

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

もくじ qiita.com

やりたいこと

前回の記事で、システムセッションで動いている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

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

もくじ qiita.com

やりたいこと

サービスから、なにかのアプリを起動しようと、サービス(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

リモートPCのProgramFilesの中にある共有フォルダにファイルをコピーしたい

qiita.com

やりたいこと

下記のように、VisualStudioで開発を行っている開発PCから、作ったアプリを動かしたいリモートPCをLANケーブルで直接つないでデバッグする。 f:id:tera1707:20220113215121p:plain

そのとき、以前のリモートデバッグの記事でやったやり方で、 デスクトップ等の、ファイル保存に管理者権限がいらないところにビルド成果物を出力してリモートデバッグすることもできるが、Program Filesなど、アプリを本当にインストールしたときに配置されるであろう場所に、デバッグするときにも配置してデバッグしたい。

しかし、単に「出力パス」にリモートPCのProgram Filesの下にあるフォルダをこういう感じで指定しても、

f:id:tera1707:20220113215804p:plain

「アクセス拒否されました」というエラーでファイルを保存できない。

1>C:\Program Files\Microsoft Visual Studio\2022\Community\MSBuild\Current\Bin\amd64\Microsoft.Common.CurrentVersion.targets(1166,5): warning MSB3191: ディレクトリ "\\192.168.31.21\InProgramFiles\Debug\net6.0-windows10.0.17763.0\" を作成できません。パス '\\192.168.31.21\InProgramFiles\Debug\net6.0-windows10.0.17763.0\' へのアクセスが拒否されました。

なんとかうまいことやって、リモートPCの管理者権限のいるフォルダに、開発PCから直接ビルド成果物を保存したい。

やり方

リモートPCで共有した、管理者権限の要るフォルダで、Usersにフルコントロールの権限を与えてやる。

手順

対象のフォルダのプロパティを開き、「セキュリティ」タブを開く。 その中の「アクセス許可を変更するには[編集]をクリックします。」の横にある「編集」ボタンを押す。

f:id:tera1707:20220113220741p:plain

ユーザー一覧で「Users」を選択して、「フルコントロール」にチェックを入れる。

f:id:tera1707:20220113221042p:plain

そうすると、直接、そのフォルダにファイルを出力できるようになる。