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
}
|