0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

schtasks /query /XML は encoding="UTF-16" と宣言したまま、コンソールのコードページの符号で出力した(日本語 Windows 11 1台での実測)

0
Posted at

Windows のタスクスケジューラの設定を、Node のスクリプトから schtasks /query /TN <タスク名> /XML で読んでいます。
取り出した XML から <Arguments> の中のパスを拾い、そのファイルが在るかを fs.existsSync で確かめる、という使い方です。

日本語を含むパス(ユーザー名が日本語)の機械で、このパスが existsSync で false になりました。
原因は、XML の宣言と実際のバイト列の文字コードが一致していないことでした。

実測: 同じタスクを3通りのコードページで取った

Windows 11 10.0.26200(日本語)/ Node v24.18.1 / 1台だけの実測です(2026-09-19)。
Node の execSync で shell: "cmd.exe" を指定して 呼び、返ってきたバイト列をそのまま調べました。
測ったのはこの呼び方だけです。

コードページ バイト数 先頭4バイト 宣言 UTF-8 として読んだときの置換文字(U+FFFD) 日本語1語が読めた符号 <Arguments> のパスで existsSync(UTF-8 で読む / Shift_JIS で読む)
指定なし(継いだ値 932) 1,864 3c 3f 78 6d encoding="UTF-16" 52個 Shift_JIS false / true
chcp 932 1,864 3c 3f 78 6d encoding="UTF-16" 52個 Shift_JIS false / true
chcp 65001 1,909 3c 3f 78 6d encoding="UTF-16" 0個 UTF-8 true / false

読み取れること(この1台・この3条件について):

  • **先頭4バイトは 3c 3f 78 6d(<?xm)で、BOM がありません。**UTF-16 なら1文字2バイトになるので、この並びにはなりません
  • **宣言は3回とも encoding="UTF-16" で、実体は3回とも UTF-16 ではありません。**UTF-16LE として読んでも日本語1語は見つかりませんでした
  • **測った2つのコードページでは、実体はそのときのコードページの符号でした。**932 なら Shift_JIS、65001 なら UTF-8 です(バイト数も変わります)。他のコードページは測っていません
  • cmd.exe 経由でコードページを指定せずに呼ぶと、継いだ値(この機械では 932)の符号になりました

つまり、測った範囲では次の3つの読み方がどれも、ある条件で壊れました。

読み方 壊れた条件(測った3条件のうち)
XML の宣言に従う(UTF-16) 3条件すべて(実体が UTF-16 ではない)
UTF-8 と決め打ちする コードページが 932 のとき(この機械の既定)
Shift_JIS と決め打ちする コードページが 65001 のとき

何が起きたか

Node の Buffer#toString() の既定は UTF-8 です。
コードページを指定せずに schtasks /query /XML を呼び、その出力を既定のまま文字列にすると、
**日本語の部分が置換文字(U+FFFD)に化けます。**ASCII の部分は壊れないので、XML としてはそれらしく読めてしまいます。

その文字列から <Arguments> の中の引用符つきのパスを取り出して fs.existsSync にかけると false でした。
同じバイト列を Shift_JIS として読めば true です(上の表の1行目)。
例外は出ません。「ファイルが無い」と区別がつかない形で false が返ります。

ASCII だけのパスなら壊れないので、英語のユーザー名の機械では再現しないはずです(確かめてはいません)。

注意: chcp は、呼んだ側のコンソールのコードページも書き換える

execSync("chcp 65001 >nul & ...", { shell: "cmd.exe" }) の chcp は子の cmd.exe の中で実行されますが、
**終わったあとも、親のコンソールのコードページは 65001 のまま残りました。**子が親と同じコンソールを共有しているためだと考えていますが、仕組みはドキュメントで確かめていません。

実行したもの 実行前のコンソール 実行後のコンソール
最後に戻さない版の測定スクリプト 932 65001
最後に起動時の値へ戻す版(下に全文) 932 932

(PowerShell のコンソールで chcp 932 にしてから node で走らせ、終わったあと同じコンソールで chcp を読みました。)

上の表の「Shift_JIS と決め打ちする」が壊れる条件は、この副作用で作られます。
コードページを変えて呼ぶなら、起動時の値を読んでおき、終わったら戻してください。

確かめ方

下のスクリプトは依存パッケージなしで、ファイルへの書き込みも通信もしません。ただしコンソールのコードページを一時的に変え、最後に起動時の値へ戻します(戻ったかどうかも codePage.before / codePage.after として出します)。
パスやユーザー名は出力しません(バイト数・先頭4バイト・真偽だけを出します)。

2つめの引数には、そのタスクの XML に含まれているはずの日本語を1語渡します(タスク名に日本語が入っていれば、それで足ります)。

node repro-schtasks-xml-encoding.mjs "<タスク名>" "<XML に含まれる日本語1語>"

argPathFound が false のときは、<Arguments> の中に引用符つきの文字列が1つも無かったという意味です(<Command> にだけパスを書いたタスク、引用符が &quot; と書かれている XML など)。そのときパスの2列は null になり、existsSync にはかけていません。拾うのは最初の引用符つきの文字列で、それがパスとは限りません("--flag" のような引数なら、existsSync は当然 false です)。

