技術

appscript PythonパッケージがSheepShaverに対して動作しない件について調べた

少し前にこちら(Macで重いバックグラウンドアプリケーションを劇的に軽くするPythonスクリプト)の記事についてコメントをいただきまして、私が書いたPythonスクリプトがSheepShaverというアプリに対しては動作しないよ、というコメントだったのですが、ここ数年Macを使っておらず検証用の環境がすぐに用意できない状態だったので結構適当に返答してしまってました。
最近少し暇になったので環境を用意し少し深く問題を調査した結果、原因が判明したので軽くメモ程度に残しておきます。

Carbon APIのGetProcessForPIDとGetProcessBundleLocationについて

コメントへの返信で、appscriptパッケージから最終的に呼ばれる当該関数がうまく動作してないのではないか、と書いていたのでその予想がアタリなのかハズレなのかをまず調べました。
結果を先に述べると、完全にハズレでした。(お恥ずかしい…)

確認のためにC言語でザッと下記のコードを書いて実行してみたところ、SheepShaverでも普通にプロセスIDからアプリケーションバンドルのパスが取得できました。
ちなみに検証環境のOSのバージョンは10.11.6 El Capitanですが、当該関数は現在では非推奨になってるようです(コメント参照)。

//
//  main.c
//  GetBundleLocationFromPID
//
//  Created by peta on 2016/10/02.
//  Copyright © 2016年 peta. All rights reserved.
//

#include <stdio.h>
#include <Carbon/Carbon.h>

int main(int argc, const char * argv[]) {
    if (argc != 2) {
        printf("Usage: %s PID\n", argv[0]);
        return 1;
    }

    int pid;
    pid = atoi(argv[1]);
    if (pid <= 0) {
        printf("The PID is invalid.\n");
        return 1;
    }

    OSStatus stat;
    ProcessSerialNumber psn;
    stat = GetProcessForPID(pid, &psn); // 10.9 から deprecated
    if (stat != errSecSuccess) {
        printf("Failed calling GetProcessForPID().\n");
        return 1;
    }

    FSRef loc;
    stat = GetProcessBundleLocation(&psn, &loc); // 10.9 から deprecated
    if (stat != errSecSuccess) {
        printf("Failed calling GetProcessBundleLocation().\n");
        return 1;
    }

    UInt8 path[PATH_MAX];
    stat = FSRefMakePath(&loc, path, sizeof(path)); // 10.8 から deprecated
    if (stat != errSecSuccess) {
        printf("Failed calling FSRefMakePath().\n");
        return 1;
    }
    
    /*
    CFStringRef str = CFStringCreateWithBytes(kCFAllocatorDefault, path, sizeof(path), kCFStringEncodingUTF16, false);
    CFIndex len = CFStringGetLength(str);
    CFIndex max = CFStringGetMaximumSizeForEncoding(len, kCFStringEncodingUTF16) + 1;
    char *cstr = (char*)malloc(sizeof(char) * max);
    if (!CFStringGetCString(str, cstr, max, kCFStringEncodingUTF16)) {
        free(cstr);
        printf("Failed calling CFStringGetCString().\n");
        return 1;
    }
    
    printf("%s\n", cstr);
    free(cstr);
    */
    
    printf("%s\n", path);
    return 0;
}

※初出のソースではFSRefMakePathで得られたバイト列path(UTF8でエンコードされたデータ)をUTF16としてデコードし再度UTF16としてエンコードしてC言語形式の文字列に変換するという謎な処理を行ってましたが(複数行コメントの部分)、元のデータがUTF8ならターミナル.appでも普通に表示できるだろうということで特に変換せずにprintfに渡すようにしました。

ちょっとappscriptのソースを読んでみた

予想がハズレたので観念して少し深くソースを読んでみることに。
appscript/py-appscript/tags/py-appscript-1.0.0/appscript_2x/lib at master · mattneub/appscript · GitHub

超ざっくり言うとappscriptは、アプリケーションリファレンスに対する最初のプロパティ等の呼び出しがあった際にそのアプリケーションがどういったプロパティやコマンド等を持っているのか動的に調べキャッシュし、2回目以降はそのキャッシュを使う(あくまでもプロパティ名やコマンド名等をキャッシュするだけで多分結果はキャッシュしない)ような仕組みになっています。

まぁ今回の件についてはキャッシュするという点は関係ないんですが、アプリケーションのプロパティ名やコマンド名等を動的に調べるという点が関係ありました。

appscriptでは動的に調べるために、アプリケーションに対して”ascrgdte”(GetDynamicTerminology)Appleイベントというものを送信してるようなんですが、SheepShaverの場合だけこの結果が空でした。
つまりSheepShaverがどんなプロパティやコマンド等を持っているかをappscript側では全く検知不可能で、当然frontmostプロパティもappscript側的には存在しないという事になるので私のスクリプトではエラーが発生してました。

もうこうなるとappscriptパッケージを使うのを止めて別の方法を使うしかないじゃん、と思ったのですが、ここでふと思い当たる点があったので試してみることに…

AppleScriptを許可せよ

結果から言うと下記のコマンドを実行するだけで私のスクリプトでも問題なく動作するようになりました(アプリのパスは実際の環境にあわせてくださいね)。

defaults write '/Applications/SheepShaver_UB_20140201/SheepShaver.app/Contents/Info' NSAppleScriptEnabled -bool YES

アプリのInfo.plistでAppleScriptによる操作を許可しただけです。

多分AppleScriptによる操作を許可しなくてもfrontmostなどのプロパティを読むだけなら可能なんでしょうけど、”ascrgdte”Appleイベントで全てのプロパティやコマンド等を列挙するような操作はAppleScriptによる操作を許可しないとできないんでしょう。

しかし初歩的なことで解決できてしまい脱力してしまいました…

余談

私が書いたスクリプトは6年くらい前のもので今見ると酷いソースだな(特にエラー処理)と思うんですが、もうMacは使ってないので多分改良はしません。
いらっしゃるかどうか分かりませんが改良したい方はご自由にどうぞ。
ただappscript自体開発もサポートもストップし新規開発では使わないよう公式に勧めているくらいなので、私もおすすめはしません。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示