From c85f425e696546e5043009cc202fa36705c03e4c Mon Sep 17 00:00:00 2001 From: Tulir Asokan Date: Sun, 6 Aug 2023 22:19:17 +0300 Subject: [PATCH] tmp --- .editorconfig | 4 ++ bridgeconfig/bridgeconfig.go | 12 +++--- bridgeservice/bridgeservice.go | 64 ++++++++++++++++++++++++++++ bridgeservice/launchagent.tpl.plist | 23 ++++++++++ bridgeservice/systemd.tpl.service | 11 +++++ cmd/bbctl/config.go | 2 +- cmd/bbctl/install-service.go | 66 +++++++++++++++++++++++++++++ cmd/bbctl/main.go | 1 + cmd/bbctl/run.go | 58 ++++++++++++++++--------- 9 files changed, 215 insertions(+), 26 deletions(-) create mode 100644 bridgeservice/bridgeservice.go create mode 100644 bridgeservice/launchagent.tpl.plist create mode 100644 bridgeservice/systemd.tpl.service create mode 100644 cmd/bbctl/install-service.go diff --git a/.editorconfig b/.editorconfig index b2e035f..41595af 100644 --- a/.editorconfig +++ b/.editorconfig @@ -11,5 +11,9 @@ insert_final_newline = true [*.yaml] indent_style = space +[*.plist] +indent_style = space +indent_size = 2 + [{.pre-commit-config.yaml,.github/workflows/*.yaml}] indent_size = 2 diff --git a/bridgeconfig/bridgeconfig.go b/bridgeconfig/bridgeconfig.go index 3485daf..7dfcce9 100644 --- a/bridgeconfig/bridgeconfig.go +++ b/bridgeconfig/bridgeconfig.go @@ -48,16 +48,16 @@ func init() { } } -func templateName(bridgeName string) string { - return fmt.Sprintf("%s.tpl.yaml", bridgeName) +func templateName(bridgeType string) string { + return fmt.Sprintf("%s.tpl.yaml", bridgeType) } -func IsSupported(bridgeName string) bool { - return tpl.Lookup(templateName(bridgeName)) != nil +func IsSupported(bridgeType string) bool { + return tpl.Lookup(templateName(bridgeType)) != nil } -func Generate(bridgeName string, params Params) (string, error) { +func Generate(bridgeType string, params Params) (string, error) { var out strings.Builder - err := tpl.ExecuteTemplate(&out, templateName(bridgeName), ¶ms) + err := tpl.ExecuteTemplate(&out, templateName(bridgeType), ¶ms) return out.String(), err } diff --git a/bridgeservice/bridgeservice.go b/bridgeservice/bridgeservice.go new file mode 100644 index 0000000..3b8088c --- /dev/null +++ b/bridgeservice/bridgeservice.go @@ -0,0 +1,64 @@ +package bridgeservice + +import ( + _ "embed" + "encoding/xml" + "fmt" + "strings" + "text/template" +) + +//go:embed systemd.tpl.service +var systemdServiceRaw string +var systemdService *template.Template + +//go:embed launchagent.tpl.plist +var launchAgentRaw string +var launchAgent *template.Template + +var tplFuncs = map[string]any{ + "xmlescape": func(input string) string { + var buf strings.Builder + _ = xml.EscapeText(&buf, []byte(input)) + return buf.String() + }, + "shelljoin": func(input []string) string { + var buf strings.Builder + for _, arg := range input { + // TODO use proper shell quoting? + _, _ = fmt.Fprintf(&buf, "%q ", arg) + } + return buf.String()[:buf.Len()-1] + }, + "join": strings.Join, +} + +func init() { + var err error + systemdService, err = template.New("systemdService").Funcs(tplFuncs).Parse(systemdServiceRaw) + if err != nil { + panic(fmt.Errorf("failed to parse systemd service template: %w", err)) + } + launchAgent, err = template.New("launchAgent").Funcs(tplFuncs).Parse(launchAgentRaw) + if err != nil { + panic(fmt.Errorf("failed to parse LaunchAgent template: %w", err)) + } +} + +type Params struct { + BridgeType string + Name string + WorkDir string +} + +func Systemd(params Params) (string, error) { + var out strings.Builder + err := systemdService.Execute(&out, ¶ms) + return out.String(), err +} + +func LaunchAgent(params Params) (string, error) { + var out strings.Builder + err := launchAgent.Execute(&out, ¶ms) + return out.String(), err +} diff --git a/bridgeservice/launchagent.tpl.plist b/bridgeservice/launchagent.tpl.plist new file mode 100644 index 0000000..ea25116 --- /dev/null +++ b/bridgeservice/launchagent.tpl.plist @@ -0,0 +1,23 @@ + + + + + Label + com.beeper.bbctl.{{ xmlescape .Name }} + ProgramArguments + + bbctl + run + {{ range .Args }} + {{ xmlescape . }} + {{ end }} + {{ xmlescape .Name }} + + KeepAlive + + RunAtLoad + + WorkingDirectory + {{ .WorkDir }} + + diff --git a/bridgeservice/systemd.tpl.service b/bridgeservice/systemd.tpl.service new file mode 100644 index 0000000..4256d83 --- /dev/null +++ b/bridgeservice/systemd.tpl.service @@ -0,0 +1,11 @@ +[Unit] +Description=Beeper {{ .BridgeType }} ({{ .Name }}) +After=network.target + +[Service] +Type=simple +WorkingDirectory={{ .WorkDir }} +ExecStart=bbctl run {{ shelljoin .Args }} "{{ .Name }}" + +[Install] +WantedBy=default.target diff --git a/cmd/bbctl/config.go b/cmd/bbctl/config.go index a8e99fe..56fae17 100644 --- a/cmd/bbctl/config.go +++ b/cmd/bbctl/config.go @@ -134,7 +134,7 @@ func doGenerateBridgeConfig(ctx *cli.Context, bridge string) (*generatedBridgeCo if err != nil { return nil, err } - if len(extraParams) != len(cliParams) { + if len(extraParams) != len(cliParams) && ctx.Command != installServiceCommand { formattedParams := make([]string, 0, len(extraParams)) for key, value := range extraParams { _, isCli := cliParams[key] diff --git a/cmd/bbctl/install-service.go b/cmd/bbctl/install-service.go new file mode 100644 index 0000000..c4657a1 --- /dev/null +++ b/cmd/bbctl/install-service.go @@ -0,0 +1,66 @@ +package main + +import ( + "os" + "runtime" + + "github.com/urfave/cli/v2" +) + +var installServiceCommand = &cli.Command{ + Name: "install-service", + Usage: "Install a system service file to run an official Beeper bridge", + ArgsUsage: "BRIDGE", + Before: RequiresAuth, + Flags: []cli.Flag{ + &cli.StringFlag{ + Name: "type", + Aliases: []string{"t"}, + EnvVars: []string{"BEEPER_BRIDGE_TYPE"}, + Usage: "The type of bridge to run.", + }, + &cli.StringSliceFlag{ + Name: "param", + Aliases: []string{"p"}, + Usage: "Set a bridge-specific config generation option. Can be specified multiple times for different keys. Format: key=value", + }, + &cli.BoolFlag{ + Name: "no-update", + Aliases: []string{"n"}, + Usage: "Don't update the bridge even if it is out of date.", + EnvVars: []string{"BEEPER_BRIDGE_NO_UPDATE"}, + }, + &cli.BoolFlag{ + Name: "force", + Aliases: []string{"f"}, + Usage: "Force register a bridge without the sh- prefix (dangerous).", + Hidden: true, + }, + }, + Action: installService, +} + +func isSystemd() bool { + stat, err := os.Stat("/run/systemd/system") + return err == nil && stat.IsDir() +} + +func installService(ctx *cli.Context) error { + if ctx.NArg() == 0 { + return UserError{"You must specify a bridge to install"} + } else if ctx.NArg() > 1 { + return UserError{"Too many arguments specified (flags must come before arguments)"} + } + bridgeName := ctx.Args().Get(0) + installed, err := doInstallBridge(ctx, bridgeName, true) + if err != nil { + return err + } + switch { + case runtime.GOOS == "darwin": + case isSystemd(): + default: + return UserError{"No supported init systems found"} + } + return nil +} diff --git a/cmd/bbctl/main.go b/cmd/bbctl/main.go index 31ae790..aaaab1c 100644 --- a/cmd/bbctl/main.go +++ b/cmd/bbctl/main.go @@ -155,6 +155,7 @@ var app = &cli.App{ whoamiCommand, configCommand, runCommand, + installServiceCommand, }, } diff --git a/cmd/bbctl/run.go b/cmd/bbctl/run.go index a59e34c..d312a03 100644 --- a/cmd/bbctl/run.go +++ b/cmd/bbctl/run.go @@ -132,67 +132,87 @@ func makeCmd(ctx context.Context, pwd, path string, args ...string) *exec.Cmd { return cmd } -func runBridge(ctx *cli.Context) error { - if ctx.NArg() == 0 { - return UserError{"You must specify a bridge to run"} - } else if ctx.NArg() > 1 { - return UserError{"Too many arguments specified (flags must come before arguments)"} - } - bridgeName := ctx.Args().Get(0) +type installedBridge struct { + Dir string + Cmd string + Args []string + Cfg *generatedBridgeConfig +} +func doInstallBridge(ctx *cli.Context, bridgeName string, installBinary bool) (*installedBridge, error) { cfg, err := doGenerateBridgeConfig(ctx, bridgeName) if err != nil { - return err + return nil, err } dataDir := GetEnvConfig(ctx).BridgeDataDir bridgeDir := filepath.Join(dataDir, bridgeName) err = os.MkdirAll(bridgeDir, 0700) if err != nil { - return err + return nil, err } err = os.WriteFile(filepath.Join(bridgeDir, "config.yaml"), []byte(cfg.Config), 0600) if err != nil { - return fmt.Errorf("failed to save config: %w", err) + return nil, fmt.Errorf("failed to save config: %w", err) } - overrideBridgeCmd := ctx.String("custom-startup-command") var bridgeCmd string var bridgeArgs []string switch cfg.BridgeType { case "imessage", "whatsapp", "discord", "slack", "gmessages": bridgeCmd = filepath.Join(dataDir, "binaries", fmt.Sprintf("mautrix-%s", cfg.BridgeType)) - if overrideBridgeCmd == "" { + if installBinary { err = updateGoBridge(ctx.Context, bridgeCmd, cfg.BridgeType, ctx.Bool("no-update")) if err != nil { - return fmt.Errorf("failed to update bridge: %w", err) + return nil, fmt.Errorf("failed to update bridge: %w", err) } } case "heisenbridge": - if overrideBridgeCmd == "" { + if installBinary { err = setupPythonVenv(ctx.Context, bridgeDir, cfg.BridgeType) if err != nil { - return fmt.Errorf("failed to update bridge: %w", err) + return nil, fmt.Errorf("failed to update bridge: %w", err) } } heisenHomeserverURL := strings.Replace(cfg.HomeserverURL, "https://", "wss://", 1) bridgeCmd = filepath.Join(bridgeDir, "venv", "bin", "python3") bridgeArgs = []string{"-m", "heisenbridge", "-c", "config.yaml", "-o", cfg.YourUserID.String(), heisenHomeserverURL} } + return &installedBridge{ + Dir: bridgeDir, + Cmd: bridgeCmd, + Args: bridgeArgs, + Cfg: cfg, + }, nil +} + +func runBridge(ctx *cli.Context) error { + if ctx.NArg() == 0 { + return UserError{"You must specify a bridge to run"} + } else if ctx.NArg() > 1 { + return UserError{"Too many arguments specified (flags must come before arguments)"} + } + bridgeName := ctx.Args().Get(0) + overrideBridgeCmd := ctx.String("custom-startup-command") + + installed, err := doInstallBridge(ctx, bridgeName, overrideBridgeCmd == "") + if err != nil { + return err + } if overrideBridgeCmd != "" { - bridgeCmd = overrideBridgeCmd + installed.Cmd = overrideBridgeCmd } - cmd := makeCmd(ctx.Context, bridgeDir, bridgeCmd, bridgeArgs...) - log.Printf("Starting [cyan]%s[reset]", cfg.BridgeType) + cmd := makeCmd(ctx.Context, installed.Dir, installed.Cmd, installed.Args...) + log.Printf("Starting [cyan]%s[reset]", installed.Cfg.BridgeType) c := make(chan os.Signal, 1) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c - log.Printf("Shutting down [cyan]%s[reset]", cfg.BridgeType) + log.Printf("Shutting down [cyan]%s[reset]", installed.Cfg.BridgeType) proc := cmd.Process if proc != nil { err := proc.Signal(syscall.SIGTERM)