背景
各所、リモートワーク化が進んでいるかと思います。リモートワークを始めて感じるのが、圧倒的な運動不足感です。外に出る機会が減ったために、自律的に運動をしないといけなくなりました。ですが、行動習慣を変えるのは難しいことです。だったら、ランダムに、唐突に運動をせざるを得ない状況を作れば良いんじゃないか?と思い、唐突に「みんなで筋肉体操」の動画を開いてくれるアプリを作ることにしました。仕事中は特に何もしなくても自動で起動していてほしいので、service化のライブラリが公開されていたGolangでやることにしました1。本当はクロスコンパイルして配布したかったのですが、Mac上でクロスコンパイルしたプログラムがWindowで想定通りの挙動にならず、見送っています。
「みんなで筋肉体操」について
NHKでたまに放映されている、短時間で割とキツめな筋トレができる番組です。NHK WORLD-JAPANのアカウントが英語版の動画をYoutubeにアップしていたので、こちらのページを開くようにしました。
開発環境
- macOS Catalina 10.15.4
- Go 1.14.1
やったこと
ソースはこちらです。
1. service化
serviceを作れるkardianos/serviceというライブラリがあるので、こちらを使いました。service.Controlのaction
引数に以下の引数を渡すことで実行ファイルをサービスとしてインストール、起動など行うことができるようになります。
フラグ名 | 効果 |
---|---|
start | サービスの起動 |
stop | サービスの停止 |
restart | サービスの再起動 |
install | サービスのインストール |
uninstall | サービスのアンインストール |
注意点
Windowsの service start
時にThe dependency service does not exist or has been marked for deletion.
のエラーが発生しました。これはservice.Config.Dependencies
にインストールされていないサービスが定義されていたことが原因でした。いらないDependenciesは含めないようにしましょう2。Dependenciesを削除することでエラーは解消されました。
2. XX分ごとにランダム発生させる
「XX分ごと」はGolang標準パッケージに含まれているtime.NewTickerを使って実装しました。「ランダム発生させる」は引数で受け取った確率でtrueを返す関数を自作して対応しています(以下のisLucky
関数)。ちなみに再生される動画も7つの動画からランダムで選択されるようになっています。色んな部位を筋トレできたほうが楽しいので。
func (p *program) run() {
t := time.NewTicker(time.Duration(p.IntervalTime) * time.Minute)
for {
select {
case <-t.C:
// 省略
if !isLucky(p.Percentage) {
mtlogger.WriteUnluckyLog()
break
}
url := getRandomURL(p.VideoList)
openVideo(url)
// 省略
}
}
}
// (引数)%でtrueを返す
func isLucky(percent int) bool {
rand := generateRandomInteger(100)
if rand < percent {
return true
}
return false
}
// 0〜(max-1)の整数をランダムに返す
func generateRandomInteger(max int) int {
rand.Seed(time.Now().UnixNano())
return rand.Intn(max)
}
3. ブラウザを開く
open-golangというライブラリが公開されているのでこちらを使いました。open.Run
関数で引数に受け取ったファイルをOSに登録されているデフォルトのアプリで開いてくれます(ファイルをダブルクリックしたときと同じ感覚ですね)。URLを受け取った場合はブラウザで開いてくれます。
func openVideo(url string) {
err := open.Run(url)
if err != nil {
panic(err)
}
}
できなかったこと
Windowsの場合、serviceから関数が呼び出された場合はブラウザが起動されませんでした。serviceがブラウザをバックグラウンドで実行してしまうようです。これは解決できませんでした。open-golang
内のソースでは、以下の部分が実際の起動部分です。
func open(input string) *exec.Cmd {
cmd := exec.Command(runDll32, cmd, input)
//cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
return cmd
}
コメントアウトされているcmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: true}
の部分が怪しいかなと思い、以下のように書き換えてビルドしてみたのですが、結果は変わらずでした。
func open(input string) *exec.Cmd {
cmd := exec.Command(runDll32, cmd, input)
// HideWindow: false -> 「隠さない=表示される」だと思ったがバックエンドで実行された
cmd.SysProcAttr = &syscall.SysProcAttr{HideWindow: false}
return cmd
}
4. 設定用のGUI
設定用UIを作成します。CUIでも良かったんですが、GUIが作れるFyneというライブラリがあるとのことでそちらを使ってみました。
ハマったこと:クロスコンパイル
クロスコンパイルしようとすると以下のエラーが出ました。
$ bash build/build.sh
../../../go/src/fyne.io/fyne/internal/painter/gl/gl_core.go:11:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/gl/v3.2-core/gl
../../../go/src/fyne.io/fyne/internal/driver/glfw/clipboard.go:5:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/glfw/v3.2/glfw
../../../go/src/fyne.io/fyne/internal/painter/gl/gl_core.go:11:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/gl/v3.2-core/gl
../../../go/src/fyne.io/fyne/internal/driver/glfw/clipboard.go:5:2: build constraints exclude all Go files in /go/src/fyne.io/fyne/vendor/github.com/go-gl/glfw/v3.2/glfw
よく見てみると、クロスコンパイル時の注意点はFyneの公式サイトに説明があり、以下の対応が必要です。
- gcc等の各OS用Cコンパイラをインストールする
- ↑のコンパイラを環境変数
CC
に設定する - 環境変数に
CGO_ENABLED=1
を設定する
Windowsの場合、mingw-w64をhomebrew経由でインストールし、以下のコマンドでビルドすることができました。
$ GOOS=windows GOARCH=amd64 CC=x86_64-w64-mingw32-gcc CGO_ENABLED=1 go build -o [outfile] [package]
課題1: Linuxビルド
Linuxビルドの場合、上記対応に加えてX11が必要…とのことでX11もインストールしたのですが、ビルドがうまく行かず…。調査して直せる気がするんですが、周りにLinuxをメイン機にしている人があまりいないので、一旦見送りました。直せたら追記します。
課題2: fyne-crossによるビルド
fyne-crossというDockerを使った専用のツールが公開されています。ですが、ビルドされたMac用アプリをMacで起動したところエラーが出てしまいました。使用してないです。
5. Makeでビルドスクリプトを書く
Makeはまともに使ったことなかったのですが、Golang書くならMake書こうという記事をいくつか見かけたので書いてみました。こちらの記事を参考に、以下のMakefileを作成しました。
GOCMD=go
GOBUILD=$(GOCMD) build
GOCLEAN=$(GOCMD) clean
GOTEST=$(GOCMD) test
GOGET=$(GOCMD) get
# 中略(変数定義)
.PHONY: build
all: build zip
pre:
$(GOGET) -u github.com/acnaman/suddenly-muscle-trainer/runtime/setting
$(GOGET) -u github.com/kardianos/service
$(GOGET) -u github.com/skratchdot/open-golang/open
$(GOGET) -u fyne.io/fyne
mkdir -p $(OUT_MAC)
mkdir -p $(OUT_WIN)
mkdir -p $(OUT_ZIPPER)
mkdir -p $(PRODDIR)
build:
$(GOBUILD) -o $(BINARY_PATH_ZIPPER) $(ZIPPERDIR)
$(GOBUILD) -o $(BINARY_PATH_RUNTIME) -v $(RUNTIMEDIR)
$(GOBUILD) -o $(BINARY_PATH_SETTING) -v $(GUIDIR)
$(WINBUILDFLAG) $(GOBUILD) -o $(BINARY_PATH_RUNTIME_WIN) -v $(RUNTIMEDIR)
$(WINBUILDFLAG) $(WINBUILDFLAG_FYNE) $(GOBUILD) -o $(BINARY_PATH_SETTING_WIN) -v $(GUIDIR)
zip:
$(BINARY_PATH_ZIPPER) -o $(PROD_MAC_PATH) $(BINARY_PATH_RUNTIME) $(BINARY_PATH_SETTING) $(SETTINGFILE)
$(BINARY_PATH_ZIPPER) -o $(PROD_WIN_PATH) $(BINARY_PATH_RUNTIME_WIN) $(BINARY_PATH_SETTING_WIN) $(SETTINGFILE)
test:
$(GOTEST) -v ./...
clean:
$(GOCLEAN)
rm -f $(BINARY_PATH_RUNTIME)
rm -f $(BINARY_PATH_RUNTIME_WIN)
rm -f $(BINARY_PATH_SETTING)
rm -f $(BINARY_PATH_SETTING_WIN)
run:
$(GOBUILD) -o $(BINARY_PATH_RUNTIME) -v $(RUNTIMEDIR)
./$(BINARY_PATH_RUNTIME)
rungui:
$(GOBUILD) -o $(BINARY_PATH_SETTING) -v $(GUIDIR)
./$(BINARY_PATH_RUNTIME)
zip作成処理はGolangで
Makefile内にzip:
ターゲットを追加しています。今回は以下のような複数のファイルをzip化して配布する必要がありました。
- service本体
- 設定GUIツール
- 初期設定ファイル
Golangだったら割と簡単に書けるのではと思い、ググってみたところやはり同じようなことを書いている方が既にいらっしゃって、こちらの記事を参考に処理を作成しました。
6. READMEを英語で書く
再生される動画が英語ということと、Vuls開発者のkotakanbeさんがREADMEを英語で書いたほうがいいとおっしゃってたので、試しに英語で書いてみました3。詳しくは以下の記事をご参照ください。
感想
FyneはかなりクセがあるのでGUIほしいからと安直に採用するのは良くないなと思いました。Goはクロスコンパイルができるので他OSにも配布が楽かなと思っていたのですが、サードパーティー製のライブラリを使うときにはクロスコンパイルできるのか、想定した挙動になるのか、特殊なコンパイル方法があるのか等注意が必要かなと感じました(特に組み合わせて使う場合)。
-
というのは口実で、純粋にGolangの勉強がしたかっただけだったりする ↩
-
公開exampleのexample/logging/main.goをコピペして書いてたらいらないサービスが含まれてました。 ↩
-
慣れないことをやるのは辛い…。かなり時間かかってしまいました。 ↩