イネマルのプログラミング備忘録

趣味プログラマのメモ

WSH JScript Chakra エンジンの共存による機能制限の回避(WScript Quit 代替)

はじめに

WSH JScriptChakra を指定すると WScript Object の機能が制限される為、
WScript.Quit 関数が利用できず、戻り値を返す術が失われます。
今回は、この問題の対処例を挙げます。

前提

Windows 環境には、Windows Script Host(WSH)と呼ばれる
Node.js の様な JavaScript が動作するスクリプトの実行環境が標準搭載されています。

技術としては古いものなので、デフォルトでは ES3 程度のサポートですが、
スクリプトエンジンを Chakra に変更することでES6の機能が利用可能です。
WSH JScript Chakra を使用した ES2015(ES6) 対応 ( スクリプトエンジン まとめ )

JScriptChakra を共存させる

既定のJScript で、Chakra 用の処理をラップする形の対処法です。
WScript.Quit は、CScript でないと戻り値が機能しないので、バッチに埋め込みます。

この方法は、JScriptChakra で各エンジンが共存するので、
機能制限の掛かる箇所は、JScript側で関数化する等の対処で回避可能になります。
※ 例えば、GetObject等のChakraでは未定義となる関数は、JScript 経由で利用できます。

0</* ::
@cscript /nologo /E:JScript "%~f0" %*

@rem 戻り値の確認用
@echo.ExitCode:%errorlevel%&pause

@exit /b %errorlevel%&*/0;//@cc_on @if(0)
//
// Chakra
//

// エントリーポイント用の関数(プロセスの戻り値を返す)
function main()
{
    // コンソールに表示
    const msg = `${WSH} for Chakra`;
    WSH.Echo(msg);
    
    // JScript側の関数を呼ぶ
    const r = JS.hoge();
    
    // 強制終了するなら、例外を投げる
    //throw "quit";
    // もしくは、早期リターン
    //return;
    
    // ポップアップを表示
    // ActiveXObject は未定義なので、CreateObject を使う
    const shell = WSH.CreateObject("WScript.Shell");
    shell.popup(msg);
    
    // 戻り値
    return r;
}

function fuga()
{
    WSH.Echo("fuga");
}

var quit=!1;let item,ie,w=WSH.CreateObject("Shell.Application").Windows(),i=0;
for(;i<w.Count;i++)if((item=w(i))&&item.hWnd==WSH.Arguments(0)){ie=item;break}
for(ie.PutProperty("@",this);!quit;)WSH.Sleep(1e3);/*@end @cc_on
for(var ckr,ie=WSH.CreateObject("InternetExplorer.Application"),sh=WSH.CreateObject("WScript.Shell"),
p=sh.Exec('cscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName+'" '+ie.hWnd);!(ckr=ie.GetProperty("@"));)
{if(0!=p.Status){for(;!p.StdErr.AtEndOfStream;)WSH.StdErr.Write(p.StdErr.Read(256));ie.Quit(),WSH.Quit(p.ExitCode)}WSH.Sleep(1e3)}
(this.CKR=ckr).JS=this,ckr.WScript=WSH,ckr.WSH=WSH,ie.Quit(),r=CKR.main(),ckr.quit=!0,WSH.Quit(r);
//
// JScript
//

function hoge()
{
   // Chakra側の関数を呼ぶ
   CKR.fuga();
   
   return 123;
}

@*/

仕組み

条件付きコンパイル 機能を使って、1つのスクリプト内に
エンジン毎(JScript, Chakra)で実行されるコードを共存させています。

また、プロセス間でオブジェクトを共有するために、
InternetExplorer Object の PutProperty / GetProperty を利用しています。

該当部分は、コーディングに直接関係無いのでMinify済みですが、
大体下記のように処理しています。

  • JScript
    1. IEオブジェクトを作成
    2. 実行引数に、IEのウィンドウハンドルを設定して、Chakraエンジンで自身を起動
    3. GetPropertyでChakraのオブジェクトが設定されるまで待機
    4. ChakraのWScriptを、JScriptの定義で上書き
    5. Chakra側のmain関数を呼び出して、戻り値を受け取る
    6. プロセス同期用のフラグを折ってChakra側を終了
    7. main関数の戻り値をWScript.Quitに指定して終了
  • Chakra
    1. 実行引数に渡されたウィンドウハンドルから、IEオブジェクトを特定
    2. PutPropertyで、IEオブジェクトに this を設定
    3. プロセス同期用のフラグが折れるまで待機

