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

以下自分のためにメモ。[こちら](http://qiita.com/hachi8833/items/12a06c5ba895f95eaf67)も参照。

[いろいろさまよった](http://qiita.com/hachi8833/items/463264f531474a856064)あげく、最終的に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`起動バイナリを起動する方法。

```lang: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))
}
```

[こちら](http://tkuchiki.hatenablog.com/entry/2014/11/10/123447)を参考に上のコードをでっちあげて起動しても、うんともすんとも言わない。コマンドを`pwd`に差し替えて調べると、Goの実行時のカレントディレクトリがホームディレクトリになってしまっていた。

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

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

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

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

```go: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

```js: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));
});
```
```json: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 build` や `go nstall` のときはビルド後のElectronバイナリを起動するための措置。
> Goプログラムで、runのときとbuildのときで挙動を変える方法がわかったらそれを使いたいのだけど、うまくググれないでいるのでとりあえずこうしてある。どなたか教えてください。フラグや環境変数を与えるみたいに、人間様がいちいち指定したくないので。

> 追記: 最初、`go run`と`go 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 run`、`go build`、Electronなどコンパイルを一括で行えるようにする。
- このラッパーに、httpディレクトリをウォッチして更新を検出する機能を追加する。そうすれば、`go run`で起動しながら動的にサーバー用アセットディレクトリ内のhtmlやcssやjsを更新して、きびきび開発できるようになりそう。動的な更新検出には[Viper](http://qiita.com/hachi8833/items/fe3d5d6b1285c5ef1821)が使えそう。
- さらにこのラッパーで、gulpみたいなsassやcoffee scriptコンパイルも行えるようにする。普通にラッパーからgulp呼ぶ方が早そうではあるけど、一か所にまとまっている方が楽。
- ORBだのhttpサーバーだのは各自好きなものを使えるようにしつつ、ゆるく標準化しておく。

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

## 追記

こうやって無理やりGoバイナリを仕込んだElectronのMASバイナリ（[0.35.6 を使用](https://github.com/electron-userland/electron-osx-sign/wiki/2.-Electron-Compatibility)）を、後はApp Storeに登録するだけになったのだけど、`codesign`で[entitlement](https://github.com/electron-userland/electron-osx-sign/wiki/3.-App-Sandbox-and-Entitlements)をどうしても指定できない。

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

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