1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
package main

import (
	"flag"
	"fmt"
	"os"
	"os/exec"
	"os/signal"
	"syscall"
)

// =============================================================================
// hosty run
// =============================================================================

func cmdRun(args []string) error {
	fs := flag.NewFlagSet("run", flag.ExitOnError)
	fs.Usage = func() {
		fmt.Print(`Usage: hosty run [-file FILE.hosty] [-name NAME] [-port PORT] [-state-dir DIR] [-ignore-version-check]

Start a hosty service, tail its logs to stdout, and clean up on exit (SIGINT/SIGTERM).
-file is optional if a data.hosty already exists for the given -name.
data.hosty and the environment database entry are preserved on exit; rootfs, credentials, and the unit file are removed.
If -port is omitted, a free port is chosen automatically.

Options:
`)
		fs.PrintDefaults()
	}
	file := fs.String("file", "", "Path to .hosty file (optional if -name app is already installed)")
	name := fs.String("name", "", "App name (overrides name in -file)")
	port := fs.String("port", "", "Port to pass to the service via HOSTY_PORT (default: free port)")
	stateDir := fs.String("state-dir", "", "State directory (default: $XDG_STATE_HOME/hosty)")
	ignoreVersionCheck := fs.Bool("ignore-version-check", false, "Allow installing an older version over a newer one")

	if err := fs.Parse(args); err != nil {
		return err
	}

	rs, err := setup(setupConfig{
		hostyFile:          *file,
		name:               *name,
		port:               *port,
		stateDir:           *stateDir,
		ignoreVersionCheck: *ignoreVersionCheck,
	})
	if err != nil {
		fs.Usage()
		return err
	}

	// Enable and start the fs companion service first, then the app service.
	if err := runCmd("systemctl", "--user", "enable", rs.name+"-fs.service"); err != nil {
		return fmt.Errorf("enabling fs service: %w", err)
	}
	if err := runCmd("systemctl", "--user", "restart", rs.name+"-fs.service"); err != nil {
		return fmt.Errorf("starting fs service: %w", err)
	}
	// Enable and restart so any updated ExecStart takes effect.
	if err := runCmd("systemctl", "--user", "enable", rs.name+".service"); err != nil {
		return fmt.Errorf("enabling service: %w", err)
	}
	if err := runCmd("systemctl", "--user", "restart", rs.name+".service"); err != nil {
		return fmt.Errorf("starting service: %w", err)
	}
	fmt.Fprintf(os.Stderr, "hosty: %s started on %s\n", rs.name, termLink("http://localhost:"+rs.port, "http://localhost:"+rs.port))

	// Tail logs — use _SYSTEMD_USER_UNIT match instead of -u so that systemd's
	// own start/stop messages are excluded; only the service process output shows.
	jctl := exec.Command("journalctl", "--user",
		"_SYSTEMD_USER_UNIT="+rs.name+".service",
		"-f", "-n", "50")
	jctl.Stdout = os.Stdout
	jctl.Stderr = os.Stderr
	if err := jctl.Start(); err != nil {
		cleanupRuntime(rs.name, rs.rootfs, rs.fsDir, rs.credsDir)
		return fmt.Errorf("starting journalctl: %w", err)
	}

	// Handle signals.
	sigs := make(chan os.Signal, 1)
	signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

	done := make(chan error, 1)
	go func() { done <- jctl.Wait() }()

	select {
	case <-sigs:
		fmt.Fprintf(os.Stderr, "\nhosty: shutting down\n")
		jctl.Process.Kill()
	case err := <-done:
		if err != nil {
			fmt.Fprintf(os.Stderr, "hosty: journalctl exited: %v\n", err)
		}
	}

	cleanupRuntime(rs.name, rs.rootfs, rs.fsDir, rs.credsDir)
	return nil
}