別解:一時ファイルを使用する方法

別解として泥臭い方法になりますが、
”戻り値だけ” であれば、一時ファイルで解決可能。
諸々の制限を回避できない分、仕組みはシンプルです。

exit /bで戻り値を設定するため、バッチに埋め込みます。
ファイル名に日時と乱数を使用しているのは、多重起動に対応するため。

0</* ::
@setlocal&set result_file=%tmp%\%~n0@%date:/=%%time::=%.%random%.txt
@cscript /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*
@set /p code=<"%result_file%"&del /f "%result_file%"
@exit /b %code%&endlocal */0;

// エントリーポイント用の関数(プロセスの戻り値を返す)
function main()
{
    WSH.Echo(`${WSH} for Chakra`);
    return 123;
}

try {
    var shell=WSH.CreateObject("WScript.Shell"),
    fso=WSH.CreateObject("Scripting.FileSystemObject"),ExitCode=0;
    ExitCode=main();
}
catch(e){WSH.Echo(`${e.name}:${e.message}`)}
finally{
    // 戻り値をテンポラリに書き出す。
    const procEnv=shell.Environment("Process"),
    file=fso.OpenTextFile(procEnv("result_file"), 2, true, -2);
    file.Write(ExitCode),file.Close();
}

おわりに

一般的に、外部から呼び出されることを想定するプログラムは、
戻り値から、エラー原因を特定できるようにしています。

Chakra を指定したスクリプトでは、スクリプトが失敗しても
戻り値から判断できない状況でしたが、これで一件落着?でしょうか。

元々 Chakra を指定する事自体が、仕様の穴をつくような方法なので、
回りくどい対応を取らざる得なくなる事もありますが、
それを差し引いても ES6 が利用できる点は大きいかと思います。

参考

JScript Chakra wscriptで、ECMAScript 2015 (ES6) を利用する

はじめに

wscript を利用する際に、Chakra エンジンを指定する方法です。

cscript で JScript を利用する場合、Chakra エンジンを指定するために
バッチファイルと組み合わせる方法がありますが、
wscript ではコマンドプロンプトの画面が不要なので、非表示で起動する方法です。

方法

必要なコードを先に載せます。
.js もしくは .jse 形式で保存して、そのまま実行可能です。

//@cc_on for(s='"'+WSH.ScriptFullName+'"',i=0;i<WSH.Arguments.length;i++)s+=' "'+WSH.Arguments(i)+'"';WSH.CreateObject("WScript.Shell").Run("wscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "+s)@if(0)

// 引数を表示
let args=[];
for(let a=WSH.Arguments,c=0;c<a.length;c++)args.push(a(c));
WSH.Echo(`実行引数の数:${WSH.Arguments.length}\n${args.join("\n")}`);

//@end

もしくは、コマンドライン引数を利用しない場合、もう少し短くなります。

//@cc_on WSH.CreateObject("WScript.Shell").Run('wscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName)@if(0)

// 適当にコードを書く
const echo = ()=>WSH.Echo("Hello WScript Chakra");
echo();

//@end

JScript エンジンで実行された後、Chakraエンジンで自身を起動し直す仕組みで、
実行すると、メッセージボックスが表示されます。

JScriptChakra のコードを埋め込む

JScript には、IEスクリプトエンジンとして利用されている経緯から
Chakra には無い、ブラウザの判別用の「条件付きコンパイル」機能が搭載されています。

これは、コメント行にコードを利用するかどうかの判定を埋め込む代物なので、
JScriptコンパイラが読み飛ばす部分(@if(0)~@end)は、処理されません。
つまり、ES6の機能を利用しても、コンパイルエラーしないという事です。

/*@cc_on

WSH.Echo("JScript で実行される範囲");

@if(0) @*/

WSH.Echo(`Chakra で実行される範囲`);

//@end

結果、ES6ベースなコードの埋め込みが実現し、
JScript で実行される範囲」から、Chakraエンジンを指定して自身を起動し直すことで、
Chakraエンジンからは、コメントアウトされていない部分だけが実行されます。

