agent 向けスクリプトを書く
agent が実行する PowerShell スクリプトは ほぼ 通常の .ps1 ファイルです。このページは、スクリプトのソースを見るだけでは分からない罠をまとめます。
agent はスクリプトをディスクに staging してから -File で実行する
PR #230 (agent version 0.42.0+) 以降、agent は次の動作をします:
- スクリプト本体を temp の
.ps1に書き出します。Windows なら%ProgramData%\Kanade\agent-scripts\<UUID>\kanade-<UUID>.ps1、それ以外 (dev のみ) なら$TMPDIR/kanade-agent-<UUID>/kanade-<UUID>.ps1。 - Writes a launcher
.ps1next to it that sets UTF-8 console encoding then& '<your-script>' @args, and re-raises your script'sexit N/ terminating error as the job's exit code (if (-not $?) { exit $LASTEXITCODE }). powershell -NoProfile -NonInteractive -ExecutionPolicy Bypass -File <launcher>を spawn します。
結果として、あなたのスクリプトでは:
- 先頭に
[CmdletBinding()]やparam(...)を 書けます。launcher の call-operator 境界が独自スコープを生み、その中ではヘッダーが有効です。 $PSCommandPathが operator のソースパスと一致することを 期待しないでください — staged 後の temp ファイルパスになります。$PSScriptRootに 書き込まないでください (次節参照)。
0.42.0 より前のエージェントは powershell -Command "<body>" を使っており、これは本文をコマンドライン式として解析するため [CmdletBinding()] を構文エラーとして弾きます。stderr に「Unexpected token '[CmdletBinding()]'」が出ていたら、その端末のエージェントが古すぎます。更新してください (エージェントの自己更新 を参照)。
run_as: user のとき $PSScriptRoot は読み取り専用
run_as: user (または system_gui) のとき、子プロセスはログオン中のユーザーとして動きます — staged ファイルを書いた LocalSystem の agent ではありません。staging ディレクトリは %ProgramData% から ACL を継承していて、Users には Read & Execute は許可されるが Modify は許可されません。
つまり:
# OK from any run_as
Get-ChildItem $PSScriptRoot # list contents
Get-Content $PSScriptRoot\anything # read
# NG from run_as: user (access denied)
New-Item -Path $PSScriptRoot\out.txt
Set-Content -Path $PSScriptRoot\log.log
代わりに $env:TEMP、$env:LOCALAPPDATA、ユーザープロファイル配下の絶対パスのいずれかに書いてください。run_as: system (SYSTEM が自分の staged dir に書ける) であっても、スクリプト終了時にディレクトリは掃除されるので、隣接ファイル書き込みはいずれにせよ壊れやすいです。
実行 identity 一覧
run_as: (マニフェスト) | 子プロセスの identity | $PSScriptRoot を読める | $PSScriptRoot に書ける | admin 権限あり |
|---|---|---|---|---|
system (デフォルト) | LocalSystem | ✓ | ✓ (ただし無意味、GC される) | はい |
user | ログオン中のユーザー | ✓ | ✗ access denied | いいえ |
system_gui | LocalSystem (ユーザーセッション内) | ✓ | ✓ (ただし無意味、GC される) | はい |
system_gui は「PsExec の -i -s」パターンです。管理者権限を持ちつつ、ユーザーのデスクトップセッションから見える形で動きます (昇格と対話ウィンドウの両方を必要とする GUI ツールに有用です)。
stdout vs Write-Host
The backend's result projector reads stdout as the script's output. For jobs with hint blocks (inventory:, check:, collect:, etc.), the projector extracts JSON payloads delimited by #KANADE-<KIND>-BEGIN and #KANADE-<KIND>-END fences (e.g. #KANADE-INVENTORY-BEGIN / #KANADE-INVENTORY-END).
For single-hint jobs without fences, backward compatibility allows parsing the entire stdout as a single JSON object. However, for multi-hint jobs (e.g. combining inventory: and check:), fenced blocks are required so each hint handler can extract its respective payload.
Do NOT use Write-Host for progress chatter. Write-Host output bleeds INTO the captured stdout stream, which can mess up unfenced JSON parsing or clutter log output.
Send progress chatter to stderr via [Console]::Error.WriteLine(...). stderr is captured into the result's separate stderr field, which the projector ignores.
[Console]::Error.WriteLine("Downloading...") # → stderr (logged, ignored by projector)
Write-Output "#KANADE-INVENTORY-BEGIN"
Write-Output ($obj | ConvertTo-Json -Compress)
Write-Output "#KANADE-INVENTORY-END"
Fencing stdout payloads ensures clean extraction even if other output is present, and allows a single job script to provide inventory facts, health check state, and file collection metadata in a single run (#821). See configs/jobs/installers/scripts/install-kanade-client.ps1 for a worked example.
デフォルトで UTF-8
launcher はスクリプト起動前に [Console]::OutputEncoding = UTF-8 と $OutputEncoding = UTF-8 を設定するので、ホストのシステムコードページに関係なくあなたの stdout / stderr は UTF-8 になります。日本語 / DE / KR / CN を含む operator スクリプトも、ホスト個別の workaround なしに SPA Activity ビューで正しく表示されます。
明示的に OEM / CP932 / Shift-JIS 出力が必要なら ($OutputEncoding を無視する legacy CLI を呼ぶケースなど)、launcher の prelude が走ったあとにスクリプト内で自分で設定してください — あなたの代入が優先されます。
ネイティブコマンドの exit code
スクリプトが成功した native コマンドで終わると全体の exit は 0 — これは PowerShell のデフォルトです。native コマンドが失敗 ($LASTEXITCODE -ne 0) してハンドリングしないと、PowerShell は依然として 0 で終了します — $ErrorActionPreference = 'Stop' は 救ってくれません。
Windows PowerShell 5.1 (Windows エンドポイントのデフォルト、agent の
powershell.exeが解決する先) は$ErrorActionPreferenceに関わらず native コマンドの非ゼロ exit を non-terminating として扱います。PowerShell 7.3+ で$PSNativeCommandUseErrorActionPreference = $trueが追加されてこれを terminating にできますが、デプロイターゲットでは使えません。必ず$LASTEXITCODEを明示的にチェックしてください。
The agent does NOT auto-propagate a leftover $LASTEXITCODE — that would exit nonzero even when your script handled the native error gracefully. The launcher only re-raises an explicit exit N or a terminating error, so the job's exit code matches running your script with powershell -File. If you want the script's exit code to reflect a specific native call, propagate it yourself:
& git pull
if ($LASTEXITCODE -ne 0) { throw "git pull failed with exit code $LASTEXITCODE" }
# ネイティブの終了コードをそのまま伝播させたい場合は:
if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE }
通常は throw のほうが望ましい — クリーンな PowerShell エラーレコードを生成し (trap { … break } クリーンアップパターンが捕まえられる) かつ非ゼロで終わるからです。exit $LASTEXITCODE は呼び出し側が正確な exit code を気にする場合に向きます。
タイムアウト
マニフェストの timeout: は agent によって適用されます。発火すると agent は PowerShell プロセスに child.kill() を呼びます — graceful な shutdown も trap も finally もありません。それを前提に設計してください:
- スクリプトが
timeout * 0.6で終わるように実行時間を見積もり、余裕を残す。 - 明示的解放が要るリソース (staging dir、lock ファイル) のクリーンアップには
trap { ... ; break }を使う —trapは terminating エラーで発火し、agent の kill では発火しません。timeout ケースには頼らないこと。 - 協調的なキャンセルが必要なら、番兵ファイルやレジストリ値をポーリングして早めに抜けてください。エージェントからスクリプトへ、穏当に「切り上げろ」と伝える手段はありません。
実行中ジョブの kill
kanade kill <exec_id> は agent が購読する kill メッセージを publish します。受信すると agent は child.kill() を呼びます — timeout 経路と同じ hard-kill。operator にはただちに Killed マークの result row が届き、終了前に agent がキャプチャできた stdout / stderr が付きます。