post Image
ElectronのスタンドアロンMacアプリに無理やりGoのHTTPサーバーを仕込む

以下自分のためにメモ。こちらも参照。

いろいろさまよったあげく、最終的にMac用のElectronスタンドアロンアプリにGoのhttpサーバーを仕込むことにした。

なぜ素直にJSでサーバーサイドを書かないのかというと、もうGoで作っちゃったからです。

GoのhttpサーバーバイナリをElectronアプリに仕込む

そのためにはGoのhttpサーバーをElectronアプリに仕込み、以下のいずれかを実現する必要がある:

  • A: Electron からGo httpサーバーを起動する
  • B: Go httpサーバーからElectronを起動する

どちらを選ぶにしても、Electronを終了したらGoサーバーもしゅっと終了して欲しい。当初Aにしようかと思ったけど、先にhttpが起動してGUIが後から起動するのが自然だろうということでBに決定。

GoからElectronを起動するには

Electronの起動バイナリElectronは、Mac用アプリ内部のContents/MacOS/の下に置かれているので、同じ場所にGoライブラリも置くことにする。

そこではたと困ったのは、Goバイナリから隣のElectron起動バイナリを起動する方法。

go
package main

import (
        "fmt"
        "os"
        "os/exec"
)

func main() {
        out, err := exec.Command("Electron").Output()

        if err != nil {
                fmt.Println(err)
                os.Exit(1)
        }

        fmt.Println(string(out))
}

こちらを参考に上のコードをでっちあげて起動しても、うんともすんとも言わない。コマンドをpwdに差し替えて調べると、Goの実行時のカレントディレクトリがホームディレクトリになってしまっていた。

シェルの仕様からしてわかるような気もするけど、とにかくGoの中からカレントディレクトリを取得する必要がある。

Goのすごい人であるmattnさんの技を使わせてもらおうと思ったのだけど、Windowsでしか動かないっぽいので、試行錯誤の上https://github.com/kardianos/osextでカレントディレクトリへのフルパスを取得できた。Macでしか試してないけど、WinやLinuxでも動くといいな。

今は意味ないけど、これまたmattnさんのgithub.com/mattn/go-pipelineを使ってシェルコマンドを複数行実行できるようにしてある。最後Printlnでコマンドを実行するのが妙な感じ。

なんやかやで以下のようになった。本編アプリからこの記事用に抜粋したので冗長な部分があるけど勘弁。
https://github.com/hachi8833/electron-go-sample にも同じものを置きました。

elec.go
package main

import (
    "flag"
    "log"
    "net/http"
    "os"

    "github.com/k0kubun/pp"
    "github.com/kardianos/osext"
    "github.com/mattn/go-pipeline"
    "github.com/zenazn/goji"
    "github.com/zenazn/goji/graceful"
    "github.com/zenazn/goji/web"
)

func hello(c web.C, w http.ResponseWriter, r *http.Request) {
    pp.Fprintf(w, "<h1>Hello, Electron-Go!</h1>")
}

func main() {
    flag.Set("bind", ":8080")
    goji.Get("/", hello)
    go goji.Serve()

    err := launchElectron()

    if err == nil {
        terminate(0)
    } else {
        log.Fatal(err)
        terminate(1)
    }

    // Termination.
    defer func() {
        terminate(0)
    }()
    return
}

func terminate(code int) {
    graceful.ShutdownNow()
    os.Exit(code)
}

func launchElectron() error {
    // Get current path
    var folderPath string
    var err error

    cond := "run" // go build の時は何か適当なのに変える(ダサ...)
    var elec, elecarg string
    if cond == "run" {
        // launch directory
        folderPath, err = os.Getwd()
        if err != nil {
            return err
        }
        elec = "electron"
        elecarg = "./Electron"
    } else {
                // launch binary
        folderPath, err = osext.ExecutableFolder()
        if err != nil {
            return err
        }
        elec = folderPath + "/Electron"
        elecarg = ""
    }

    out, err := pipeline.Output(
        []string{elec, elecarg},
    )
    if err != nil {
        pp.Println(err)
        return err
    }
    pp.Println(string(out))
    return nil
}

