(Golang / Wails) 作成したアプリが開発環境以外で動作しない

経緯

自分の開発環境で満足の行く動きまで完成させた Wails アプリが、開発環境以外で動かない、という現象に遭遇しました。

現象としては、ダブルクリックで作成したアプリの exe を叩くと、一瞬タスクバーにアイコンが表示されるかされないかくらいのタイミングで即座に落ちてしまう、という状態です。

内部的にログ出力等を仕込んでいるのですが、一切出力がないことから、本当にアプリの起動直後の処理くらいのタイミングで落ちているものと推測されます。

が、一切ログがないのでヒントがなく、「折角作成したのに……。」と途方に暮れていたのですが、途方に暮れるだけでは解決しないのでしぶしぶ原因切り分けを行いました。

調査

まず当初プロジェクトを切ったときと同じように Wails のプロジェクト初期設定を実行します。

> wails init -n Wailing-Wall -t react-ts 
Wails CLI v2.8.2


# Initialising Project 'Wailing-Wall'
Project Name      | Wailing-Wall
Project Directory | PATH\TO\PROJECT\Wailing-Wall
Template          | React + Vite (Typescript)
Template Source   | https://wails.io

Initialised project 'Wailing-Wall' in 17.775s.

 ♥   If Wails is useful to you or your company, please consider sponsoring the project:
https://github.com/sponsors/leaanthony

ディレクトリに移動して作業開始。

まずそのままビルドします。

>wails build -clean -webview2 embed -debug

念のためクリーンビルドで Webview 同梱、デバッグモード指定してみます。

……起動しました。ということは、 Wails 自体は問題なさそうです。 exe は正しい形でビルドされているし、ビューワ側(ブラウザ、レンダリングエンジン)も問題ないようです。

となると、内部の処理で本当にごくごく最初の部分でコケている、ということか……。

これもどこから切り分けるか、と思ったのですが仕方ないのでコードの最初の方から徐々に機能を追加して、コケたところで原因がある、というように段階的に見ていくことにしました。

結果

すると、思いの外早く結果が出ました。というよりも、本当に最初の部分でコケていました。

main.go (ほぼ初期コード)

package main

import (
    "embed"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        Title:  "Wailing Wall",
        Width:  640,
        Height: 480,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 255},
        OnStartup:        app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

初期生成されたコードはほぼこのような形 (ウィンドウサイズやタイトルのみ編集済み) でした。

main.gp (コケたコード)

package main

import (
    "embed"
    "time"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"
)

//go:embed all:frontend/dist
var assets embed.FS

func main() {
    _, tzerr := time.LoadLocation("Asia/Tokyo")
    if tzerr != nil {
        panic(tzerr)
    }

    // Create an instance of the app structure
    app := NewApp()

    // Create application with options
    err := wails.Run(&options.App{
        Title:  "Wailing Wall",
        Width:  640,
        Height: 480,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 255},
        OnStartup:        app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err.Error())
    }
}

コケたコード(main.go)はこちら。

初期コードにタイムゾーンの指定を加えただけです。ところが、これだけでコケるようになりました……。

https://crieit.net/posts/Go-time-LoadLocation に書いてあるとおり、 time.LoadLocation を下手に呼び出すと環境によっては unknown time zone エラーが起こるため 次のように time.FixedZone で Location を生成します。
(筆者註: リンク先が記事が消えているのでリンクを潰しています)

Golang での時刻の扱い方を整理する | Melody

調べてみたところ、ものすごく気になる情報が見付かりました。

日本国内でしか使わないと考えていたので軽率にタイムゾーンを指定していましたが、それがまずかったらしい……という推測に至りました。ウソでしょ……。

main.go (修正 + α)

package main

import (
    "embed"
    "errors"
    "fmt"
    "os"
    "time"

    "github.com/wailsapp/wails/v2"
    "github.com/wailsapp/wails/v2/pkg/options"
    "github.com/wailsapp/wails/v2/pkg/options/assetserver"

    "go.uber.org/zap"
    "go.uber.org/zap/zapcore"
)

