WSHでないjavascriptでSleepする

wshでは、WScript.Sleepすれば良いだけですが、ブラウザーHTAの場合はそうはいきません。
もちろん、setTimeoutの綱渡りを繰り返せば良いだけですが、functionを分割する必要があります。


ということで、手始めに制約ありまくりのsleepを実装してみた。
そのうち制約を解除して行く、、、かもしれない。



まずはサンプル

wshで電卓を制御するスクリプト
SendKeys メソッド

var WshShell = WScript.CreateObject("WScript.Shell");
WshShell.Run("calc");
WScript.Sleep(1000);
WshShell.AppActivate("電卓");
WScript.Sleep(1000);
WshShell.SendKeys ("1{+}");
WScript.Sleep(500);
WshShell.SendKeys("2");
WScript.Sleep(500);
WshShell.SendKeys("~");
WScript.Sleep(500);
WshShell.SendKeys("*3");
WScript.Sleep(500);
WshShell.SendKeys("~");
WScript.Sleep(100);
WScript.Echo("result:9?")

これを、HTAなりHTML(ActiveXなのでIE限定だけど)にするには、結構難儀でした。
もちろん、ビジーループやping(やTimeout)実行を使って良ければ大したことはありませんが、
まっとうな方法を考えると、setTimeoutかと思います。
そうすると、Sleep前後を関数に分割して、setTimeoutによる関数綱渡りです。
8分割ですか。


これを、オレオレ言語で勝手にsetTimeoutするようにしてみました。
HTAにすると、ActiveXの警告がなくて良いかもね。
注目すべきは、
<script id="sampleFunc" type="text/x-junejun-sleep-support-javascript">
のscriptタグ部分で、上のwsh版とほとんど変わりありません。

