diff options
Diffstat (limited to 'cmd')
| -rw-r--r-- | cmd/app/app.go | 26 | ||||
| -rw-r--r-- | cmd/app/app_merged.go | 15 | ||||
| -rw-r--r-- | cmd/app/app_sep.go | 15 | ||||
| -rw-r--r-- | cmd/app/app_test.go | 10 | ||||
| -rw-r--r-- | cmd/app/doc.go | 362 | ||||
| -rw-r--r-- | cmd/app/lock.go | 22 | ||||
| -rw-r--r-- | cmd/app/main.go | 137 | ||||
| -rw-r--r-- | cmd/dist/VERSION | 2 | ||||
| -rw-r--r-- | cmd/hakurei/command.go | 6 | ||||
| -rw-r--r-- | cmd/hakurei/parse.go | 11 | ||||
| -rw-r--r-- | cmd/hakurei/parse_test.go | 2 | ||||
| -rw-r--r-- | cmd/hakurei/print.go | 13 | ||||
| -rw-r--r-- | cmd/hakurei/print_test.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/cache.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/cache_test.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/daemon.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/daemon_test.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/info.go | 6 | ||||
| -rw-r--r-- | cmd/mbf/info_test.go | 2 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/api.go (renamed from cmd/mbf/internal/pkgserver/api.go) | 4 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/api_test.go (renamed from cmd/mbf/internal/pkgserver/api_test.go) | 2 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/index.go (renamed from cmd/mbf/internal/pkgserver/index.go) | 4 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/index_test.go (renamed from cmd/mbf/internal/pkgserver/index_test.go) | 2 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/search.go (renamed from cmd/mbf/internal/pkgserver/search.go) | 2 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/index.html (renamed from cmd/mbf/internal/pkgserver/ui/index.html) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/index.ts (renamed from cmd/mbf/internal/pkgserver/ui/index.ts) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/style.css (renamed from cmd/mbf/internal/pkgserver/ui/style.css) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/tsconfig.json (renamed from cmd/mbf/internal/pkgserver/ui/tsconfig.json) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/ui.go (renamed from cmd/mbf/internal/pkgserver/ui/ui.go) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/ui_full.go (renamed from cmd/mbf/internal/pkgserver/ui/ui_full.go) | 0 | ||||
| -rw-r--r-- | cmd/mbf/internal/pkgsite/ui/ui_stub.go (renamed from cmd/mbf/internal/pkgserver/ui/ui_stub.go) | 0 | ||||
| -rw-r--r-- | cmd/mbf/main.go | 8 |
32 files changed, 592 insertions, 69 deletions
diff --git a/cmd/app/app.go b/cmd/app/app.go index 9679413f..ca809334 100644 --- a/cmd/app/app.go +++ b/cmd/app/app.go @@ -14,6 +14,24 @@ import ( "hakurei.app/hst" ) +// base is the root of the persistent state directory. +type base check.Absolute + +// append calls filepath.Join with b as the first element. +func (b *base) append(elem ...string) *check.Absolute { + return (*check.Absolute)(b).Append(elem...) +} + +// initial returns the pathname of the bottom layer. +func (b *base) initial() *check.Absolute { + return b.append("initial") +} + +// state returns the pathname of the home directory for id. +func (b *base) state(id string) *check.Absolute { + return b.append("state", id) +} + // parsePair parses a NUL-delimited quoted paths pair. func parsePair(s string) (source, target *check.Absolute, err error) { var p string @@ -35,7 +53,7 @@ func parsePair(s string) (source, target *check.Absolute, err error) { // corresponding [hst.Config]. func parse( id string, - base *check.Absolute, + base *base, r io.Reader, templateP *string, ) (*hst.Config, error) { @@ -44,7 +62,7 @@ func parse( root := hst.FSOverlay{ Target: fhs.AbsRoot, - Lower: []*check.Absolute{base.Append("initial")}, + Lower: []*check.Absolute{base.initial()}, } c := hst.Config{ ID: id, @@ -65,7 +83,7 @@ func parse( {FilesystemConfig: &root}, {FilesystemConfig: &hst.FSBind{ Target: home, - Source: base.Append("state", id), + Source: base.state(id), Write: true, Ensure: true, }}, @@ -110,7 +128,7 @@ func parse( *templateP = template } c.Identity = v - root.Upper = base.Append("template", template) + root.Upper = base.template(template) } if err := scanOnce(); err != nil { diff --git a/cmd/app/app_merged.go b/cmd/app/app_merged.go new file mode 100644 index 00000000..e0a31f94 --- /dev/null +++ b/cmd/app/app_merged.go @@ -0,0 +1,15 @@ +//go:build merge + +package main + +import "hakurei.app/check" + +// template returns the pathname of an upperdir. +func (b *base) template(template string) *check.Absolute { + return b.append("template", template, "upper") +} + +// work returns the pathname of a workdir. +func (b *base) work(template string) *check.Absolute { + return b.append("template", template, "work") +} diff --git a/cmd/app/app_sep.go b/cmd/app/app_sep.go new file mode 100644 index 00000000..9ec98e41 --- /dev/null +++ b/cmd/app/app_sep.go @@ -0,0 +1,15 @@ +//go:build !merge + +package main + +import "hakurei.app/check" + +// template returns the pathname of an upperdir. +func (b *base) template(template string) *check.Absolute { + return b.append("template", template) +} + +// work returns the pathname of a workdir. +func (b *base) work(template string) *check.Absolute { + return b.append("work", template) +} diff --git a/cmd/app/app_test.go b/cmd/app/app_test.go index 1aab4759..90c0068e 100644 --- a/cmd/app/app_test.go +++ b/cmd/app/app_test.go @@ -13,7 +13,7 @@ import ( func TestParse(t *testing.T) { t.Parallel() - base := fhs.AbsProc.Append("nonexistent") + b := (*base)(fhs.AbsProc.Append("nonexistent")) testCases := []struct { name string data string @@ -74,13 +74,13 @@ talk com.canonical.Unity {FilesystemConfig: &hst.FSOverlay{ Target: fhs.AbsRoot, Lower: []*check.Absolute{ - base.Append("initial"), + b.initial(), }, - Upper: base.Append("template", "nonfree"), + Upper: b.template("nonfree"), }}, {FilesystemConfig: &hst.FSBind{ Target: hst.AbsPrivateTmp.Append("home"), - Source: base.Append("state", "com.discordapp.Discord"), + Source: b.state("com.discordapp.Discord"), Write: true, Ensure: true, }}, @@ -128,7 +128,7 @@ talk com.canonical.Unity got, err := parse( tc.name, - base, + b, strings.NewReader(tc.data), nil, ) diff --git a/cmd/app/doc.go b/cmd/app/doc.go new file mode 100644 index 00000000..ea91315e --- /dev/null +++ b/cmd/app/doc.go @@ -0,0 +1,362 @@ +/* +The app program is a proof-of-concept frontend for cmd/hakurei. + +This program is not covered by the compatibility promise. The command line +interface and configuration syntax may change at any time. + +# Installation + +Compile cmd/app with these arguments: + + go build -trimpath \ + -ldflags='-s -w + -buildid= + -X hakurei.app/internal/info.hsuPath=/usr/local/bin/hsu' \ + ./cmd/app + +Replace /usr/local/bin/hsu with the hakurei installation's absolute hsu +pathname. + +# Sharing files + +Sharing files between apps is only possible with the permissionless shared +filesystem ([cmd/sharefs]), as apps generally do not share credentials. It can +be built without any special linker flags. Create a symlink at +/usr/bin/mount.fuse.sharefs pointing to the sharefs binary when mounting sharefs +via fstab: + + sharefs /sdcard fuse.sharefs rw,noexec,nosuid,nodev,noatime,allow_other,mkdir,source=/var/lib/sdcard,setuid=1023,setgid=1023 0 0 + +Replace /var/lib/sdcard with the location of the backing directory. User and +group id must be specified in numerical form, it must own the backing directory. +The mount point does not have to be /sdcard. + +# General setup + +The entire directory structure containing persistent app data must be created +manually for now, due to the different ownership requirements being impossible +to set up directly through cmd/hsu. + +The environment variable ROSA_APP_PATH should be set to the absolute pathname of +the persistent state directory. This document will use $ROSA_APP_PATH to refer +to the persistent state directory. The $ROSA_APP_PATH directory should be owned +by the user declared in /etc/hsurc and invoking cmd/app, and must be readable +and executable by all subordinate users. This can be done by making it +world-readable and executable, or by adding a group directive to the common +file. + +The $ROSA_APP_PATH/app directory contains app configuration files, named after +their reverse-DNS style application identifier string. It should be owned by +the user invoking cmd/app. + +The $ROSA_APP_PATH/initial directory contains the read-only base of every +app template. Everything within it, including the directory itself, should be +owned by the reserved user, its user and group id obtained by the command: + + app id + +This document assumes a [Void Linux rootfs tarball] is unpacked here. It is +possible to use other Linux distributions; Void linux is selected here because +its package manager works correctly out of the box in the hakurei container +environment. If the use case does not require glibc, [Alpine Linux] or +[Chimera Linux] might be more suitable. + +Once the tarball is unpacked at $ROSA_APP_PATH/initial, create a directory named +chronos in $ROSA_APP_PATH/initial/home, and directories block, bus, class, dev, +and devices in $ROSA_APP_PATH/initial/sys. Then, recursively change ownerships +of files and directories in $ROSA_APP_PATH/initial to the reserved user/group. + +The $ROSA_APP_PATH/lock directory contains lock files guarding entry into +mutable and app containers of every template. It must be owned by the user +invoking cmd/app. + +The $ROSA_APP_PATH/state directory contains app home directories, named after +their reverse-DNS style application identifier string. The directory itself +should be owned by the user invoking cmd/app; each directory inside must be +owned by the subordinate user/group id of its app, obtained by the command: + + app id "$APPID" + +where "$APPID" is the second component of the first line in the configuration +file of the corresponding app. It is considered good practice, but not required, +for these directory to have the permission bits set to 0700. + +The $ROSA_APP_PATH/template directory contains templates that apps are based +on, which are all derived from the initial layer. The directory itself +should be owned by the user invoking cmd/app, and each directory inside should +be owned by the reserved user/group. Their names are used in the first component +of the first line in the configuration file of each app, to specify that the +file is derived from that template. + +The $ROSA_APP_PATH/work directory contains overlay work directories for mutable +containers. It must contain one manually created, empty directory for each +template, named after the template directory itself, owned by the reserved +user/group. The directory itself and its contents should have the permission +bits set to 0700. + +# Snapshots and cloning + +To reduce disk space used by templates, [ZFS clones] or an equivalent can be +used. In such a setup, where each template occupies a different filesystem, the +"merge" build tag is required, and the work directory can be omitted during +directory creation. Instead, the individual template directories contain both +upper and work directories. + +# Configuring the base template + +This section describes basic setup required for the typical desktop use case. +Generally, multiple templates are created to mitigate the global nature of +conventional package managers. The setup described in this section applies to +all templates. For convenience, a "base" template should be created, containing +this setup, and all future templates should be copied from the base template. +This section assumes the template is named "base". + +Create an empty directory at $ROSA_APP_PATH/template/base, owned by the reserved +user, with permission bits set to 0755. Create its corresponding work directory +at $ROSA_APP_PATH/work/base, also owned by the reserved user, with permission +bits set to 0700. Once this is complete, enter its mutable container with the +command: + + app enter --shell=/bin/sh base + +This command overrides the container configuration to use the shell program at +/bin/sh. Normally, cmd/app uses zsh, which is not yet installed. + +Once in the container, populate /etc/resolv.conf with some well-known DNS +service, for example: + + nameserver 8.8.8.8 + nameserver 8.8.4.4 + +The mutable container does not bind the host /etc/resolv.conf. It is generally +recommended to populate it with a well-known service here to avoid trouble in +future template changes. A later section configures regular app containers to +use host configuration. + +After populating nameserver configuration, install zsh using the package manager +provided by the distribution unpacked earlier. On Void Linux, this is done by +the command: + + xbps-install zsh + +Wait for the package installation to complete, configure zsh if necessary, then +exit from the shell, and re-enter the container without overriding the login +shell: + + app enter base + +Some packages are generally required in the container for the typical desktop +use case: xdg-user-dirs to reconfigure "well known" user directories to point +to the inner sharefs mount point, mesa to interact with the GPU, dconf to +configure GTK, xdg-desktop-portal-gtk to have gtk talk to dconf. Fonts generally +need to be installed as well. Additionally, a text editor can be installed for +editing configuration files later on. On Void Linux, these packages are +installed by the command: + + xbps-install \ + vim \ + mesa \ + mesa-dri \ + mesa-intel-dri \ + mesa-vaapi \ + mesa-vulkan-intel \ + mesa-vulkan-radeon \ + mesa-vulkan-overlay-layer \ + dconf \ + xdg-user-dirs \ + xdg-desktop-portal-gtk \ + noto-fonts-ttf \ + noto-fonts-ttf-variable \ + noto-fonts-ttf-extra \ + noto-fonts-emoji \ + noto-fonts-cjk \ + noto-fonts-cjk-variable \ + noto-fonts-cjk-sans \ + noto-fonts-cjk-sans-variable \ + noto-fonts-cjk-serif \ + noto-fonts-cjk-serif-variable + +Add -32bit variants of mesa packages if multilib support is required. Adjust +the package selection based on use case. + +Edit /etc/xdg/user-dirs.defaults and point directories to the inner sharefs +mount point as required. The resulting file should look like this: + + # Default settings for user directories + # + # The values are relative pathnames from the home directory and + # will be translated on a per-path-element basis into the users locale + DESKTOP=../../sdcard/Desktop + DOWNLOAD=../../sdcard/Download + TEMPLATES=../../sdcard/Templates + PUBLICSHARE=../../sdcard/Public + DOCUMENTS=../../sdcard/Documents + MUSIC=../../sdcard/Music + PICTURES=../../sdcard/Pictures + VIDEOS=../../sdcard/Movies + PROJECTS=../../sdcard/Projects + # Another alternative is: + #MUSIC=Documents/Music + #PICTURES=Documents/Pictures + #VIDEOS=Documents/Videos + +If this is configured, it must be applied to the home directory of each app +individually. This can be done for all apps via the shell expression: + + app run | xargs -n 1 echo app run --command=xdg-user-dirs-update + +This must also be done for every newly created app. + +The /etc/dconf directory can be set up to provide defaults across all apps. To +do so, edit /etc/dconf/profile/user: + + user-db:user + system-db:local + system-db:site + system-db:distro + +Then, place files in /etc/dconf/db/local.d containing dconf configuration. For +example: + + [org/gnome/desktop/interface] + gtk-enable-primary-paste=true + color-scheme='prefer-dark' + gtk-theme='adw-gtk3-dark' + icon-theme='Papirus-Dark' + +Themes must be installed in the container. Change colour-scheme and themes +accordingly. Setting gtk-enable-primary-paste restores clipboard behaviour that +GNOME maintainers decided to break. + +After editing these configuration files, update dconf system databases: + + dconf update + +# Common configuration + +The optional $ROSA_APP_PATH/common file contains common configuration included +after the specific configuration of each app. Some bind mounts are required for +almost every graphical program, and many widely used libraries are configured +through the environment. For most setups, these directives are generally +required: + + ; for libudev + ro "/sys/block" + ro "/sys/bus" + ro "/sys/class" + ro "/sys/dev" + ro "/sys/devices" + + env EDITOR=vim + ; for apps to play nice with xdg-dbus-proxy + ro+ "/etc/machine-id" + ; template must have a /etc/resolv.conf file + ro+ "/etc/resolv.conf" + ; group name of the sharefs group configured on host + group media_rw + ; for sharefs + rw "/sdcard" + ; refer to /usr/share/zoneinfo + env TZ=Asia/Tokyo + + ; must be installed first + env XCURSOR_THEME=volantes_cursors + ; must be generated first if using glibc + env LANG=en_GB.UTF-8 + env LC_COLLATE=C + +# Installing an app + +An app is defined by a configuration file in $ROSA_APP_PATH/app, and a +persistent state directory in $ROSA_APP_PATH/state. Before creating an app, its +identity must be decided. Different apps should generally not share an identity. +The next unused identity can be found using the command: + + app next + +If the -v argument is added before the "next" command, cmd/app will additionally +show apps sharing the same identity. + +Once the identity is decided, the subordinate user/group id can be obtained by +the command: + + app id "$APPID" + +where "$APPID" is the identity of this app. A directory must be created under +$ROSA_APP_PATH/state, owned by this user and group id. Its permission bits +should be set to 0700. A configuration file with the same name, owned by the +user invoking cmd/app, must be placed in $ROSA_APP_PATH/app. Its contents are +described in the next section. The reverse-DNS style application identifier +string is submitted to the Wayland display server, and used as part of the +dbus preset if enabled, so the correct identifier must be obtained. If no such +identifier exist, use the domain of the app home page. + +# Configuring an app + +The configuration file starts with two structural directives, and the remaining +lines are freestanding directives applied in order. + +The first structural directive is a line containing two components separated by +the ':' byte. The first component is the template used by the app. the second +component is the decimal representation of the app identity. The second +structural directive is a shell expression passed to the shell serving as the +initial process of the app. + +After the structural directives, each line contains exactly one directive or +comment. Comment lines begin with a ';' byte: these lines are not interpreted by +cmd/app in any way. + +The following section documents currently available freestanding directives. + +# Directives + + interactive start initial process as an interactive shell + gpu expose GPU devices to the container + system_bus enable system bus in the dbus proxy + + wayland expose a Wayland pathname socket via security-context-v1 + x11 expose the X11 pathname socket + dbus enable the per-container xdg-dbus-proxy daemon + pipewire expose a pipewire pathname socket via SecurityContext + + multiarch unblock system calls required for multiarch to work on + multiarch-enabled targets (amd64, arm64) + devel unblock ptrace and friends + userns unblock userns creation and container setup syscalls + net enable network access + abstract enable access to external abstract unix sockets + tty unblock dangerous terminal I/O (faking input) + mapuid map the target user id to the user id of the user + invoking cmd/app in the container user namespace + device mount /dev/ from the init mount namespace as is in the + container mount namespace + + share_runtime share XDG_RUNTIME_DIR between containers under the same + identity + share_tmpdir share TMPDIR between containers under the same identity + + username <name> set username of the emulated user + hostname <name> set container hostname + env KEY=VALUE set an environment variable for the initial process + + ro "pathname" make a host path available to the container; the string + must be presented in Go string literal syntax, to + specify a different inner pathname, end the outer + pathname with NUL and specify the inner pathname after + the NUL byte + rw "pathname" like ro, but the resulting mount entry is made writable + ro+ "pathname" like ro, but is skipped if pathname does not exist + rw+ "pathname" like ro+, but the resulting mount entry is made writable + + own name add an own policy for the dbus proxy + own_system name like own, but for the system bus if enabled + talk name add a talk policy for the dbus proxy + talk_system name like talk, but for the system bus if enabled + +[cmd/sharefs]: https://pkg.go.dev/hakurei.app/cmd/sharefs +[Void Linux rootfs tarball]: https://voidlinux.org/download +[Alpine Linux]: https://alpinelinux.org +[Chimera Linux]: https://chimera-linux.org +[ZFS clones]: https://openzfs.github.io/openzfs-docs/man/v2.4/8/zfs-clone.8.html +*/ +package main diff --git a/cmd/app/lock.go b/cmd/app/lock.go index 5a22efba..2a419ea1 100644 --- a/cmd/app/lock.go +++ b/cmd/app/lock.go @@ -6,8 +6,8 @@ import ( "os" "strconv" "strings" + "syscall" - "hakurei.app/check" "hakurei.app/fhs" "hakurei.app/hst" "hakurei.app/internal/env" @@ -23,15 +23,15 @@ func (e MutationConflictError) Error() string { } // informTemplate guards intention of a template or its derivatives. -func informTemplate(base *check.Absolute, name string, mutable bool) (func() error, error) { - mu := lockedfile.MutexAt(base.Append("lock", name).String()) +func informTemplate(b *base, name string, mutable bool) (func() error, error) { + mu := lockedfile.MutexAt(b.append("lock", name).String()) if unlock, err := mu.Lock(); err != nil { return nil, err } else { defer unlock() } - marker := base.Append("lock", "."+name) + marker := b.append("lock", "."+name) if p, err := os.ReadFile(marker.String()); err == nil { if _, err = os.Stat(fhs.AbsProc.Append(string(p)).String()); err == nil { return nil, MutationConflictError(p) @@ -57,7 +57,7 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err var s hst.State for eh := range entries { s = hst.State{} - if _, err := eh.Load(&s); err != nil { + if _, err := eh.Load(&s, syscall.Kill); err != nil { return nil, err } @@ -71,8 +71,8 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err if !root.Target.Is(fhs.AbsRoot) || len(root.Lower) != 1 || - !root.Lower[0].Is(base.Append("initial")) || - !root.Upper.Is(base.Append("template", name)) || + !root.Lower[0].Is(b.initial()) || + !root.Upper.Is(b.template(name)) || root.Work != nil { continue } @@ -101,12 +101,12 @@ func informTemplate(base *check.Absolute, name string, mutable bool) (func() err } // acquireTemplate obtains exclusivity of a template. -func acquireTemplate(base *check.Absolute, name string) (remove func() error, err error) { - return informTemplate(base, name, true) +func acquireTemplate(b *base, name string) (remove func() error, err error) { + return informTemplate(b, name, true) } // enterTemplate checks against exclusivity of a template. -func enterTemplate(base *check.Absolute, name string) error { - _, err := informTemplate(base, name, false) +func enterTemplate(b *base, name string) error { + _, err := informTemplate(b, name, false) return err } diff --git a/cmd/app/main.go b/cmd/app/main.go index 00bceb57..9b51d601 100644 --- a/cmd/app/main.go +++ b/cmd/app/main.go @@ -1,24 +1,25 @@ -// The app program is a proof-of-concept frontend for cmd/hakurei. -// -// This program is not covered by the compatibility promise. The command line -// interface and configuration syntax may change at any time. package main import ( "context" "errors" + "fmt" "io" "log" "os" "os/exec" "os/signal" "path/filepath" + "slices" + "strconv" + "strings" "syscall" "hakurei.app/check" "hakurei.app/command" "hakurei.app/fhs" "hakurei.app/hst" + "hakurei.app/internal/outcome" "hakurei.app/message" ) @@ -36,7 +37,7 @@ func main() { flagBase string flagInsecure bool - base, template, initial *check.Absolute + b *base ) c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) { msg.SwapVerbose(flagVerbose) @@ -44,14 +45,15 @@ func main() { if flagBase == "" { flagBase = "state" } + + var a *check.Absolute if flagBase, err = filepath.Abs(flagBase); err != nil { return - } else if base, err = check.NewAbs(flagBase); err != nil { + } else if a, err = check.NewAbs(flagBase); err != nil { return } + b = (*base)(a) - template = base.Append("template") - initial = base.Append("initial") return }).Flag( &flagVerbose, @@ -76,7 +78,7 @@ func main() { "enter", "Enter mutable state template", func(args []string) error { if len(args) != 1 { - return list(template, true) + return list(b.append("template"), true) } config := hst.Config{ @@ -86,9 +88,9 @@ func main() { Filesystem: []hst.FilesystemConfigJSON{ {FilesystemConfig: &hst.FSOverlay{ Target: fhs.AbsRoot, - Lower: []*check.Absolute{initial}, - Upper: template.Append(args[0]), - Work: base.Append("work", args[0]), + Lower: []*check.Absolute{b.initial()}, + Upper: b.template(args[0]), + Work: b.work(args[0]), }}, {FilesystemConfig: &hst.FSEphemeral{ Target: fhs.AbsTmp, @@ -122,7 +124,7 @@ func main() { config.Container.Home = a } - remove, err := acquireTemplate(base, args[0]) + remove, err := acquireTemplate(b, args[0]) if err != nil { return err } @@ -148,19 +150,19 @@ func main() { "run", "Start the named application", func(args []string) error { if len(args) < 1 { - return list(base.Append("app"), false) + return list(b.append("app"), false) } var config *hst.Config var r io.Reader - f, err := os.Open(base.Append("app", args[0]).String()) + f, err := os.Open(b.append("app", args[0]).String()) if err != nil { return err } r = f var common *os.File - if common, err = os.Open(base.Append("common").String()); err != nil { + if common, err = os.Open(b.append("common").String()); err != nil { if !errors.Is(err, os.ErrNotExist) { _ = f.Close() return err @@ -170,7 +172,7 @@ func main() { } var name string - config, err = parse(args[0], base, r, &name) + config, err = parse(args[0], b, r, &name) if closeErr := f.Close(); err == nil { err = closeErr } @@ -187,7 +189,7 @@ func main() { config.Container.Args[2] = flagCommand } - if err = enterTemplate(base, name); err != nil { + if err = enterTemplate(b, name); err != nil { return err } return run(ctx, msg, flagInsecure, config, args[1:]...) @@ -200,6 +202,105 @@ func main() { ) } + c.NewCommand( + "id", "Show user/group id of the specified appid", + func(args []string) error { + var appid int + switch len(args) { + case 0: + log.Println("appid not specified, assuming reserved user") + break + + case 1: + var err error + appid, err = strconv.Atoi(args[0]) + if err != nil { + return os.ErrInvalid + } + break + + default: + return errors.New("id requires 1 argument") + } + + fmt.Println(hst.ToUser(outcome.Info().User, appid)) + return nil + }, + ) + + c.NewCommand( + "next", "Find next unused identity", + func([]string) error { + var names []string + if dents, err := os.ReadDir(b.append("app").String()); err != nil { + return err + } else { + names = make([]string, 0, len(dents)) + for _, dent := range dents { + name := dent.Name() + if dent.IsDir() || (len(name) > 0 && name[0] == '.') { + continue + } + names = append(names, name) + } + } + + apps := make([]*hst.Config, len(names)) + for i, name := range names { + r, err := os.Open(b.append("app", name).String()) + if err != nil { + return err + } + + apps[i], err = parse(name, b, r, nil) + if closeErr := r.Close(); err == nil { + err = closeErr + } + if err != nil { + return err + } + } + + p := make(map[int][]*hst.Config) + for _, config := range apps { + p[config.Identity] = append(p[config.Identity], config) + } + + identities := make([]int, 0, len(p)) + for identity, a := range p { + identities = append(identities, identity) + if msg.IsVerbose() && len(a) != 1 { + ids := make([]string, len(a)) + for i, config := range a { + ids[i] = config.ID + } + slices.Sort(ids) + msg.Verbosef( + "%s shares identity %d", + strings.Join(ids, ", "), identity, + ) + } + } + + if len(identities) == 0 { + // 0 is the reserved identity + fmt.Println(1) + return nil + } + + slices.Sort(identities) + next := identities[0] - 1 + for _, identity := range identities { + if identity != next+1 { + break + } + next = identity + } + fmt.Println(next + 1) + return nil + }, + ) + c.MustParse(os.Args[1:], func(err error) { if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil { os.Exit(e.ExitCode()) diff --git a/cmd/dist/VERSION b/cmd/dist/VERSION index a9e3e23c..f10bec9e 100644 --- a/cmd/dist/VERSION +++ b/cmd/dist/VERSION @@ -1 +1 @@ -v0.4.7 +v0.4.8 diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index 374d521c..76004d4f 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -351,7 +351,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr if !flagNoStore { var sc hst.Paths env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) - entry = tryIdentifier(msg, name, outcome.NewStore(&sc)) + entry = tryIdentifier(msg, syscall.Kill, name, outcome.NewStore(&sc)) } if entry == nil { @@ -380,7 +380,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr var sc hst.Paths env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) - entry := tryIdentifier(msg, args[0], outcome.NewStore(&sc)) + entry := tryIdentifier(msg, syscall.Kill, args[0], outcome.NewStore(&sc)) if entry == nil { log.Fatalf("%q does not match any active instance", args[0]) } @@ -395,7 +395,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr c.NewCommand("ps", "List active instances", func(args []string) error { var sc hst.Paths env.CopyPaths().Copy(&sc, new(outcome.Hsu).MustID(nil)) - printPs(msg, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON) + printPs(msg, syscall.Kill, os.Stdout, time.Now().UTC(), outcome.NewStore(&sc), flagShort, flagJSON) return errSuccess }).Flag(&flagShort, "short", command.BoolFlag(false), "Print instance id") } diff --git a/cmd/hakurei/parse.go b/cmd/hakurei/parse.go index 31d0dfa6..09721fad 100644 --- a/cmd/hakurei/parse.go +++ b/cmd/hakurei/parse.go @@ -99,7 +99,12 @@ func shortIdentifierString(s string) string { // tryIdentifier attempts to match [hst.State] from a [hex] representation of // [hst.ID] or a prefix of its lower half. -func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State { +func tryIdentifier( + msg message.Msg, + kill store.KillFunc, + name string, + s *store.Store, +) *hst.State { const ( likeShort = 1 << iota likeFull @@ -145,7 +150,7 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State { if strings.HasPrefix(eh.ID.String()[len(hst.ID{}):], name) { var entry hst.State - if _, err := eh.Load(&entry); err != nil { + if _, err := eh.Load(&entry, kill); err != nil { msg.GetLogger().Println(getMessage("cannot load state entry:", err)) continue } @@ -168,7 +173,7 @@ func tryIdentifier(msg message.Msg, name string, s *store.Store) *hst.State { if eh.ID == likelyID { var entry hst.State - if _, err := eh.Load(&entry); err != nil { + if _, err := eh.Load(&entry, kill); err != nil { msg.GetLogger().Println(getMessage("cannot load state entry:", err)) continue } diff --git a/cmd/hakurei/parse_test.go b/cmd/hakurei/parse_test.go index 1a574966..6f8a2299 100644 --- a/cmd/hakurei/parse_test.go +++ b/cmd/hakurei/parse_test.go @@ -108,7 +108,7 @@ func TestTryIdentifier(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - got := tryIdentifier(msg, tc.s, store.New(base)) + got := tryIdentifier(msg, nil, tc.s, store.New(base)) if !reflect.DeepEqual(got, tc.want) { t.Errorf("tryIdentifier: %#v, want %#v", got, tc.want) } diff --git a/cmd/hakurei/print.go b/cmd/hakurei/print.go index 780dc55b..2ac626e7 100644 --- a/cmd/hakurei/print.go +++ b/cmd/hakurei/print.go @@ -170,7 +170,14 @@ func printShowInstance( } // printPs writes a representation of active instances to output. -func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, short, flagJSON bool) { +func printPs( + msg message.Msg, + kill store.KillFunc, + output io.Writer, + now time.Time, + s *store.Store, + short, flagJSON bool, +) { f := func(a func(eh *store.EntryHandle)) { entries, copyError := s.All() for eh := range entries { @@ -184,7 +191,7 @@ func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, s if short { // short output requires identifier only var identifiers []*hst.ID f(func(eh *store.EntryHandle) { - if _, err := eh.Load(nil); err != nil { // passes through decode error + if _, err := eh.Load(nil, kill); err != nil { // passes through decode error msg.GetLogger().Println(getMessage("cannot validate state entry header:", err)) return } @@ -206,7 +213,7 @@ func printPs(msg message.Msg, output io.Writer, now time.Time, s *store.Store, s var instances []*hst.State f(func(eh *store.EntryHandle) { var state hst.State - if _, err := eh.Load(&state); err != nil { // passes through decode error + if _, err := eh.Load(&state, kill); err != nil { // passes through decode error msg.GetLogger().Println(getMessage("cannot load state entry:", err)) return } diff --git a/cmd/hakurei/print_test.go b/cmd/hakurei/print_test.go index a6e213c2..310fc885 100644 --- a/cmd/hakurei/print_test.go +++ b/cmd/hakurei/print_test.go @@ -770,7 +770,7 @@ func TestPrintPs(t *testing.T) { var printBuf, logBuf bytes.Buffer msg := message.New(log.New(&logBuf, "check: ", 0)) msg.SwapVerbose(true) - printPs(msg, &printBuf, testTime, s, tc.short, tc.json) + printPs(msg, nil, &printBuf, testTime, s, tc.short, tc.json) if got := printBuf.String(); got != tc.want { t.Errorf("printPs:\n%s\nwant\n%s", got, tc.want) return diff --git a/cmd/mbf/cache.go b/cmd/mbf/cache.go index 847d7d4e..0e20ca99 100644 --- a/cmd/mbf/cache.go +++ b/cmd/mbf/cache.go @@ -9,9 +9,9 @@ import ( "testing" "hakurei.app/check" - "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" "hakurei.app/message" + "hakurei.app/pkg" ) // cache refers to an instance of [pkg.Cache] that might be open. diff --git a/cmd/mbf/cache_test.go b/cmd/mbf/cache_test.go index a4d33af5..498743e9 100644 --- a/cmd/mbf/cache_test.go +++ b/cmd/mbf/cache_test.go @@ -5,8 +5,8 @@ import ( "os" "testing" - "hakurei.app/internal/pkg" "hakurei.app/message" + "hakurei.app/pkg" ) func TestCache(t *testing.T) { diff --git a/cmd/mbf/daemon.go b/cmd/mbf/daemon.go index d694bea0..72ab2b24 100644 --- a/cmd/mbf/daemon.go +++ b/cmd/mbf/daemon.go @@ -16,7 +16,7 @@ import ( "unique" "hakurei.app/check" - "hakurei.app/internal/pkg" + "hakurei.app/pkg" ) // daemonTimeout is the maximum amount of time cureFromIR will wait on I/O. diff --git a/cmd/mbf/daemon_test.go b/cmd/mbf/daemon_test.go index 97b3ba9b..58456f67 100644 --- a/cmd/mbf/daemon_test.go +++ b/cmd/mbf/daemon_test.go @@ -15,8 +15,8 @@ import ( "time" "hakurei.app/check" - "hakurei.app/internal/pkg" "hakurei.app/message" + "hakurei.app/pkg" ) func TestNoReply(t *testing.T) { diff --git a/cmd/mbf/info.go b/cmd/mbf/info.go index d0afc8f5..d8691f60 100644 --- a/cmd/mbf/info.go +++ b/cmd/mbf/info.go @@ -8,8 +8,8 @@ import ( "strings" "unique" - "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" + "hakurei.app/pkg" ) // commandInfo implements the info subcommand. @@ -56,9 +56,9 @@ func commandInfo( mustPrintln("website : " + strings.TrimSuffix(meta.Website, "/")) } - if len(meta.Dependencies) > 0 { + if len(meta.Runtimes) > 0 { mustPrint("depends on :") - for _, d := range meta.Dependencies { + for _, d := range meta.Runtimes { _meta, _ := rosa.MustLoad(d) s := _meta.Name if _meta.Version != rosa.Unversioned { diff --git a/cmd/mbf/info_test.go b/cmd/mbf/info_test.go index d85266ef..deeef459 100644 --- a/cmd/mbf/info_test.go +++ b/cmd/mbf/info_test.go @@ -13,9 +13,9 @@ import ( "unique" "unsafe" - "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" "hakurei.app/message" + "hakurei.app/pkg" ) func TestInfo(t *testing.T) { diff --git a/cmd/mbf/internal/pkgserver/api.go b/cmd/mbf/internal/pkgsite/api.go index 68ccd471..a3094359 100644 --- a/cmd/mbf/internal/pkgserver/api.go +++ b/cmd/mbf/internal/pkgsite/api.go @@ -1,5 +1,5 @@ -// Package pkgserver implements the package metadata service backend. -package pkgserver +// Package pkgsite implements the package metadata website. +package pkgsite import ( "context" diff --git a/cmd/mbf/internal/pkgserver/api_test.go b/cmd/mbf/internal/pkgsite/api_test.go index 1552fec9..d2c21d81 100644 --- a/cmd/mbf/internal/pkgserver/api_test.go +++ b/cmd/mbf/internal/pkgsite/api_test.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "net/http" diff --git a/cmd/mbf/internal/pkgserver/index.go b/cmd/mbf/internal/pkgsite/index.go index f62a7e73..760806c0 100644 --- a/cmd/mbf/internal/pkgserver/index.go +++ b/cmd/mbf/internal/pkgsite/index.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "cmp" @@ -6,8 +6,8 @@ import ( "slices" "strings" - "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" + "hakurei.app/pkg" ) const ( diff --git a/cmd/mbf/internal/pkgserver/index_test.go b/cmd/mbf/internal/pkgsite/index_test.go index 8f3b5530..abf78876 100644 --- a/cmd/mbf/internal/pkgserver/index_test.go +++ b/cmd/mbf/internal/pkgsite/index_test.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "bytes" diff --git a/cmd/mbf/internal/pkgserver/search.go b/cmd/mbf/internal/pkgsite/search.go index 15947804..ef7cd76f 100644 --- a/cmd/mbf/internal/pkgserver/search.go +++ b/cmd/mbf/internal/pkgsite/search.go @@ -1,4 +1,4 @@ -package pkgserver +package pkgsite import ( "cmp" diff --git a/cmd/mbf/internal/pkgserver/ui/index.html b/cmd/mbf/internal/pkgsite/ui/index.html index 6a5d72b4..6a5d72b4 100644 --- a/cmd/mbf/internal/pkgserver/ui/index.html +++ b/cmd/mbf/internal/pkgsite/ui/index.html diff --git a/cmd/mbf/internal/pkgserver/ui/index.ts b/cmd/mbf/internal/pkgsite/ui/index.ts index 0784b81f..0784b81f 100644 --- a/cmd/mbf/internal/pkgserver/ui/index.ts +++ b/cmd/mbf/internal/pkgsite/ui/index.ts diff --git a/cmd/mbf/internal/pkgserver/ui/style.css b/cmd/mbf/internal/pkgsite/ui/style.css index b4f281ac..b4f281ac 100644 --- a/cmd/mbf/internal/pkgserver/ui/style.css +++ b/cmd/mbf/internal/pkgsite/ui/style.css diff --git a/cmd/mbf/internal/pkgserver/ui/tsconfig.json b/cmd/mbf/internal/pkgsite/ui/tsconfig.json index 24df4936..24df4936 100644 --- a/cmd/mbf/internal/pkgserver/ui/tsconfig.json +++ b/cmd/mbf/internal/pkgsite/ui/tsconfig.json diff --git a/cmd/mbf/internal/pkgserver/ui/ui.go b/cmd/mbf/internal/pkgsite/ui/ui.go index 3fc0d89c..3fc0d89c 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui.go +++ b/cmd/mbf/internal/pkgsite/ui/ui.go diff --git a/cmd/mbf/internal/pkgserver/ui/ui_full.go b/cmd/mbf/internal/pkgsite/ui/ui_full.go index 564787dc..564787dc 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui_full.go +++ b/cmd/mbf/internal/pkgsite/ui/ui_full.go diff --git a/cmd/mbf/internal/pkgserver/ui/ui_stub.go b/cmd/mbf/internal/pkgsite/ui/ui_stub.go index daf010f8..daf010f8 100644 --- a/cmd/mbf/internal/pkgserver/ui/ui_stub.go +++ b/cmd/mbf/internal/pkgsite/ui/ui_stub.go diff --git a/cmd/mbf/main.go b/cmd/mbf/main.go index c6e1940f..22bfbc3e 100644 --- a/cmd/mbf/main.go +++ b/cmd/mbf/main.go @@ -39,12 +39,12 @@ import ( "hakurei.app/command" "hakurei.app/ext" "hakurei.app/fhs" - "hakurei.app/internal/pkg" "hakurei.app/internal/rosa" "hakurei.app/message" + "hakurei.app/pkg" - "hakurei.app/cmd/mbf/internal/pkgserver" - "hakurei.app/cmd/mbf/internal/pkgserver/ui" + "hakurei.app/cmd/mbf/internal/pkgsite" + "hakurei.app/cmd/mbf/internal/pkgsite/ui" ) // builtin contains native and embedded [rosa.Artifact] registrations. @@ -343,7 +343,7 @@ func main() { var mux http.ServeMux ui.Register(&mux) - if err = pkgserver.Register(ctx, &mux, r); err != nil { + if err = pkgsite.Register(ctx, &mux, r); err != nil { return } |
