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
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
|
package main
import (
"flag"
"fmt"
"os"
"path/filepath"
"strings"
)
// =============================================================================
// hosty stop
// =============================================================================
func cmdStop(args []string) error {
fset := flag.NewFlagSet("stop", flag.ExitOnError)
fset.Usage = func() {
fmt.Print(`Usage: hosty stop -name NAME [-state-dir DIR] [-remove-data] [-system]
Stop and uninstall a hosty service.
Without -system: stops the --user unit. With -system: detaches the root portable
service and removes its host units. Use the same -system as 'hosty start'.
Preserves data.hosty and the app's entry in the environment database by default
(app data, secrets and port assignment).
-remove-data also deletes the app data and forgets its secrets and port (irreversible).
Options:
`)
fset.PrintDefaults()
}
name := fset.String("name", "", "App name (required)")
stateDir := fset.String("state-dir", "", "State directory (default: $XDG_STATE_HOME/hosty, or /var/lib/hosty with -system)")
removeData := fset.Bool("remove-data", false, "Also delete data.hosty and all app state (irreversible)")
system := fset.Bool("system", false, "Detach a root portable service instead of a --user unit")
if err := fset.Parse(args); err != nil {
return err
}
if *name == "" {
fset.Usage()
return fmt.Errorf("-name is required")
}
sd, err := resolveStateDir(*stateDir, *system)
if err != nil {
return err
}
appDir := filepath.Join(sd, *name)
fsDir := filepath.Join(appDir, "fs")
credsDir := filepath.Join(appDir, "credentials")
if *system {
imageDir := filepath.Join(sd, "images", *name)
// System mode mounts FUSE under /run (see systemFSDir), not the state dir.
cleanupSystem(*name, imageDir, systemFSDir(*name), credsDir)
} else {
rootfs := filepath.Join(appDir, "rootfs")
cleanupRuntime(*name, rootfs, fsDir, credsDir)
}
if *removeData {
if err := removeAppData(appDir); err != nil {
return fmt.Errorf("removing app data: %w", err)
}
// The app's secrets and port claim live in the environment database
// now, not in the directory just deleted. Without this they would
// survive -remove-data — which used to remove them by removing the
// file they were in, so forgetting it here is what preserves the
// existing meaning of the flag rather than changing it.
//
// ON DELETE CASCADE takes the secrets with the row; the port is freed
// by the same delete.
envDB, err := openEnvDB(sd)
if err != nil {
return err
}
defer envDB.Close()
if err := appForget(envDB, *name); err != nil {
return err
}
fmt.Fprintf(os.Stderr, "hosty: removed %s from %s (secrets and port assignment)\n", *name, envDBName)
}
return nil
}
// removeAppData deletes an app's state directory, following the StateDirectory=
// indirection systemd introduces in system mode.
//
// With DynamicUser, StateDirectory= puts the real directory at
// /var/lib/private/<...> and leaves only a symlink at the public path. Plain
// RemoveAll on that path therefore unlinks the symlink and silently leaves all
// app data (including secrets) behind — where a later fresh install then trips
// over it. Resolve the link first and remove both ends.
func removeAppData(appDir string) error {
target, err := filepath.EvalSymlinks(appDir)
if err != nil {
if os.IsNotExist(err) {
return nil
}
// Unresolvable (e.g. dangling link): fall back to removing the path itself.
target = appDir
}
if target != appDir {
// Only follow the link if it stays inside /var/lib, so a hand-edited or
// hostile symlink cannot redirect a recursive delete somewhere else.
if !strings.HasPrefix(target, "/var/lib/") {
return fmt.Errorf("refusing to remove %s: it resolves outside /var/lib (%s)", appDir, target)
}
fmt.Fprintf(os.Stderr, "hosty: removing app data at %s (via %s)\n", target, appDir)
if err := os.RemoveAll(target); err != nil {
return err
}
return os.Remove(appDir)
}
fmt.Fprintf(os.Stderr, "hosty: removing app data at %s\n", appDir)
return os.RemoveAll(appDir)
}
|