<html>
<head>
<title>電卓制御</title>
<script type="text/javascript">
<!--
var JunejunSleepSupportJavascript = function(__name) {
	this.__name = __name;
	this.__getName = function(funcName, no, stack) {
		var ret = funcName + '(' + no + ', ' + stack + ')';
		if(no == -1) {
			ret = 0;
			while(this.__getName(funcName, ret, stack) in this.__src) ret++;
		}
		return ret;
	}
	this.__eval = function(__src, __name) {
		var __param = this.__param[__name];
		var ret = eval(__src);
		this.__param[__name] = __param;
		return ret;
	}
	this.__src = {};
	this.__param = {};
	this.__reSpecial = /[\r\n;]\s*(sleep|call)\s*\(/m;
	this.__step = function(funcName, no, stack) {
		var name = this.__getName(funcName, no, stack);
		var src = this.__src[name];
		var bSet = false;
		if(!bSet) {
			if(this.__reSpecial.test(src)) {
				// 特殊処理の場合は、その前までを実行し、特殊処理する
				this.__eval(RegExp.leftContext, name);	// $`
				src = RegExp.rightContext;	// $'
				var cond = src.substr(0, this.__getPair(src, "(", ")", 0));
				src = src.substr(cond.length + 1);
				this.__src[name] = src;
				switch(RegExp.$1) {
					case "sleep":
						// sleepの場合は、setTimeout
						setTimeout(this.__name + '.__step("' + funcName + '", ' + no + ', ' + stack + ')', this.__eval(cond, name));
						bSet = true;
						break;
					case "call":
						// callの場合は、stackを加算、実行する
						this.__eval('this.__run(' + cond + ', "' + funcName + '", ' + no + ', ' + (stack + 1) + ')', name);
						bSet = true;
						break;
				}
			} else {
				// 問題なければ、まるまる実行
				this.__eval(src, name);
			}
		}
		if(!bSet) {
			delete this.__src[name];
			delete this.__param[name];
			if(stack > 0) this.__step(funcName, no, stack - 1);
		}
	}
	this.__getPair = function(org, pre, suf, start) {
		var preIdx = org.indexOf(pre, start);
		var sufIdx = org.indexOf(suf, start);
		while(sufIdx > preIdx && preIdx > 0) {
			preIdx = org.indexOf(pre, preIdx + 1);
			sufIdx = org.indexOf(suf, sufIdx + 1);
		}
		return sufIdx;
	}
	this.__reCommentLine = /\/\/[^\r\n]*[\r\n]/gm;
	this.__reCommentBlock = /\/\*((?!\*\/)(.|\s))*\*\//gm;
	this.__run = function(funcName, param, stackName, no, stack) {
		var ss = document.getElementsByTagName('script');
		for(var i = 0; i < ss.length; i++) {
			if(ss[i].type == "text/x-junejun-sleep-support-javascript" && ss[i].id == funcName) {
				// 行コメントとブロックコメントを削除する
				var src = ss[i].text.replace(this.__reCommentLine, "");
				src = src.replace(this.__reCommentBlock, "");
				// 実行する
				if(no == -1) no = this.__getName(stackName, no, stack);
				var name = this.__getName(stackName, no, stack);
				this.__src[name] = src;
				this.__param[name] = param;
				this.__step(stackName, no, stack);
				break;
			}
		}
	}
	this.run = function(funcName, param) {
		this.__run(funcName, param, funcName, -1, 0);
	}
};
//-->
</script>

<script type="text/javascript">
<!--
/* この版の注意
・sleepはブロック内では使用できません(if文中やループ中は無理)
・callはブロック内では使用できません
・eval内のvar宣言変数は、sleep等をまたぐことができません
*/


var __$_ = new JunejunSleepSupportJavascript("__$_");
function main() {
	alert('start main');
	__$_.run('sampleFunc', {});
	alert('end main');
}
//-->
</script>
<script id="sampleFunc" type="text/x-junejun-sleep-support-javascript">
	__param.wsh = new ActiveXObject("WScript.Shell");
	__param.wsh.Run("calc");
	sleep(1000);
	__param.wsh.AppActivate("電卓");
	sleep(1000);
	__param.wsh.SendKeys ("1{+}");
	sleep(500);
	__param.wsh.SendKeys("2");
	sleep(500);
	__param.wsh.SendKeys("~");
	sleep(500);
	__param.wsh.SendKeys("*3");
	sleep(500);
	__param.wsh.SendKeys("~");
	sleep(100);
	alert("result:9?");
</script>
</head>

<body onload="main();">
</body>
</html>

解説

サンプルの動作
  • bodyのonloadでmain関数を呼ぶ
  • main関数では、今回作成したオブジェクトのrunメソッドを呼ぶ
  • runメソッドが、id="sampleFunc"をsleep前後で分割実行する
  • sleepをsetTimeoutにしているため、main関数は通常先に終了します

サンプルはActiveXを使っているので、IE限定ですが、それがなければ、Firefoxでも動く、、、はず。

使い方

JunejunSleepSupportJavascriptクラスに全てを詰め込んでます。
このクラスをグローバル変数にnew(引数はグローバル変数名)して使用する想定です。
このクラスの定義部分を外部スクリプトにすれば、結構すっきりするのではないでしょうか。


そして、idが関数名相当で、type="text/x-junejun-sleep-support-javascript"のscript(オレオレ関数)を書きます。
基本、普通のjavascriptとして記述してもらえれば、OKです。


最後に、グローバルにしたオブジェクトのrunメソッドを、第一引数:関数名相当、第二引数:渡すパラメーターで呼び出せば、勝手にsetTimeoutしながらeval実行します。
なお、第二引数は__paramとして参照・更新できます。
オブジェクトの__で始まるものは変更しないでくださいね。
もちろん、runメソッドも。


sleep以外に、callも使えます。
callの引数はrunメソッドと同じで、別のオレオレ関数を呼び出します。

基本思想

なるべくjavascript
処理速度のために、まとめてevalする。
sleep/call前後で分割して、前をeval、後ろをsetTimeout。
これを、__stepメソッドで実装しています。
this以下が見えてしまうのはやむなしとして、それ以外をなるべく見えないようにするために、__evalメソッドを用意しています。
ローカル変数は、__paramを使う想定。
(通常、runメソッドの第二引数をオブジェクトにして、そこに詰め込むのがお勧め)


制約は、main関数のあるscript内にコメントとして書きましたが、

  • sleep/callはブロック内では使用できません(if文中やループ中は無理)
  • var宣言変数は、sleep等をまたぐことができません(__paramがオブジェクトならそこに追加するとか)

ブロック内で使えないのがかなり痛いです。
少なくともwhileはサポートしたいところ。