無事コマンドプロンプトを表示せずに、Chakraエンジンでスクリプトが実行されました。

おわりに

JScriptChakra のコードを埋め込む方法は、
こちらを参考にしました。
qiita.com

Chakraで再起動する処理は、コーディングに直接関係ないので
Minifierを使って1行に収めてしまいましたが、
コマンドライン引数を利用する為の処理が、そこそこ長くなっています。

WSH.Arguments は、配列型ではないので無難にforを使いましたが、
もしかすると、より短く書けるかもしれません。

おわり。

WSH WSF JScript・VBScript で、YAML をパースする方法(js-yaml)

JScriptyaml ファイルをパースしたいとき、
wsf形式と外部パッケージのjs-yamlで、簡単に実現できます。
github.com

実装

yamlファイル。(sample.yaml
文字コードは、SJISを使用。

# 設定ファイル
config:
    title: "AppName"
    width: 1280
    height: 720

wsf形式が関連付けされていない環境があるので、
サンプルは適当なバッチに埋め込んで使います。

<!-- :
@setlocal&pushd %~dp0
@cscript //nologo "%~f0?.wsf" %* || pause
@popd&endlocal&exit /b
-->
<job>
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.14/es5-shim.min.js" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/es5-shim/4.5.14/es5-sham.min.js" />
<script src="https://cdnjs.cloudflare.com/ajax/libs/js-yaml/3.13.1/js-yaml.js" />
<object id="shell" progid="WScript.Shell" />
<object id="fso" progid="Scripting.FileSystemObject" />
<script language="JScript">
WSH.Quit(function()
{
    try{
        // 読み込み
        var yamlFile = fso.OpenTextFile("sample.yaml", 1, true);
        var yamlText = yamlFile.ReadAll();
        yamlFile.Close();
        var doc = jsyaml.load(yamlText);
        
        // 表示
        WSH.Echo(doc.config.title);
        WSH.Echo(doc.config.width);
        WSH.Echo(doc.config.height);
        
        // Enter入力待ち
        WSH.StdIn.ReadLine();
    }
    catch(e){
        WSH.Echo(e.name + ":" + e.message);
        return -1;
    }
    return 0;
}());
</script>
</job>

VBScript

<script language="VBScript">
' 読み込み
Set yamlFile = fso.OpenTextFile("sample.yaml", 1, True)
yamlText = yamlFile.ReadAll
yamlFile.Close
Set doc = jsyaml.load(yamlText)

' 表示
WSH.Echo(doc.config.title)
WSH.Echo(doc.config.width)
WSH.Echo(doc.config.height)

' Enter入力待ち
WSH.StdIn.ReadLine

' 解放
Set yamlFile = Nothing
Set doc = Nothing
</script>

仕組み

js-yamlでは、ES5に対応した環境が求められますが、
wsf 形式は、ES3程度の機能しか使えないのでes5-shimライブラリを使って
擬似的にES5環境を扱えるようにします。
読み込んだ後は、jsyamlオブジェクトを扱うだけです。

github.com

その他

オフライン環境で使いたい場合など、
ライブラリの参照にCDNを使いたくなければ、ローカルに保存して使いましょう。

<script src="./es5-shim.min.js" />
<script src="./es5-sham.min.js" />
<script src="./js-yaml.js" />

以上、おわり。

WSH JScript で、高精度に処理時間を計測する(Performance API)

はじめに

JScriptで、スクリプトの処理時間を計測するとき、
Date オブジェクトを使って計測することがありますが、これはミリ秒単位の精度です。

厳密な速度計測が不要であれば、ミリ秒もあれば事足りますが、
より精度の高いマイクロ秒単位が必要であれば、
Web API (Performance API) の Performance インターフェースを使います。

実装

計測対象の関数は、ここから拝借。
フィボナッチ数を出力する関数を作る - Qiita

function do_something()
{
    var fib = function (n) { return n > 2 ? fib(n - 1) + fib(n - 2) : 1; };
    fib(33);
}

Performance インターフェースによる計測

JScript環境の、Performance インターフェースは、
JSON オブジェクト等と同様で、htmlfile オブジェクトから取得できます。

WSH.Quit(function()
{
    try{
        // Performance を取得
        var htmlfile = WSH.CreateObject("htmlfile");
        htmlfile.write('<meta http-equiv="x-ua-compatible" content="IE=Edge"/>');
        var performance = htmlfile.parentWindow.performance;
        htmlfile.close();
        
        // 計測
        var startTime = performance.now();
        do_something();
        var endTime = performance.now();
        
        // 結果
        // 浮動小数が得られる。小数点以下が、マイクロ秒。
        // (小数第1位までしか得られない?)
        WSH.Echo((endTime - startTime) + " ms");
    }
    catch(e){
        WSH.Echo(e.name + ":" + e.message);
        return -1;
    }
    return 0;
}());

過去の手法

Date オブジェクトによる計測

WSH.Quit(function()
{
    try{
        // 計測
        var startTime = new Date();
        do_something();
        var endTime = new Date();
        
        // 結果(※ これでは、ミリ秒に丸められた結果しか得られない)
        WSH.Echo((endTime.getTime() - startTime.getTime()) + " ms");
    }
    catch(e){
        WSH.Echo(e.name + ":" + e.message);
        return -1;
    }
    return 0;
}());

その他メモ

バッチコマンドの計測であれば、powershell の Measure-Command が手軽です。

powershell -c (Measure-Command { (ここに計測するコマンド) })

おわり。 developer.mozilla.org

WSH JScript で、JSONファイルをパースする(Chakra を利用する場合)

メモ

既定のJScriptJSONオブジェクトを利用する方法の別解。
Chakra エンジンを使うとJSONオブジェクトが定義済みで、そのまま使える模様。

実装

エンジンを指定するため、適当な .bat に埋め込んで使います。

0</*! ::
@%windir%\System32\cscript.exe //nologo //E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause
@pause rem 入力待機
@exit /b */0;

(()=>{
    try
    {    // JSONをパース
        const jsonTxt =
        `{"Application": {"title": "SampleApp", "width": 1280, "height": 720}}`;
        let rootObj = JSON.parse(jsonTxt);
        
        // 結果を表示
        let app = rootObj.Application;
        WSH.Echo(`${app.title}\n${app.width}\n${app.height}`);
    }
    catch(e){
        WSH.Echo(e.name + " : " + e.message);
    }
    finally{}
})();

結果

SampleApp
1280
720

以上、おわり。

関連

developer.mozilla.org inemaru.hatenablog.com

WSH JScript Chakra を使用した ES2015(ES6) 対応 ( スクリプトエンジン まとめ )

はじめに

WSH JScript (wscript cscript) で使用できる スクリプトエンジン まとめです。

JavaScript限定、プリインストールされている物だけなので注意。
ツールとして独立しているChakraCoreや、その他サードパーティーは含みません。

JavaScript エンジン について

JScript (JavaScript) だけでも、4種類ほどスクリプトエンジンがあるようです。
以下、それぞれバッチファイルから cscript で実行する際のサンプルコマンドです。

JScript

JScript を使用する際、一般的に指定されるスクリプトエンジンです。
バージョンは、環境によって異なりますが、
現時点のOSサポートを考慮すれば、5.7 か 5.8 でしょう。

rem jscript.dll
%windir%\System32\cscript.exe /nologo /E:JScript "%~f0" %*

JScript.Compact

JScript Compact Profile (ECMA 327) で実行する、スクリプトエンジンです。
MSの情報が少ないので、詳細は未確認ですが
簡単に説明すれば、「いくつかの機能を制限して、パフォーマンス向上を図る設定を使用する」と言った感じでしょうか。
機能の制限について、少なくとも with ステートは、サポートされていないようです。

rem JScript Compact Profile (ECMA 327)
%windir%\System32\cscript.exe /nologo /E:JScript.Compact "%~f0" %*

JScript9

IE9 から使用されている、JScript9.dll をスクリプトエンジンとして指定できます。
ProgID が公開されていないので、CLSID を指定する必要があります。

ちなみに、JScript9 の 9 は、エンジンバージョンと言うわけでは無い様で、
Windows10 では、バージョン 11.0 として扱われていました。(中身は、Chakra っぽい?)

JScript と同程度の機能しか使えませんが、JScript9.dll の方が高速に動作します。
ただし、JScript 互換は完全ではなく WScript.Quit が使用できません。

rem jscript9.dll
%windir%\System32\cscript.exe /nologo /E:{16d51579-a30b-4c8b-a276-0ff4dc41e755} "%~f0" %*

Chakra

Microsoft EdgeJavaScriptエンジン です。
こちらも、ProgID が公開されていないので、CLSID を指定する必要があります。

Chakra を使用する場合は、ES2015(ES6) に対応している為、クラスなどが使用できます。
ただし、JScript9 と同様に、WScript.Quit に加え GetObject、VBArray 等が、使用できません。

rem chakra.dll
%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*

Chakraを使用したスクリプト を バッチに埋め込む場合

JScript9までは、@if を使用してバッチファイルにJScriptを埋め込むことが出来ましたが、
Chakra は、IE由来の条件付きコンパイルをサポートしていないため、@if を解釈できません。
JScript でハマる日々 - m2

@if(0==0) /*
@rem 指定できるエンジンは、JScript9 まで。Chakra 不可
@%windir%\System32\cscript.exe /nologo /E:JScript "%~f0" %*||pause
@exit /b %errorlevel%&*/@end

JScript共通の埋め込み

@if を使用しないshebangモドキがあるので、そちらを使います。
ildar-shaimordanov/hello.jscript.bat

0</* ::
@%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause
@exit /b %errorlevel%&*/0;

Chakra限定の埋め込み

Chakra が「<!--」をコメント扱いすることを利用した方法があります。
https://qiita.com/snipsnipsnip/items/50e4ca88e3ce3f8cffda

<!-- : ^
/*
@%windir%\System32\cscript.exe /nologo /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "%~f0" %*||pause
@exit /b %errorlevel%&*/

JScript9 や Chakra で、戻り値を返したい場合

WScript.Quit が使えないのは、少々痛手なので、代替案です。
Chakra側に定義した関数の戻り値を、JScript側の WScript.Quit で受け取る形です。

.bat にコピペで、動作確認できます。

0</* ::
@cscript /nologo /E:JScript "%~f0" %*
@echo.Result:%errorlevel%&pause
@exit /b %errorlevel%&*/0;//@cc_on @if(0)

// 疑似エントリーポイント
function main()
{
    WSH.Echo(`${WSH} for Chakra`);
    return 123;// WScript.Quit(<戻り値>) の代替
}

var quit=!1;let item,ie,w=WSH.CreateObject("Shell.Application").Windows(),i=0;
for(;i<w.Count;i++)if((item=w(i))&&item.hWnd==WSH.Arguments(0)){ie=item;break}
for(ie.PutProperty("@",this);!quit;)WSH.Sleep(1e3);/*@end @cc_on
for(var ckr,ie=WSH.CreateObject("InternetExplorer.Application"),sh=WSH.CreateObject("WScript.Shell"),
p=sh.Exec('cscript /E:{1B7CD997-E5FF-4932-A7A6-2A9E636DA385} "'+WSH.ScriptFullName+'" '+ie.hWnd);!(ckr=ie.GetProperty("@"));)
{if(0!=p.Status){for(;!p.StdErr.AtEndOfStream;)WSH.StdErr.Write(p.StdErr.Read(256));ie.Quit(),WSH.Quit(p.ExitCode)}WSH.Sleep(1e3)}
(this.CKR=ckr).JS=this,ckr.WSH=WSH,ie.Quit(),r=CKR.main(),ckr.quit=!0,WSH.Quit(r);@*/

仕組みについて
WSH JScript Chakra エンジンの共存による機能制限の回避(WScript Quit 代替) - イネマルのプログラミング備忘録

補足

一応触れておくとChakraCoreを利用する場合、
WScript.Quit は定義済みで普通に呼び出せるので、このような悩みとは無縁です。
https://github.com/microsoft/ChakraCore/blob/bd91335361ddd215622d810cc7341a180abd2db4/bin/ch/WScriptJsrt.cpp#L1090

おわりに

WSH ってだけで、今更感満載のレガシーなのですが、
初期状態のWindowsで使用できる手軽さで、根強く?使われている様子。

今回のまとめは、WSH JScript で、クライアントの環境に依存せずに
ES2015 ベースのコードを使えないか調査したときの副産物です。

結果的に、ES2015 対応は行っていませんが、
TypeScript 経由で、対応できそうかなと言ったところでオチ。

参考リンク