2015/04/28

JavaScript for Automation (JXA) を利用してMac AppStoreからアプリケーションを自動インストールさせる

OS X 10.10からJavaScript for Automation (通称 JXA)が導入されて、オートメーションのためのスクリプトがより書きやすくなりました。

そのJXAを使って、「AppStore.appを起動させてから、購入済みのアプリをInstalllさせる」というスクリプトを書いてみました。

その結果が次の地味なgifです。購入済みリストからMouseposéを探しだしてインストールさせています。

JXAスクリプトとChefやAnsibleなんかを組み合わせると、自動でストアアプリも入れられるようになる…かもしれません。

前提

  • OS X 10.10 以降であること
  • AppStore.appが特定のApple IDでサインイン済みであること
  • Accessibilityでスクリプトを実行するアプリケーション (Terminal.appやScript Editor.appなど) に許可があること

インタプリタの起動

導入部分は次の記事が詳しいので参考にしてください。

とりあえず今回はコンソールからインタラクティブモードで実行させます。

$ osascript -l JavaScript -i

インタプリタ上で次のようにするだけで、AppStoreアプリが起動します。

>> Application("App Store").activate()
=> true

最小化ボタンのクリック

AppStore.appではapp.buttons[1]が最小化ボタンのようだったので、試しにスクリプトで最小化ボタンをクリックさせてみました。

>> system = Application("System Events")
=> Application("System Events")
>> app = system.processes["App Store"].windows["App Store"]
=> Application("System Events").processes.byName("App Store").windows.byName("App Store")
>> app.buttons[1].description()
=> "minimize button"
>> app.buttons[1].click()
=> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").buttons.at(1)

Application('System Events')はUIオートメーションのためのアプリケーションで、このアプリから他のプロセスのウィンドウを触りにいっています。

UI要素の列挙

app.entireContents()でUI要素のツリー内容がダンプされます。このコマンドで操作したい対象の見当をつけるとよいでしょう。

>> app.entireContents() # ↓ 実際は改行や整列なし
=> [Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(0).groups.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(0).groups.at(0).buttons.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(0).groups.at(0).buttons.at(1),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(1),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(1).radioButtons.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(2),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(2).radioButtons.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(3),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(3).radioButtons.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(4),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(4).radioButtons.at(0),
    Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(5),
        :

なお、properties()attributes()で、オブジェクトの属性などの列挙もできるようです。

  • app.properties()
  • app.attributes()

Purchasesボタンのクリック

上のタブから「購入済み」ボタンをクリックさせてみます。先ほどのダンプからそれっぽいものを見つけて、description()してみます。

>> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(4).radioButtons.at(0).description()
=> "Purchases"

Purchasesなので、これをクリックさせてみましょう。

>> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").toolbars.at(0).groups.at(4).radioButtons.at(0).click()

ちなみに、上記の式は次の式と等価です。

>> app.toolbars[0].groups[4].radioButtons[0].click()

これで、「購入済み」タブに移動して、例えば次のような購入済みのアプリの一覧が表示されるはずです。

Installボタンのクリック

「購入済み」タブで、再度app.entireContents()してみます。

>> app.entireContents()
=> [...
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).uiElements.byName("Mouseposé"),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).uiElements.byName("Mouseposé").groups.at(0),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).uiElements.byName("Mouseposé").groups.at(0).images.at(0),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.byName("Mouseposé"),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.byName("Mouseposé").uiElements.byName("Mouseposé"),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.byName("Mouseposé").uiElements.byName("Mouseposé").staticTexts.byName("Mouseposé"),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(1),
app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(1).staticTexts.byName("Boinx Software"),
    :

適当にアプリ名を取ってみましょう。

>> app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.name()[0]
=> "Mouseposé"

このリストから、直でボタンクリックしたかったのですが、columns.at(0).uiElements.at(8)あたりの要素をクリックしてもうまくいかなかったので、アプリ詳細ページに飛ばして、そこでインストールすることにしました。

>> app.groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.byName("Mouseposé").uiElements.byName("Mouseposé").staticTexts.byName("Mouseposé").click()
=> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).tables.at(0).columns.at(0).uiElements.at(8).lists.at(0).groups.at(0).uiElements.byName("Mouseposé").uiElements.byName("Mouseposé").staticTexts.byName("Mouseposé")

移動後に、

>> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).groups.at(0).groups.at(0).buttons.at(0).description()
=> "Install, Mouseposé, Free"
>> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).groups.at(0).groups.at(0).buttons.at(0).click()
=> Application("System Events").applicationProcesses.byName("App Store").windows.byName("App Store").groups.at(0).groups.at(0).scrollAreas.at(0).uiElements.at(0).groups.at(0).groups.at(0).buttons.at(0)

でインストールすることができました。

まとめ

上記の結果をまとめて関数化してみました。

通信状態によって、画面遷移に時間がかかることがあるので、てきとうにdelay()で数秒待ったりしてやって、できたスクリプトが次の通りです。

function installFromAppStore(targetAppName) {
    var AppStoreApp = "App Store"
    var app = Application("System Events").processes[AppStoreApp].windows[AppStoreApp]
    Application(AppStoreApp).activate()
    delay(3)
    app.toolbars[0].groups[4].radioButtons[0].click()
    delay(3)
    var tableRows = app.groups[0].groups[0].scrollAreas[0].uiElements[0].tables[0].columns[0].uiElements
    var len = tableRows.length
    for (var i = 1; i < len; ++i) {
        var elem = tableRows[i].lists[0].groups[0].uiElements
        var name = elem.name()[0]
        if (name === targetAppName) {
             elem.byName(targetAppName).uiElements.byName(targetAppName).staticTexts.byName(targetAppName).click()
             delay(3)
             app.groups[0].groups[0].scrollAreas[0].uiElements[0].groups[0].groups[0].buttons[0].click()
             delay(3)
             break
        }
    }
}
installFromAppStore("Mouseposé")

問題点

上記のスクリプトでストアアプリをインストールすることはできましたが、今のところ次のような問題があります。

  • 現状、最大1つしかインストールできない (同時にやって大丈夫かどうか不明)
  • インストール完了を知る術がない
  • AppStoreのレイアウトが変わるたびに追従させる必要がある
  • インストール自動化したりしたら、Appleに怒られちゃうかもしれない

(おまけ) アプリの検索

検索フィールドの文字列を取ったり、

>> app.toolbars.at(0).groups.at(6).textFields.at(0).value()
=> "foobar"  // 検索フィールドにてきとうに入れてみた文字列

検索フィールドに文字列を入れたり、

>> app.toolbars.at(0).groups.at(6).textFields.at(0).value = "newValue!!"
=> "newValue!!"

検索フィールドの文字列で検索できたりします。

>> app.toolbars.at(0).groups.at(6).textFields.at(0).buttons.at(0).click()

関連リンク

0 件のコメント:

コメントを投稿

注: コメントを投稿できるのは、このブログのメンバーだけです。