npm token create
をNodeJSのプログラム中から呼びたくて、
execだとプロセスが終わるまで帰ってこないので、対話的なプログラムには向かない
const exec = child.exec("npm token create --json", (err, stdout, stderr) => {
console.info({ err, stdout, stderr })
})
非同期処理を書きたくないのでspawnSyncを使おうとした。
やりたいことは
- パスワードの入力を求めるメッセージはコンソールに表示したい
- ユーザーが入力するパスワードはコンソールに表示したくない
- 最終結果は(秘密のトークンを含むので)コンソールに表示したくない
である。
とりあえず
const child = require('child_process')
const options = {
encoding: "utf-8",
stdio: "inherit"
}
const result = child.spawnSync("npm", ["token", "create", "--json"], options);
console.info("result", result)
こうすると、1と2はクリアできるけど3がだめ。結果が普通に表示される。(そりゃそうだ)
じゃあこういうことでしょ?
const child = require('child_process')
const { Writable } = require('stream')
class Hook extends Writable {
_write(chunk, encoding, callback) {
console.log("chunk", chunk.toString())
callback()
}
}
const hook = new Hook();
hook.fd = 1 // これがないとspawnに渡せない
const options = {
encoding: "utf-8",
stdio: ["inherit", hook, "inherit"]
}
const result = child.spawnSync("npm", ["token", "create", "--json"], options)
console.info("result", result)
と思ってやってみたが、挙動は変わらない。hook._write
が呼ばれていない。
ちなみにhook.fd
の値をいくつか変えて試してみたが、コンソールに表示されなくなったりnpm ERR! code EPIPE
が出たりする。
調べていると、spawnはカスタムストリームを受け取れない(バグ)という情報があった。
https://stackoverflow.com/questions/34967278/nodejs-child-process-spawn-custom-stdio
https://github.com/nodejs/node-v0.x-archive/issues/4030
これにならってこうすると、
const child = require('child_process')
const result = {};
const { Writable } = require('stream')
class Hook extends Writable {
_write(chunk, encoding, callback) {
const str = chunk.toString()
try {
result = JSON.parse(str)
}
catch {
console.log(str)
}
callback()
}
}
const hook = new Hook();
const options = {
stdio: ["inherit", "pipe", "inherit"]
}
const process = child.spawn("npm", ["token", "create", "--json"], options)
process.stdout.pipe(hook)
process.on("close", () => {
console.info("result", result)
})
1と3はクリアできたが2がダメ。
1と3についても、渡ってきたデータがJSON.parse
を通るかどうかで分岐させているのでどうもすっきりしない。
ここにあるような、process.stdout.write
を一時的に上書きするアプローチもだめだった。
上書きしたwriteが呼ばれてなさそうな挙動。
https://stackoverflow.com/questions/26675055/nodejs-parse-process-stdout-to-a-variable
https://gist.github.com/pguillory/729616
結局、stdinもstdoutもpipeしてしまって、状況をひとつひとつハンドリングするこのようなコードになってしまった。
1も2も3もクリアできはしたけど、ぜんぜん納得いってない。
const child = require('child_process');
// stdinとstdoutはユーザーコードで処理する。stderrは親プロセスに流す
const options = {
stdio: ['pipe', 'pipe', 'inherit']
}
const proc = child.spawn("npm", ["token", "create", "--json"], options);
// デフォルトは`Buffer`(バイト列)なのでutf-8を指定
proc.stdout.setEncoding('utf-8')
// パスワード入力を受け付けるためにprompt-syncを使う
const prompt = require('prompt-sync')({ sigint: true })
let result = {};
proc.stdout.on('data', (data) => {
try {
// JSONでパースしてみる
result = JSON.parse(data)
// できたら結果が出たということなので子プロセスを終了してよい
proc.kill()
}
// パースできなかったらこちらへ
catch (e) {
// パスワード入力を求められてたら
if (data === "npm password: ") {
// prompt.hideで入力受け付け
const answer = prompt.hide(data)
// 入力された文字列をproc.stdinに渡す
// 改行コードを送らないと向こうで処理を始めてくれない
proc.stdin.write(answer + "\n")
}
}
});
proc.stdout.on('end', () => {
console.info("result", result)
});
node main.js
npm password:
result {
token: 'xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx',
cidr_whitelist: [],
readonly: false,
created: '2020-08-14T06:03:21.551Z'
}
そういえば、stdoutをpipeしているのに、npm password:
がコンソールに表示されるのはなんでだ・・・?
https://github.com/npm/cli/blob/latest/lib/token.js#L210
https://github.com/npm/cli/blob/latest/lib/utils/read-user-info.js#L41
https://github.com/npm/read/blob/master/lib/read.js#L19
こう読み進めていくと、結局process.stdout
を使っていそうなのだが、child_process
内でのprocess.stdout
への出力がpipeされるのではないのか・・・?
ちょっとよくわからないので放置しとく・・・。