//go:embed all:frontend/dist
var assets embed.FS

var ErrLogPath = errors.New("ログ出力先パスが存在しません。")

// ファイル存在チェック
func FileExists(filename string) bool {
    _, err := os.Stat(filename)
    return err == nil
}

// logger, (dev用 は console 形式で出力 / prod用 は json 形式で出力 のコードをカスタマイズ: https://zenn.dev/yukihaga/scraps/19b101e0faf857 + https://christina04.hatenablog.com/entry/golang-zap-tips)
func GetLogger(tz *time.Location) *zap.Logger {

    // 使用変数定義
    var enc zapcore.Encoder
    var infoOutput zapcore.WriteSyncer
    var errorOutput zapcore.WriteSyncer

    // ログファイルパス
    logPath := "./logs/"

    // タイムゾーン指定
    t := time.Now().In(tz)

    // ログ出力先存在チェック
    if !FileExists(logPath) {
        panic(ErrLogPath)
    }
    // ログレベルに応じて出力先指定
    infoFile, _ := os.OpenFile(fmt.Sprintf("%s%04d-%02d-%02d_info.log", logPath, t.Year(), t.Month(), t.Day()), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
    errorFile, _ := os.OpenFile(fmt.Sprintf("%s%04d-%02d-%02d_error.log", logPath, t.Year(), t.Month(), t.Day()), os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)

    highPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl >= zapcore.ErrorLevel
    })
    lowPriority := zap.LevelEnablerFunc(func(lvl zapcore.Level) bool {
        return lvl < zapcore.ErrorLevel
    })

    // json 形式で出力
    enc = zapcore.NewConsoleEncoder(zap.NewProductionEncoderConfig())
    infoOutput = zapcore.AddSync(infoFile)
    errorOutput = zapcore.NewMultiWriteSyncer(os.Stderr, errorFile)

    // オブジェクト生成
    core := zapcore.NewTee(
        zapcore.NewCore(enc, errorOutput, highPriority),
        zapcore.NewCore(enc, infoOutput, lowPriority),
    )

    logger := zap.New(core)

    return logger
}

func main() {
    jst := time.FixedZone("Asia/Tokyo", 9*60*60)

    // Create an instance of the app structure
    app := NewApp()

    // zap logger
    logger := GetLogger(jst)

    // テストログ出力
    logger.Info(fmt.Sprintf("ロガーのインスタンス化に成功しました。 / タイムゾーン: %s", jst.String()), zap.String("datetime", time.Now().In(jst).Format(time.RFC3339)), zap.Time("now", time.Now().In(jst)))

    // Create application with options
    err := wails.Run(&options.App{
        Title:  "Wailing Wall",
        Width:  640,
        Height: 480,
        AssetServer: &assetserver.Options{
            Assets: assets,
        },
        BackgroundColour: &options.RGBA{R: 0, G: 0, B: 0, A: 255},
        OnStartup:        app.startup,
        Bind: []interface{}{
            app,
        },
    })

    if err != nil {
        println("Error:", err.Error())
        logger.Error("アプリケーション実行時にエラーが発生しました。", zap.String("datetime", time.Now().In(jst).Format(time.RFC3339)), zap.Time("now", time.Now().In(jst)))
    }
}

ということで上述の参考記事の通りに修正したコードがこちら。 jst, tzerr := time.LoadLocation("Asia/Tokyo") としていたところを jst := time.FixedZone("Asia/Tokyo", 9*60*60) に変更しました。

このままでは動作が分かりづらいのでロガーも実装してテストログを吐かせるようにして見ます。ファイル出力なのでサブディレクトリ logs を追加した上で、ですが。

そしてビルドしてみたところ……動きました。

上述の調査と検証から、原因は本当にタイムゾーン指定だったようです。まさかそんなところでコケるとは…… (そしてコードの実行順的にまだロガーの定義ができていない段階なのでエラー発生時は panic() で潰していたため、冒頭に述べたように「起動後、即座に落ちる」という現象に繋がっていた模様)。

参考

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。