次はElectronアプリをでっちあげる。上のelec.goと同じディレクトリに以下を作る。ディレクトリ名をElectronにしてあるのは、MacのElectronアプリの起動バイナリと同じ名前にしようと思って。

Electron/
|– main.js
|– package.json

main.js
var app = require("app");
var BrowserWindow = require("browser-window");
var Menu = require("menu");
var mainWindow = null;

app.on("window-all-closed", function(){
    app.quit();
});

app.on("ready", function () {
    mainWindow = new BrowserWindow({
        width: 1280,
        height: 1024,
        'node-integration': false //これを指定しないと httpサーバー側のJSが動かない
    });
    // mainWindow.openDevTools();
    mainWindow.loadUrl('http://localhost:8080/');
    mainWindow.on("closed", function () {
        mainWindow =  null;
    });

    // Create the Application's main menu
    var template = [{
        label: "Application",
        submenu: [
            { label: "About enno_go", selector: "orderFrontStandardAboutPanel:" },
            { type: "separator" },
            { label: "Quit", accelerator: "Command+Q", click: function() { app.quit(); }}
        ]}, {
        label: "Edit",
        submenu: [
            { label: "Undo", accelerator: "CmdOrCtrl+Z", selector: "undo:" },
            { label: "Redo", accelerator: "Shift+CmdOrCtrl+Z", selector: "redo:" },
            { type: "separator" },
            { label: "Cut", accelerator: "CmdOrCtrl+X", selector: "cut:" },
            { label: "Copy", accelerator: "CmdOrCtrl+C", selector: "copy:" },
            { label: "Paste", accelerator: "CmdOrCtrl+V", selector: "paste:" },
            { label: "Select All", accelerator: "CmdOrCtrl+A", selector: "selectAll:" }
        ]}
    ];

    Menu.setApplicationMenu(Menu.buildFromTemplate(template));
});
package.json
{
  "name": "elec",
  "version": "1.0.0",
  "description": "Web GUI for enno_go by Electron",
  "main": "main.js",
  "keywords": [],
  "author": "hachi8833",
  "license": "BSD"
}

go run

ここまで作ったら、go run elec.goを実行。goからElectronディレクトリを叩いてGUIが表示され、elec.go内部のhttpサーバーにアクセスして「Helloなんちゃら」が表示されることと、⌘+Qで終了することを確認。

ビルド

次はビルド。はなはだかっこ悪いけど、cond := "run"の引用符の中を何か違うのに変える。Mac用Electronアプリのための対応なので、Windowsではこんなことしなくていいと思う。

go run のときはビルド前のElectron/ディレクトリ、go buildgo nstall のときはビルド後のElectronバイナリを起動するための措置。
Goプログラムで、runのときとbuildのときで挙動を変える方法がわかったらそれを使いたいのだけど、うまくググれないでいるのでとりあえずこうしてある。どなたか教えてください。フラグや環境変数を与えるみたいに、人間様がいちいち指定したくないので。

追記: 最初、go rungo buildの両方でosext.ExecutableFolder()でバイナリパスを取得できていたのだけど、Goを1.6にアップグレードした後マシンをリブートしたらなぜかgo runで取得できなくなった。代わりに本来のos.Getwd()の方がなぜかgo runでパスを取れるようになったので、上のコードとGithubのを修正。

追記: と思ったらまたosext.ExecutableFolder()でも動くようになった。狐に化かされたんだろうか。

そして以下を順に実行し、GoとElectronをそれぞれビルド。

  • go build
  • electron-packager Electron elec --platform=darwin --arch=x64 --version=0.36.7

mv elec /elec-darwin-x64/elec.app/Contents/MacOSでGoバイナリをElectronアプリ内に仕込む。

最後に/elec-darwin-x64/elec.app/Contents/Info.plistを開いて

