npm token create
をNodeJSのプログラム中から呼びたくて、
execだとプロセスが終わるまで帰ってこないので、対話的なプログラムには向かない
constexec=child.exec("npm token create --json",(err,stdout,stderr)=>{console.info({err,stdout,stderr})})
非同期処理を書きたくないのでspawnSyncを使おうとした。
やりたいことは
- パスワードの入力を求めるメッセージはコンソールに表示したい
- ユーザーが入力するパスワードはコンソールに表示したくない
- 最終結果は(秘密のトークンを含むので)コンソールに表示したくない
である。
とりあえず
constchild=require('child_process')constoptions={encoding:"utf-8",stdio:"inherit"}constresult=child.spawnSync("npm",["token","create","--json"],options);console.info("result",result)
こうすると、1と2はクリアできるけど3がだめ。結果が普通に表示される。(そりゃそうだ)
じゃあこういうことでしょ?
constchild=require('child_process')const{Writable}=require('stream')classHookextendsWritable{_write(chunk,encoding,callback){console.log("chunk",chunk.toString())callback()}}consthook=newHook();hook.fd=1// これがないとspawnに渡せないconstoptions={encoding:"utf-8",stdio:["inherit",hook,"inherit"]}constresult=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
これにならってこうすると、
constchild=require('child_process')constresult={};const{Writable}=require('stream')classHookextendsWritable{_write(chunk,encoding,callback){conststr=chunk.toString()try{result=JSON.parse(str)}catch{console.log(str)}callback()}}consthook=newHook();constoptions={stdio:["inherit","pipe","inherit"]}constprocess=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もクリアできはしたけど、ぜんぜん納得いってない。
constchild=require('child_process');// stdinとstdoutはユーザーコードで処理する。stderrは親プロセスに流すconstoptions={stdio:['pipe','pipe','inherit']}constproc=child.spawn("npm",["token","create","--json"],options);// デフォルトは`Buffer`(バイト列)なのでutf-8を指定proc.stdout.setEncoding('utf-8')// パスワード入力を受け付けるためにprompt-syncを使うconstprompt=require('prompt-sync')({sigint:true})letresult={};proc.stdout.on('data',(data)=>{try{// JSONでパースしてみるresult=JSON.parse(data)// できたら結果が出たということなので子プロセスを終了してよいproc.kill()}// パースできなかったらこちらへcatch(e){// パスワード入力を求められてたらif(data==="npm password: "){// prompt.hideで入力受け付けconstanswer=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されるのではないのか・・・?
ちょっとよくわからないので放置しとく・・・。