codePage.before が null のときは、起動時のコードページを読めなかったので**戻していません。**その場合は自分で chcp を確かめてください。

// schtasks /query /XML が返すバイト列の文字コードを測る(依存0・ファイルへの書き込みなし)。
// コードページ(chcp)を「指定なし / 932 / 65001」の3通りにして、cmd.exe 経由で同じタスクの XML を取る。
// ★副作用: chcp は子の cmd.exe で実行しても、同じコンソールを使う親のコードページまで書き換える。
//   そのため最後に、起動時のコードページへ戻し、戻ったかどうかも出力する。
// 本文(パスやユーザー名)は1文字も出さない。出すのはバイト数・先頭4バイト・判定の真偽だけ。
// 使い方: node repro-schtasks-xml-encoding.mjs <タスク名> <XML に含まれるはずの日本語1語>
import { execSync } from "node:child_process";
import fs from "node:fs";

const [task, probe] = process.argv.slice(2);
if (!task || !probe) {
  console.error("使い方: node repro-schtasks-xml-encoding.mjs <タスク名> <XML に含まれるはずの日本語1語>");
  process.exit(1);
}
const run = (cmd) => execSync(cmd, { shell: "cmd.exe", env: { ...process.env, REPRO_TASK: task } });
const currentCp = () => (run("chcp").toString().match(/\d+/) || [null])[0];
const inheritedCp = currentCp();
// <Arguments> の中の、引用符で囲まれた最初のパス。見つからなければ null(existsSync にはかけない)
const argPath = (text) => (text.match(/<Arguments>[^<]*?"([^"]+)"/) || [null, null])[1];
const exists = (p) => (p === null ? null : fs.existsSync(p));
const results = [];
try {
  for (const cp of [null, "932", "65001"]) {
    // タスク名は JS 側でコマンド文字列に連結せず、環境変数で渡す(展開するのは cmd)
    const query = 'schtasks /query /TN "%REPRO_TASK%" /XML';
    const buf = run(cp ? `chcp ${cp} >nul & ${query}` : query);
    const asUtf8 = buf.toString("utf8");
    const asSjis = new TextDecoder("shift_jis").decode(buf);
    results.push({
      chcp: cp ?? `指定なし(継いだ値 ${inheritedCp})`,
      bytes: buf.length,
      head: [...buf.subarray(0, 4)].map((b) => b.toString(16).padStart(2, "0")).join(" "),
      declaration: (asUtf8.match(/<\?xml[^>]*\?>/) || [null])[0],
      utf8ReplacementChars: (asUtf8.match(/\uFFFD/g) || []).length,
      probeReadableAsUtf8: asUtf8.includes(probe),
      probeReadableAsShiftJIS: asSjis.includes(probe),
      probeReadableAsUtf16le: new TextDecoder("utf-16le").decode(buf).includes(probe),
      // 引用符つきのパスが <Arguments> に在ったか(false なら下の2つは null)
      argPathFound: argPath(asUtf8) !== null,
      // そのパスを2通りに読んで fs.existsSync にかけた結果(パスそのものは出さない)
      argPathExistsAsUtf8: exists(argPath(asUtf8)),
      argPathExistsAsShiftJIS: exists(argPath(asSjis)),
    });
  }
} finally {
  if (inheritedCp) run(`chcp ${inheritedCp} >nul`);
}
console.log(JSON.stringify({
  measuredAt: new Date().toISOString(), node: process.version, probe, results,
  codePage: { before: inheritedCp, after: currentCp() },
}, null, 1));

直し方の候補(測ったのは1つ目だけ)

  1. コードページを自分で決めてから呼び、終わったら戻す。chcp 65001 >nul & schtasks ... /XML にして UTF-8 として読むと、置換文字は0個・existsSync も true でした(表の3行目)。**戻す処理は上のスクリプトの finally の形で、正常に終わった場合に戻ることを確かめました。**途中で失敗した場合は測っていません。Ctrl+C で止めた場合は finally が走らず、65001 のまま残ることがあります(これも測っていません)
  2. 読んだ文字列に U+FFFD が含まれていたら、別の方法で取り直す(PowerShell の Export-ScheduledTask など)。Export-ScheduledTask 側の文字コードは今回測っていません

どちらにしても、この実測の範囲では、XML の宣言に書かれた encoding は実体と一致しませんでした。

測っていないこと

  • 測ったのはこの PC 1台・日本語の Windows 11 だけです。他の言語の Windows、他の版の Windows では確かめていません
  • 測ったコードページは 932 と 65001 の2つだけです
  • cmd.exe 経由の execSync 以外(execFile で直接起動、PowerShell から呼ぶ、ファイルへリダイレクトする)は測っていません
  • なぜ宣言が UTF-16 のままなのかは分かりません(ドキュメントでは確かめていません)

タスクスケジューラで定期実行している構成を、読み取りだけで点検する道具を置いています: https://h26005.tailf680df.ts.net:10000/free.html

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?