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

趣味プログラマのメモ

Windows カスタムURLスキームの設定方法(URLからアプリを起動する)

メモ

ブラウザからローカルのアプリケーションを起動するには、カスタムURLスキームを利用する。
Windows では、レジストリの設定により実現できる。

レジストリの設定

任意のアプリ(MyApp.exe)を登録する例
これで、myapp:// が認識されるようになる。
実行引数には、URL文字列が入るので適当なパーサーでパラメータを取り出して使う。

Windows Registry Editor Version 5.00

[HKEY_CLASSES_ROOT\MyApp]
@="URL:myapp"
"URL Protocol"=""

[HKEY_CLASSES_ROOT\MyApp\shell]

[HKEY_CLASSES_ROOT\MyApp\shell\Open]

[HKEY_CLASSES_ROOT\MyApp\shell\Open\Command]
@="\"C:\\Program Files\\MyApp\\MyApp.exe\" \"%1\""

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を使いましたが、
もしかすると、より短く書けるかもしれません。

おわり。

V言語 vlang コンソールで入力待ちする

メモ

・OS 非依存
標準入力で待機する。

import os

fn main() {
    // 入力待ち
    os.get_line()
}

Windows 環境
pause コマンドで待機する。

import os

fn main() {
    // 入力待ち
    os.system('pause')
}

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