<key>CFBundleExecutable</key>
<string>Electron</string>

上を以下のように書き換える

<key>CFBundleExecutable</key>
<string>elec</string>

これで/elec-darwin-x64/内のelec.appをダブルクリックすればアプリが起動する。起動直後にMacのファイアウォールが警告を表示するけど、これはシステム環境設定で変更すれば非表示にできるはず。もしかするとElectronに証明書を置いたら出なくなるのだろうか。どなたかご存じですか。

上ではpackage.jsonの置き換えなどを手動でやってるけど、本編アプリではシェルスクリプトを適当にこしらえて一括でビルドしている。

スタンドアロンにしたことでアプリのサイズは100MB超えになってしまったけど、GoでクロスプラットフォームのウェブGUIスタンドアロンアプリを開発するめどが立ったのは本当にうれしい。自分にとっては「本物のGo GUI」。

まだ誰もやってないみたいだけど、こんな感じでそのうちElectron+GolangでMac以外にも、Win/Linux/Android/iOS向けのアプリを一括で焼き焼きできるようになったらいいな。

今後の夢

今は手作り感出すぎているのだけど、もっと手を加えて洗練させて、Go+ElectronのクロスプラットフォームウェブGUIフレームワークに成長したらいいな。誰かやりませんか。

以下ざっくりとメモ。

  • Go httpサーバーから直接 Electronを起動するのをやめて、ElectronとGo httpサーバーの間に何らかのラッパーを置き、httpサーバーの起動と終了をラッパーで管理する。これならGo以外にどんなhttpサーバーでも使えるようになりそうだし、Electronがアップデートされたときにも更新しやすくなるのでは。
  • このラッパーをGoパッケージ化し、気楽にElectronと連携できるようにする。
  • ビルドスクリプトを整備し、go rungo build、Electronなどコンパイルを一括で行えるようにする。
  • このラッパーに、httpディレクトリをウォッチして更新を検出する機能を追加する。そうすれば、go runで起動しながら動的にサーバー用アセットディレクトリ内のhtmlやcssやjsを更新して、きびきび開発できるようになりそう。動的な更新検出にはViperが使えそう。
  • さらにこのラッパーで、gulpみたいなsassやcoffee scriptコンパイルも行えるようにする。普通にラッパーからgulp呼ぶ方が早そうではあるけど、一か所にまとまっている方が楽。
  • ORBだのhttpサーバーだのは各自好きなものを使えるようにしつつ、ゆるく標準化しておく。

「そんな車輪などとっくに発明されておるわ!」というのがあったら教えてくださいw

追記

こうやって無理やりGoバイナリを仕込んだElectronのMASバイナリ(0.35.6 を使用)を、後はApp Storeに登録するだけになったのだけど、codesignentitlementをどうしても指定できない。

  • Entitlement(child/parent)を指定すると、署名は成功するのにElectronアプリが起動しなくなる。
  • Entitlementを指定しないと、Electronアプリは起動するけど、今度はApp Storeで「sandoboxがねえぞゴルァ」と怒られる。
  • codesign -iオプションでBundle IDを指定しても変わらない。
  • Electron-packagerのチームは6.0.0でelectron-osx-signをpackagerに統合しようと奮闘中らしいけどまだリリースされていない。ドキュメントの一部が先行して更新されているのでいろいろ紛らわしい。

はまった…どなたか切り抜けた方はいらっしゃいますか?


『 Go 』Article List
Category List

Eye Catch Image
Read More

Androidに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

AWSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Bitcoinに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

CentOSに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

dockerに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

GitHubに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Goに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Javaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

JavaScriptに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Laravelに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Pythonに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Rubyに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Scalaに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Swiftに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Unityに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Vue.jsに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

Wordpressに関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。

Eye Catch Image
Read More

機械学習に関する現役のエンジニアのノウハウ・トレンドのトピックなど技術的な情報を提供しています。コード・プログラムの丁寧な解説をはじめ、初心者にもわかりやすいように写真や動画を多く使用しています。