package main import ( "context" "embed" "fmt" corelog "log" "log/slog" _ "net/http/pprof" "os" "runtime/debug" "git.sr.ht/~ashkeel/strimertul/log" "git.sr.ht/~ashkeel/strimertul/utils" "github.com/apenwarr/fixconsole" "github.com/urfave/cli/v2" "github.com/wailsapp/wails/v2" "github.com/wailsapp/wails/v2/pkg/options" "github.com/wailsapp/wails/v2/pkg/options/assetserver" "github.com/wailsapp/wails/v2/pkg/runtime" ) const devVersionMarker = "v0.0.0-UNKNOWN" var appVersion = devVersionMarker const ( crashReportURL = "https://crash.strimertul.stream/upload" ) //go:embed frontend/dist var frontend embed.FS func main() { if err := fixconsole.FixConsoleIfNeeded(); err != nil { corelog.Fatal(err) } var panicLog *os.File app := &cli.App{ Name: "strimertul", Usage: "the small broadcasting suite for Twitch", Version: appVersion, Action: cliMain, Flags: []cli.Flag{ &cli.StringFlag{Name: "log-level", Usage: "logging level (debug,info,warn,error)", Value: "info"}, &cli.StringFlag{Name: "driver", Usage: "specify database driver", Value: "auto"}, &cli.StringFlag{Name: "database-dir", Aliases: []string{"db-dir"}, Usage: "specify database directory", Value: "data"}, &cli.StringFlag{Name: "backup-dir", Aliases: []string{"b-dir"}, Usage: "specify backup directory", Value: "backups"}, &cli.IntFlag{Name: "backup-interval", Aliases: []string{"b-i"}, Usage: "specify backup interval (in minutes, 0 to disable)", Value: 60}, &cli.IntFlag{Name: "max-backups", Aliases: []string{"b-max"}, Usage: "maximum number of backups to keep, older ones will be deleted, set to 0 to keep all", Value: 20}, }, Commands: []*cli.Command{ { Name: "import", Usage: "import database from JSON file", ArgsUsage: "[-f input.json]", Flags: []cli.Flag{ &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "file to open", DefaultText: "STDIN"}, }, Action: cliImport, }, { Name: "export", Usage: "export database as JSON file", ArgsUsage: "[-f output.json]", Flags: []cli.Flag{ &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "file to save to", DefaultText: "STDOUT"}, }, Action: cliExport, }, { Name: "restore", Usage: "restore database from backup", ArgsUsage: "[-f backup.db]", Flags: []cli.Flag{ &cli.StringFlag{Name: "file", Aliases: []string{"f"}, Usage: "backup to open", DefaultText: "STDOUT"}, }, Action: cliRestore, }, }, Before: func(ctx *cli.Context) error { // Initialize logger with global flags level := slog.LevelInfo if err := level.UnmarshalText([]byte(ctx.String("log-level"))); err != nil { return cli.Exit(fmt.Sprintf("Invalid log level: %s", err), 1) } log.Init(level) // Create file for panics var err error panicLog, err = os.OpenFile(log.PanicFilename, os.O_APPEND|os.O_WRONLY|os.O_CREATE, 0o666) if err != nil { slog.Warn("Could not create panic log", log.Error(err)) } else { utils.RedirectStderr(panicLog) } // For development builds, force crash dumps if isDev() { debug.SetTraceback("crash") } return nil }, After: func(_ *cli.Context) error { if panicLog != nil { utils.Close(panicLog) } return nil }, } if err := app.Run(os.Args); err != nil { corelog.Fatal(err) } } func cliMain(ctx *cli.Context) error { // Create an instance of the app structure app := NewApp(ctx) // Create application with options err := wails.Run(&options.App{ Title: "strimertul", Width: 1024, Height: 768, MinWidth: 480, MinHeight: 300, AssetServer: &assetserver.Options{ Assets: frontend, }, SingleInstanceLock: &options.SingleInstanceLock{ UniqueId: "d1272ae3-765a-4768-97cb-3203b788e7c5", OnSecondInstanceLaunch: app.onSecondInstanceLaunch, }, EnableDefaultContextMenu: true, BackgroundColour: &options.RGBA{R: 27, G: 38, B: 54, A: 1}, OnStartup: app.startup, OnShutdown: app.stop, OnBeforeClose: func(ctx context.Context) (prevent bool) { dialog, err := runtime.MessageDialog(ctx, runtime.MessageDialogOptions{ Type: runtime.QuestionDialog, Title: "Quit?", Message: "Are you sure you want to quit?", }) if err != nil { return false } return dialog != "Yes" }, Bind: []any{ app, }, }) if err != nil { return cli.Exit(fmt.Errorf("%s: %w", "App exited unexpectedly", err), 1) } return nil } func warnOnError(err error, text string, fields ...any) { if err != nil { fields = append(fields, log.Error(err)) slog.Warn(text, fields...) } } func fatalError(err error, text string) error { return cli.Exit(fmt.Errorf("%s: %w", text, err), 1) } // isDev checks if the running code is a development version func isDev() bool { return appVersion == devVersionMarker }