Compare commits

..

4 Commits

353 changed files with 11553 additions and 18904 deletions
-3
View File
@@ -13,6 +13,3 @@
# cmd/dist default destination # cmd/dist default destination
/dist /dist
# local packages
/internal/rosa/package/local
+2 -8
View File
@@ -20,8 +20,8 @@ func (e AbsoluteError) Error() string {
} }
func (e AbsoluteError) Is(target error) bool { func (e AbsoluteError) Is(target error) bool {
ce, ok := errors.AsType[AbsoluteError](target) var ce AbsoluteError
if !ok { if !errors.As(target, &ce) {
return errors.Is(target, syscall.EINVAL) return errors.Is(target, syscall.EINVAL)
} }
return e == ce return e == ce
@@ -31,8 +31,6 @@ func (e AbsoluteError) Is(target error) bool {
type Absolute struct{ pathname unique.Handle[string] } type Absolute struct{ pathname unique.Handle[string] }
var ( var (
_ fmt.GoStringer = new(Absolute)
_ encoding.TextAppender = new(Absolute) _ encoding.TextAppender = new(Absolute)
_ encoding.TextMarshaler = new(Absolute) _ encoding.TextMarshaler = new(Absolute)
_ encoding.TextUnmarshaler = new(Absolute) _ encoding.TextUnmarshaler = new(Absolute)
@@ -42,10 +40,6 @@ var (
_ encoding.BinaryUnmarshaler = new(Absolute) _ encoding.BinaryUnmarshaler = new(Absolute)
) )
func (a *Absolute) GoString() string {
return fmt.Sprintf("check.MustAbs(%q)", a.String())
}
// ok returns whether [Absolute] is not the zero value. // ok returns whether [Absolute] is not the zero value.
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) } func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
-264
View File
@@ -1,264 +0,0 @@
package main
import (
"bufio"
"fmt"
"io"
"strconv"
"strings"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
// parsePair parses a NUL-delimited quoted paths pair.
func parsePair(s string) (source, target *check.Absolute, err error) {
var p string
if p, err = strconv.Unquote(s); err != nil {
return
}
_source, _target, ok := strings.Cut(p, "\x00")
if source, err = check.NewAbs(_source); err != nil {
return
}
if !ok {
return
}
target, err = check.NewAbs(_target)
return
}
// parse decodes a high-level configuration stream and returns its
// corresponding [hst.Config].
func parse(id string, base *check.Absolute, r io.Reader) (*hst.Config, error) {
shell := fhs.AbsRoot.Append("bin", "zsh")
home := hst.AbsPrivateTmp.Append("home")
c := hst.Config{
ID: id,
Enablements: new(hst.Enablements),
SessionBus: &hst.BusConfig{
Own: []string{
id + ".*",
"org.mpris.MediaPlayer2." + id + ".*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: make(map[string]string),
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
},
Upper: base.Append("template", "upper"),
}},
{FilesystemConfig: &hst.FSBind{
Target: home,
Source: base.Append("state", id),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
},
Username: "chronos",
Shell: shell,
Home: home,
Path: shell,
Args: []string{"zsh", "-c"},
Flags: hst.FCoverRun,
},
}
s := bufio.NewScanner(r)
scanOnce := func() error {
if s.Scan() {
return nil
}
if err := s.Err(); err != nil {
return err
}
return io.ErrUnexpectedEOF
}
if err := scanOnce(); err != nil {
return nil, err
}
if v, err := strconv.Atoi(s.Text()); err != nil {
return nil, err
} else {
c.Identity = v
}
if err := scanOnce(); err != nil {
return nil, err
}
c.Container.Args = append(c.Container.Args, s.Text())
var flagGPU, flagSystemBus bool
flags := map[string]*bool{
"gpu": &flagGPU,
"system_bus": &flagSystemBus,
}
for s.Scan() {
key, value, ok := strings.Cut(s.Text(), " ")
if key != "" && key[0] == ';' {
continue
}
if !ok {
if key == "" {
continue
}
var p *bool
if p, ok = flags[key]; ok {
*p = true
continue
}
switch key {
case "wayland":
*c.Enablements |= hst.EWayland
case "x11":
*c.Enablements |= hst.EX11
case "dbus":
*c.Enablements |= hst.EDBus
case "pipewire":
*c.Enablements |= hst.EPipeWire
case "multiarch":
c.Container.Flags |= hst.FMultiarch
case "devel":
c.Container.Flags |= hst.FDevel
case "userns":
c.Container.Flags |= hst.FUserns
case "net":
c.Container.Flags |= hst.FHostNet
case "abstract":
c.Container.Flags |= hst.FHostAbstract
case "tty":
c.Container.Flags |= hst.FTty
case "mapuid":
c.Container.Flags |= hst.FMapRealUID
case "device":
c.Container.Flags |= hst.FDevice
case "share_runtime":
c.Container.Flags |= hst.FShareRuntime
case "share_tmpdir":
c.Container.Flags |= hst.FShareTmpdir
default:
return nil, fmt.Errorf("invalid flag %q", key)
}
continue
}
switch key {
case "group":
c.Groups = append(c.Groups, value)
continue
case "env":
if key, value, ok = strings.Cut(value, "="); !ok {
return nil, fmt.Errorf("invalid environment %q", key)
}
c.Container.Env[key] = value
continue
case "ro":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
}},
)
continue
case "rw":
source, target, err := parsePair(value)
if err != nil {
return nil, err
}
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSBind{
Target: target,
Source: source,
Write: true,
}},
)
continue
case "own":
c.SessionBus.Own = append(c.SessionBus.Own, value)
continue
case "own_system":
c.SystemBus.Own = append(c.SystemBus.Own, value)
continue
case "talk":
c.SessionBus.Talk = append(c.SessionBus.Talk, value)
continue
case "talk_system":
c.SystemBus.Talk = append(c.SystemBus.Talk, value)
continue
default:
return nil, fmt.Errorf("invalid key %q", key)
}
}
if err := s.Err(); err != nil {
return nil, err
}
if flagGPU {
c.Container.Filesystem = append(c.Container.Filesystem, []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
}...)
}
if !flagSystemBus {
c.SystemBus = nil
}
if c.Container.Flags&hst.FShareTmpdir == 0 {
c.Container.Filesystem = append(c.Container.Filesystem,
hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 01777,
}},
)
}
return &c, nil
}
-152
View File
@@ -1,152 +0,0 @@
package main
import (
"reflect"
"strings"
"testing"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/hst"
)
func TestParse(t *testing.T) {
t.Parallel()
base := fhs.AbsProc.Append("nonexistent")
testCases := []struct {
name string
data string
want *hst.Config
err error
}{
{"com.discordapp.Discord", `8
exec Discord --ozone-platform-hint=wayland
gpu
wayland
dbus
system_bus
pipewire
userns
net
mapuid
share_runtime
share_tmpdir
group media_rw
env ELECTRON_TRASH=gio
rw "/sdcard"
; remove before reusing
ro "/bin\x00/.hakurei/bin"
talk org.kde.StatusNotifierWatcher
talk com.canonical.AppMenu.Registrar
talk com.canonical.indicator.application
talk com.canonical.Unity
`, &hst.Config{
Identity: 8,
ID: "com.discordapp.Discord",
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire),
Groups: []string{"media_rw"},
SessionBus: &hst.BusConfig{
Talk: []string{
"org.kde.StatusNotifierWatcher",
"com.canonical.AppMenu.Registrar",
"com.canonical.indicator.application",
"com.canonical.Unity",
},
Own: []string{
"com.discordapp.Discord.*",
"org.mpris.MediaPlayer2.com.discordapp.Discord.*",
},
Filter: true,
},
SystemBus: &hst.BusConfig{Filter: true},
Container: &hst.ContainerConfig{
Env: map[string]string{
"ELECTRON_TRASH": "gio",
},
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{
base.Append("template", "initial"),
},
Upper: base.Append("template", "upper"),
}},
{FilesystemConfig: &hst.FSBind{
Target: hst.AbsPrivateTmp.Append("home"),
Source: base.Append("state", "com.discordapp.Discord"),
Write: true,
Ensure: true,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsVar.Append("tmp"),
Write: true,
Perm: 01777,
}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("block")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("bus")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("class")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("dev")}},
{FilesystemConfig: &hst.FSBind{Source: fhs.AbsSys.Append("devices")}},
{FilesystemConfig: &hst.FSBind{
Source: check.MustAbs("/sdcard"),
Write: true,
}},
{FilesystemConfig: &hst.FSBind{
Target: check.MustAbs("/.hakurei/bin"),
Source: check.MustAbs("/bin"),
}},
{FilesystemConfig: &hst.FSBind{
Source: fhs.AbsDev.Append("dri"),
Device: true,
Optional: true,
}},
},
Username: "chronos",
Shell: fhs.AbsRoot.Append("bin", "zsh"),
Home: hst.AbsPrivateTmp.Append("home"),
Path: fhs.AbsRoot.Append("bin", "zsh"),
Args: []string{
"zsh", "-c",
"exec Discord --ozone-platform-hint=wayland",
},
Flags: hst.FCoverRun | hst.FUserns | hst.FHostNet | hst.FMapRealUID |
hst.FShareRuntime | hst.FShareTmpdir,
},
}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
got, err := parse(
tc.name,
base,
strings.NewReader(tc.data),
)
if !reflect.DeepEqual(err, tc.err) {
t.Errorf("parse: error = %v, want %v", err, tc.err)
}
if err != nil {
return
}
if !reflect.DeepEqual(got, tc.want) {
t.Errorf("parse: %#v, want %#v", got, tc.want)
}
})
}
}
-170
View File
@@ -1,170 +0,0 @@
// 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"
"log"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"hakurei.app/check"
"hakurei.app/command"
"hakurei.app/fhs"
"hakurei.app/hst"
"hakurei.app/message"
)
func main() {
log.SetFlags(0)
log.SetPrefix("app: ")
msg := message.New(log.Default())
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop()
var (
flagVerbose bool
flagBase string
base, template, initial, upper, work *check.Absolute
)
c := command.New(os.Stderr, log.Printf, "app", func([]string) (err error) {
msg.SwapVerbose(flagVerbose)
flagBase = os.ExpandEnv(flagBase)
if flagBase == "" {
flagBase = "state"
}
if flagBase, err = filepath.Abs(flagBase); err != nil {
return
} else if base, err = check.NewAbs(flagBase); err != nil {
return
}
template = base.Append("template")
initial = template.Append("initial")
upper = template.Append("upper")
work = template.Append("work")
return
}).Flag(
&flagVerbose,
"v", command.BoolFlag(false),
"Increase log verbosity",
).Flag(
&flagBase,
"d", command.StringFlag("$HAKUREI_APP_PATH"),
"Configuration and state directory",
)
{
var (
flagShell string
flagHome string
)
c.NewCommand(
"enter", "Enter mutable state template",
func([]string) error {
config := hst.Config{
ID: "app.hakurei.mutable",
Container: &hst.ContainerConfig{
Hostname: "mutable",
Filesystem: []hst.FilesystemConfigJSON{
{FilesystemConfig: &hst.FSOverlay{
Target: fhs.AbsRoot,
Lower: []*check.Absolute{initial},
Upper: upper,
Work: work,
}},
{FilesystemConfig: &hst.FSEphemeral{
Target: fhs.AbsTmp,
Write: true,
Perm: 0755,
}},
},
Username: "chronos",
Flags: hst.FMultiarch |
hst.FDevel |
hst.FUserns |
hst.FHostNet |
hst.FTty,
},
}
if a, err := check.NewAbs(flagShell); err != nil {
return err
} else {
config.Container.Shell = a
config.Container.Path = a
config.Container.Args = []string{
"-" + filepath.Base(flagShell),
}
}
if a, err := check.NewAbs(flagHome); err != nil {
return err
} else {
config.Container.Home = a
}
return run(ctx, msg, &config)
},
).Flag(
&flagShell,
"shell", command.StringFlag("/bin/zsh"),
"Shell program within container",
).Flag(
&flagHome,
"home", command.StringFlag("/home/chronos"),
"Home directory within container",
)
}
c.NewCommand(
"run", "Start the named application",
func(args []string) error {
if len(args) != 1 {
return errors.New("run requires 1 argument")
}
var config *hst.Config
f, err := os.Open(base.Append("app", args[0]).String())
if err != nil {
return err
}
config, err = parse(args[0], base, f)
if closeErr := f.Close(); err == nil {
err = closeErr
}
if err != nil {
return err
}
return run(ctx, msg, config)
},
)
c.MustParse(os.Args[1:], func(err error) {
if e, ok := errors.AsType[*exec.ExitError](err); ok && e != nil {
os.Exit(e.ExitCode())
}
if w, ok := err.(interface{ Unwrap() []error }); !ok {
log.Fatal(err)
} else {
errs := w.Unwrap()
for i, e := range errs {
if i == len(errs)-1 {
log.Fatal(e)
}
log.Println(e)
}
}
})
}
-51
View File
@@ -1,51 +0,0 @@
package main
import (
"context"
"encoding/json"
"os"
"os/exec"
"syscall"
"hakurei.app/hst"
"hakurei.app/message"
)
// run starts a container via cmd/hakurei and returns after it terminates.
func run(ctx context.Context, msg message.Msg, config *hst.Config) error {
c, cancel := context.WithCancel(ctx)
defer cancel()
cmd := exec.CommandContext(c, "hakurei")
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
cmd.Cancel = func() error {
return cmd.Process.Signal(syscall.SIGINT)
}
if msg.IsVerbose() {
cmd.Args = append(cmd.Args, "-v")
}
cmd.Args = append(cmd.Args, "run", "3")
r, w, err := os.Pipe()
if err != nil {
return err
}
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
if err = cmd.Start(); err != nil {
_, _ = r.Close(), w.Close()
return err
}
if err = r.Close(); err != nil {
_ = w.Close()
return err
} else if err = json.NewEncoder(w).Encode(&config); err != nil {
_ = w.Close()
return err
} else if err = w.Close(); err != nil {
return err
}
return cmd.Wait()
}
-1
View File
@@ -1 +0,0 @@
v0.4.4
+1 -6
View File
@@ -18,13 +18,8 @@ import (
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings"
) )
//go:generate sh -c "git describe --tags > VERSION"
//go:embed VERSION
var version string
// getenv looks up an environment variable, and returns fallback if it is unset. // getenv looks up an environment variable, and returns fallback if it is unset.
func getenv(key, fallback string) string { func getenv(key, fallback string) string {
if v, ok := os.LookupEnv(key); ok { if v, ok := os.LookupEnv(key); ok {
@@ -52,7 +47,7 @@ func main() {
verbose := os.Getenv("VERBOSE") != "" verbose := os.Getenv("VERBOSE") != ""
runTests := os.Getenv("HAKUREI_DIST_MAKE") == "" runTests := os.Getenv("HAKUREI_DIST_MAKE") == ""
version = getenv("HAKUREI_VERSION", strings.TrimSpace(version)) version := getenv("HAKUREI_VERSION", "untagged")
prefix := getenv("PREFIX", "/usr") prefix := getenv("PREFIX", "/usr")
destdir := getenv("DESTDIR", "dist") destdir := getenv("DESTDIR", "dist")
+20 -153
View File
@@ -5,91 +5,17 @@
package main package main
import ( import (
"context"
"crypto/rand"
"log" "log"
"os" "os"
"os/signal"
"runtime" "runtime"
"runtime/pprof"
"slices"
"strings" "strings"
. "syscall" . "syscall"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
var r report.Reporter
func init() {
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
r.SetOutput(log.Default())
// this handles SIGQUIT to provide useful debugging information without
// terminating, and prevents the runtime from throwing on the must family
// of early error reporting functions, DO NOT REMOVE
c := make(chan os.Signal, 1)
signal.Notify(c, SIGQUIT)
go func() {
for {
<-c
if p := pprof.Lookup("goroutine"); p == nil {
log.Println("initial built-in goroutine profile does not exist")
} else if err := p.WriteTo(os.Stderr, 2); err != nil {
log.Println(err)
}
}
}()
}
// fatal calls [log.Println] with v and blocks forever. Must be called from
// main. Must not be used after error reporting is set up.
func fatal(v ...any) {
log.Println(v...)
log.Println("unable to continue, please reboot and resolve the problem manually")
select {}
}
// must calls fatal with err if it is non-nil.
func must(err error) {
if err != nil {
log.Println(err)
select {}
}
}
// mustSyscall is like must, but with an additional action name.
func mustSyscall(action string, err error) {
if err != nil {
fatal("cannot "+action+":", err)
select {}
}
}
// must1 is like must, but with an additional passed through value.
func must1[T any](v T, err error) T {
must(err)
return v
}
const (
// optionSystem specifies devpath of the system device.
optionSystem = "system"
// flagVerbose increases output verbosity.
flagVerbose = "verbose"
// flagStrict sets [report.DStrict] on r.
flagStrict = "strict"
// flagNoRecover sets [report.DNoRecover] on r.
flagNoRecover = "no_recover"
) )
func main() { func main() {
runtime.LockOSThread() runtime.LockOSThread()
log.SetFlags(0)
log.SetPrefix("earlyinit: ")
var ( var (
option map[string]string option map[string]string
@@ -107,44 +33,15 @@ func main() {
} }
} }
{ if err := Mount(
var flag uint64
if slices.Contains(flags, flagStrict) {
flag |= report.DStrict
}
if slices.Contains(flags, flagNoRecover) {
flag |= report.DNoRecover
}
log.Printf("reporting flags %x", flag)
r.SetFlags(flag)
}
msg := message.New(log.Default())
msg.SwapVerbose(slices.Contains(flags, flagVerbose))
mustSyscall("mount devtmpfs", Mount(
"devtmpfs", "devtmpfs",
"/dev/", "/dev/",
"devtmpfs", "devtmpfs",
MS_NOSUID|MS_NOEXEC, MS_NOSUID|MS_NOEXEC,
"", "",
)) ); err != nil {
must(os.Mkdir("/dev/pts/", 0)) log.Fatalf("cannot mount devtmpfs: %v", err)
mustSyscall("mount devpts", Mount( }
"devpts",
"/dev/pts/",
"devpts",
MS_NOSUID|MS_NOEXEC,
"mode=620,ptmxmode=666",
))
must(os.Mkdir("/dev/shm/", 0))
mustSyscall("mount shm", Mount(
"shm",
"/dev/shm/",
"tmpfs",
MS_NOSUID|MS_NODEV,
"",
))
// The kernel might be unable to set up the console. When that happens, // The kernel might be unable to set up the console. When that happens,
// printk is called with "Warning: unable to open an initial console." // printk is called with "Warning: unable to open an initial console."
@@ -201,49 +98,6 @@ func main() {
"", "",
)) ))
conn := must1(uevent.Dial(-128 * 1024 * 1024))
events := make(chan *uevent.Message, 1<<10)
var uuid uevent.UUID
must1(rand.Read(uuid[:]))
ctx, cancel := context.WithCancel(context.Background())
go consume(ctx, msg, &r, conn, uuid, events)
s := kobject.New(uuid, func(o *kobject.Object, env map[string]string) {
p := make([]string, 0, len(env))
for k, v := range env {
p = append(p, k+"="+v)
}
slices.Sort(p)
log.Printf("change %s: %s", o.DevPath, strings.Join(p, ", "))
}, func(err error) {
severity := report.Inconsistent
if e, ok := err.(kobject.EventError); ok && e.Kind == kobject.EBadTarget {
severity = report.Trivial
}
r.Dispatch(
severity,
"processed inconsistent uevent",
err,
)
})
go func() {
s.Consume(ctx, events)
log.Println("closing NETLINK_KOBJECT_UEVENT socket")
cancel()
if err := conn.Close(); err != nil {
log.Fatal(err) // not reached
}
}()
must(os.Mkdir("/system", 0))
if devpath := option[optionSystem]; devpath == "" {
fatal("system must be nonempty")
} else {
log.Printf("waiting for devpath pattern %q", devpath)
mustMountSystem(ctx, s, devpath)
}
// after top level has been set up // after top level has been set up
mustSyscall("remount root", Mount( mustSyscall("remount root", Mount(
"", "",
@@ -259,6 +113,19 @@ func main() {
[]byte("/system/lib/firmware"), []byte("/system/lib/firmware"),
0, 0,
)) ))
go dispatchModprobe(ctx, s)
} }
// mustSyscall calls [log.Fatalln] if err is non-nil.
func mustSyscall(action string, err error) {
if err != nil {
log.Fatalln("cannot "+action+":", err)
}
}
// must calls [log.Fatal] with err if it is non-nil.
func must(err error) {
if err != nil {
log.Fatal(err)
}
}
-73
View File
@@ -1,73 +0,0 @@
package main
import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
)
// ModprobeError describes an unsuccessful modprobe invocation.
type ModprobeError struct {
ModAlias string `json:"modalias"`
Stdout string `json:"stdout"`
Stderr string `json:"stderr"`
ExitCode int `json:"exit_code"`
}
var _ report.RepresentableError = ModprobeError{}
func (ModprobeError) Representable() {}
func (e ModprobeError) Error() string {
return fmt.Sprintf(
"modprobe exit status %d: %s",
e.ExitCode, strings.TrimSpace(e.Stderr),
)
}
// dispatchModprobe invokes modprobe for [uevent.KOBJ_ADD] events raising new
// MODALIAS strings.
func dispatchModprobe(
ctx context.Context,
s *kobject.State,
) {
aliases := make(chan string, 1<<8)
go func() {
defer close(aliases)
s.Range(ctx, func(o *kobject.Object, act uevent.KobjectAction) bool {
if act == uevent.KOBJ_ADD && o.Driver == "" && o.ModAlias != "" {
aliases <- o.ModAlias
}
return true
})
}()
for alias := range aliases {
stdout, err := exec.Command("/system/sbin/modprobe", alias).Output()
if err == nil {
if len(stdout) > 0 {
log.Println(string(stdout))
}
continue
}
exitError, ok := errors.AsType[*exec.ExitError](err)
if !ok || exitError == nil {
r.Dispatch(report.Degraded, "invoke modprobe", err)
continue
}
r.Dispatch(report.Trivial, "load device driver", ModprobeError{
ModAlias: alias,
Stdout: string(stdout),
Stderr: string(exitError.Stderr),
ExitCode: exitError.ExitCode(),
})
}
}
-71
View File
@@ -1,71 +0,0 @@
package main
import (
"context"
"errors"
"os"
"path/filepath"
"strconv"
"syscall"
"time"
"hakurei.app/check"
"hakurei.app/fhs"
"hakurei.app/internal/kobject"
"hakurei.app/internal/uevent"
)
// mustMountSystem waits for and mounts a system device matching pattern.
func mustMountSystem(
ctx context.Context,
s *kobject.State,
pattern string,
) {
c, stop := context.WithTimeout(ctx, 30*time.Second)
defer stop()
for {
var matchErr error
var systemPath *check.Absolute
s.Range(c, func(o *kobject.Object, act uevent.KobjectAction) bool {
if (act != uevent.KOBJ_ADD && act != uevent.KOBJ_CHANGE) ||
o.Subsystem != "block" ||
o.Env["DEVTYPE"] != "disk" {
return true
}
if ok, err := filepath.Match(pattern, o.DevPath); err != nil {
matchErr = err
return false
} else if !ok {
return true
}
name, ok := o.Env["DEVNAME"]
if !ok {
return true
}
systemPath = fhs.AbsDev.Append(name)
return false
})
if c.Err() != nil {
fatal("devpath", strconv.Quote(pattern), "never appeared")
}
if matchErr != nil {
fatal("cannot match system devpath:", matchErr)
}
err := syscall.Mount(
systemPath.String(),
"/system/",
"squashfs",
0,
"threads=multi",
)
if err == nil {
break
}
if !errors.Is(err, os.ErrNotExist) {
fatal("cannot mount system:", err)
}
}
}
-104
View File
@@ -1,104 +0,0 @@
package main
import (
"context"
"time"
"hakurei.app/fhs"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
"hakurei.app/message"
)
// newRejectColdboot returns a function to be called on every subsequent pending
// coldboot, and returns whether coldboot should proceed. Rejection is sticky.
func newRejectColdboot() func() bool {
// one coldboot per five minutes, two consecutive coldboot
const (
coldbootInterval = 5 * time.Minute
coldbootBurst = 2
)
done := make(chan struct{})
s := make(chan struct{}, coldbootBurst)
s <- struct{}{} // for early fault before reporting is ready
go func() {
t := time.NewTicker(coldbootInterval)
for {
select {
case <-done:
return
case <-t.C:
select {
case s <- struct{}{}:
default:
}
}
}
}()
return func() bool {
select {
case <-s:
return true
case <-done:
return false
default:
close(done)
return false
}
}
}
// consume continuously consumes events from conn with retries.
func consume(
ctx context.Context,
msg message.Msg,
r *report.Reporter,
conn *uevent.Conn,
uuid uevent.UUID,
events chan<- *uevent.Message,
) {
defer close(events)
nextColdboot := newRejectColdboot()
coldboot := true
retry:
if dispatchErr := conn.Consume(ctx, fhs.Sys, &uuid, events, coldboot, func(path string) {
msg.Verbose("coldboot visited", path)
}, func(err error) bool {
if _, ok := err.(uevent.NeedsColdboot); ok && !nextColdboot() {
r.Dispatch(
report.Degraded,
"rejecting coldboot loop",
err,
)
return false
}
r.Dispatch(
report.Inconsistent,
"consumed invalid message",
err,
)
return true
}, nil); dispatchErr != nil {
if _, ok := dispatchErr.(uevent.Recoverable); !ok {
r.Dispatch(
report.Fatal,
"discontinuing uevent processing due to nonrecoverable error",
dispatchErr,
)
return
}
if _, ok := dispatchErr.(uevent.NeedsColdboot); ok {
// coldboot loop rejected by handler
coldboot = false
}
goto retry
}
}
-35
View File
@@ -1,35 +0,0 @@
package main
import (
"testing"
"testing/synctest"
"time"
)
func TestRejectColdboot(t *testing.T) {
t.Parallel()
synctest.Test(t, func(t *testing.T) {
nextColdboot := newRejectColdboot()
want := func(want bool) {
if got := nextColdboot(); got != want {
t.Fatalf("nextColdboot: %v, want %v", got, want)
}
}
synctest.Wait()
want(true)
time.Sleep(time.Hour)
synctest.Wait()
want(true)
want(true)
time.Sleep(5 * time.Minute)
synctest.Wait()
want(true)
want(false)
time.Sleep(time.Hour)
synctest.Wait()
want(false)
want(false)
})
}
+5 -6
View File
@@ -7,8 +7,7 @@ import (
"strconv" "strconv"
) )
// decodeJSON decodes json from r and stores it in v. A non-nil error results in // decodeJSON decodes json from r and stores it in v. A non-nil error results in a call to fatal.
// a call to fatal.
func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) { func decodeJSON(fatal func(v ...any), op string, r io.Reader, v any) {
err := json.NewDecoder(r).Decode(v) err := json.NewDecoder(r).Decode(v)
if err == nil { if err == nil {
@@ -48,14 +47,14 @@ func encodeJSON(fatal func(v ...any), output io.Writer, short bool, v any) {
} }
if err := encoder.Encode(v); err != nil { if err := encoder.Encode(v); err != nil {
if e, ok := errors.AsType[*json.MarshalerError](err); ok && e != nil { var marshalerError *json.MarshalerError
if errors.As(err, &marshalerError) && marshalerError != nil {
// this likely indicates an implementation error in hst // this likely indicates an implementation error in hst
fatal("cannot encode json for " + e.Type.String() + ": " + e.Err.Error()) fatal("cannot encode json for " + marshalerError.Type.String() + ": " + marshalerError.Err.Error())
return return
} }
// UnsupportedTypeError, UnsupportedValueError: incorrect usage, does // UnsupportedTypeError, UnsupportedValueError: incorrect usage, does not need to be handled
// not need to be handled
fatal("cannot write json: " + err.Error()) fatal("cannot write json: " + err.Error())
} }
} }
+2 -5
View File
@@ -64,7 +64,7 @@ func TestPrintShowInstance(t *testing.T) {
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
@@ -161,7 +161,7 @@ App
Identity: 9 (org.chromium.Chromium) Identity: 9 (org.chromium.Chromium)
Enablements: wayland, dbus, pipewire Enablements: wayland, dbus, pipewire
Groups: video, dialout, plugdev Groups: video, dialout, plugdev
Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir Flags: multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir
Home: /data/data/org.chromium.Chromium Home: /data/data/org.chromium.Chromium
Hostname: localhost Hostname: localhost
Path: /run/current-system/sw/bin/chromium Path: /run/current-system/sw/bin/chromium
@@ -355,7 +355,6 @@ App
"multiarch": true, "multiarch": true,
"map_real_uid": true, "map_real_uid": true,
"device": true, "device": true,
"cover_run": true,
"share_runtime": true, "share_runtime": true,
"share_tmpdir": true "share_tmpdir": true
}, },
@@ -507,7 +506,6 @@ App
"multiarch": true, "multiarch": true,
"map_real_uid": true, "map_real_uid": true,
"device": true, "device": true,
"cover_run": true,
"share_runtime": true, "share_runtime": true,
"share_tmpdir": true "share_tmpdir": true
} }
@@ -706,7 +704,6 @@ func TestPrintPs(t *testing.T) {
"multiarch": true, "multiarch": true,
"map_real_uid": true, "map_real_uid": true,
"device": true, "device": true,
"cover_run": true,
"share_runtime": true, "share_runtime": true,
"share_tmpdir": true "share_tmpdir": true
}, },
+23 -1
View File
@@ -21,6 +21,15 @@
// following paragraphs are considered an internal detail and not covered by the // following paragraphs are considered an internal detail and not covered by the
// compatibility promise. // compatibility promise.
// //
// After checking credentials, hsu checks via /proc/ the absolute pathname of
// its parent process, and fails if it does not match the hakurei pathname set
// at link time. This is not a security feature: the priv-side is considered
// trusted, and this feature makes no attempt to address the racy nature of
// querying /proc/, or debuggers attached to the parent process. Instead, this
// aims to discourage misuse and reduce confusion if the user accidentally
// stumbles upon this program. It also prevents accidental use of the incorrect
// installation of hsu in some environments.
//
// Since target container environment variables are set up in shim via the // Since target container environment variables are set up in shim via the
// [container] infrastructure, the environment is used for parameters from the // [container] infrastructure, the environment is used for parameters from the
// parent process. // parent process.
@@ -53,6 +62,7 @@ import (
"runtime" "runtime"
"slices" "slices"
"strconv" "strconv"
"strings"
"syscall" "syscall"
) )
@@ -97,6 +107,18 @@ func main() {
return return
} }
var toolPath string
pexe := filepath.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
if p, err := os.Readlink(pexe); err != nil {
log.Fatalf("cannot read parent executable path: %v", err)
} else if strings.HasSuffix(p, " (deleted)") {
log.Fatal("hakurei executable has been deleted")
} else if p != hakureiPath {
log.Fatal("this program must be started by hakurei")
} else {
toolPath = p
}
// refuse to run if hsurc is not protected correctly // refuse to run if hsurc is not protected correctly
if s, err := os.Stat(hsuConfPath); err != nil { if s, err := os.Stat(hsuConfPath); err != nil {
log.Fatal(err) log.Fatal(err)
@@ -183,7 +205,7 @@ func main() {
log.Fatalf("cannot set no_new_privs flag: %s", errno.Error()) log.Fatalf("cannot set no_new_privs flag: %s", errno.Error())
} }
if err := syscall.Exec(hakureiPath, []string{ if err := syscall.Exec(toolPath, []string{
"hakurei", "hakurei",
"shim", "shim",
}, []string{ }, []string{
+20 -21
View File
@@ -2,14 +2,13 @@ package main
import ( import (
"context" "context"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
"testing" "testing"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/rosa"
"hakurei.app/message" "hakurei.app/message"
) )
@@ -31,7 +30,7 @@ type cache struct {
// Loaded artifact of [rosa.QEMU]. // Loaded artifact of [rosa.QEMU].
qemu pkg.Artifact qemu pkg.Artifact
base, mirror string base string
} }
// open opens the underlying [pkg.Cache]. // open opens the underlying [pkg.Cache].
@@ -87,21 +86,6 @@ func (cache *cache) open() (err error) {
} }
done <- struct{}{} done <- struct{}{}
if cache.mirror != "" {
var pub []byte
pub, err = os.ReadFile(base.Append("ed25519.pub").String())
if err != nil {
cache.c.Close()
return
}
var r rosa.Remote
if r, err = rosa.NewRemote(cache.mirror, pub, http.DefaultClient); err != nil {
cache.c.Close()
return err
}
cache.c.SetExternal(r)
}
if cache.qemu != nil { if cache.qemu != nil {
var pathname *check.Absolute var pathname *check.Absolute
pathname, _, err = cache.c.Cure(cache.qemu) pathname, _, err = cache.c.Cure(cache.qemu)
@@ -110,9 +94,24 @@ func (cache *cache) open() (err error) {
return return
} }
for arch, entry := range rosa.Arches(pathname) { pkg.RegisterArch("riscv64", container.BinfmtEntry{
pkg.RegisterArch(arch, entry) Offset: 0,
} Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xf3\x00",
Mask: "\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
Interpreter: pathname.Append(
"system/bin",
"qemu-riscv64",
),
})
pkg.RegisterArch("arm64", container.BinfmtEntry{
Offset: 0,
Magic: "\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\xb7\x00",
Mask: "\xff\xff\xff\xff\xff\xff\xff\xfc\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff",
Interpreter: pathname.Append(
"system/bin",
"qemu-aarch64",
),
})
} }
return return
+9 -13
View File
@@ -6,7 +6,6 @@ import (
"io" "io"
"os" "os"
"strings" "strings"
"unique"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
"hakurei.app/internal/rosa" "hakurei.app/internal/rosa"
@@ -36,19 +35,17 @@ func commandInfo(
} }
} }
t := rosa.Native().Std()
for i, name := range args { for i, name := range args {
handle := rosa.ArtifactH(unique.Make(name)) if p, ok := rosa.ResolveName(name); !ok {
if meta, a := t.Load(handle); meta == nil {
return fmt.Errorf("unknown artifact %q", name) return fmt.Errorf("unknown artifact %q", name)
} else { } else {
var suffix string var suffix string
if version := rosa.Std.Version(p); version != rosa.Unversioned {
if meta.Version != rosa.Unversioned { suffix += "-" + version
suffix += "-" + meta.Version
} }
mustPrintln("name : " + name + suffix) mustPrintln("name : " + name + suffix)
meta := rosa.GetMetadata(p)
mustPrintln("description : " + meta.Description) mustPrintln("description : " + meta.Description)
if meta.Website != "" { if meta.Website != "" {
mustPrintln("website : " + mustPrintln("website : " +
@@ -57,10 +54,9 @@ func commandInfo(
if len(meta.Dependencies) > 0 { if len(meta.Dependencies) > 0 {
mustPrint("depends on :") mustPrint("depends on :")
for _, d := range meta.Dependencies { for _, d := range meta.Dependencies {
_meta, _ := rosa.Native().Std().MustLoad(d) s := rosa.GetMetadata(d).Name
s := _meta.Name if version := rosa.Std.Version(d); version != rosa.Unversioned {
if _meta.Version != rosa.Unversioned { s += "-" + version
s += "-" + _meta.Version
} }
mustPrint(" " + s) mustPrint(" " + s)
} }
@@ -72,7 +68,7 @@ func commandInfo(
if r == nil { if r == nil {
var f io.ReadSeekCloser var f io.ReadSeekCloser
err = cm.Do(func(cache *pkg.Cache) (err error) { err = cm.Do(func(cache *pkg.Cache) (err error) {
f, err = cache.OpenStatus(a) f, err = cache.OpenStatus(rosa.Std.Load(p))
return return
}) })
if err != nil { if err != nil {
@@ -91,7 +87,7 @@ func commandInfo(
} }
} }
} else if err = cm.Do(func(cache *pkg.Cache) (err error) { } else if err = cm.Do(func(cache *pkg.Cache) (err error) {
status, n := r.ArtifactOf(cache.Ident(a)) status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
if status == nil { if status == nil {
mustPrintln( mustPrintln(
statusPrefix + "not in report", statusPrefix + "not in report",
+13 -22
View File
@@ -10,7 +10,6 @@ import (
"strings" "strings"
"syscall" "syscall"
"testing" "testing"
"unique"
"unsafe" "unsafe"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
@@ -21,14 +20,6 @@ import (
func TestInfo(t *testing.T) { func TestInfo(t *testing.T) {
t.Parallel() t.Parallel()
_t := rosa.Native().Std()
qemuMeta, _ := _t.Load(rosa.H("qemu"))
glibMeta, _ := _t.Load(rosa.H("glib"))
zlibMeta, zlib := _t.Load(rosa.H("zlib"))
zstdMeta, _ := _t.Load(rosa.H("zstd"))
hakureiMeta, _ := _t.Load(rosa.H("hakurei"))
hakureiDistMeta, _ := _t.Load(rosa.H("hakurei-dist"))
testCases := []struct { testCases := []struct {
name string name string
args []string args []string
@@ -38,24 +29,24 @@ func TestInfo(t *testing.T) {
wantErr any wantErr any
}{ }{
{"qemu", []string{"qemu"}, nil, "", ` {"qemu", []string{"qemu"}, nil, "", `
name : qemu-` + qemuMeta.Version + ` name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
description : a generic and open source machine emulator and virtualizer description : a generic and open source machine emulator and virtualizer
website : https://www.qemu.org website : https://www.qemu.org
depends on : glib-` + glibMeta.Version + ` zstd-` + zstdMeta.Version + ` depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
`, nil}, `, nil},
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", ` {"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
name : hakurei-` + hakureiMeta.Version + ` name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
description : low-level userspace tooling for Rosa OS description : low-level userspace tooling for Rosa OS
website : https://hakurei.app website : https://hakurei.app
name : hakurei-dist-` + hakureiDistMeta.Version + ` name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
description : low-level userspace tooling for Rosa OS (distribution tarball) description : low-level userspace tooling for Rosa OS (distribution tarball)
website : https://hakurei.app website : https://hakurei.app
`, nil}, `, nil},
{"nonexistent", []string{"zlib", "\x00"}, nil, "", ` {"nonexistent", []string{"zlib", "\x00"}, nil, "", `
name : zlib-` + zlibMeta.Version + ` name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library description : lossless data-compression library
website : https://zlib.net website : https://zlib.net
@@ -65,12 +56,12 @@ website : https://zlib.net
"zstd": "internal/pkg (amd64) on satori\n", "zstd": "internal/pkg (amd64) on satori\n",
"hakurei": "internal/pkg (amd64) on satori\n\n", "hakurei": "internal/pkg (amd64) on satori\n\n",
}, "", ` }, "", `
name : zlib-` + zlibMeta.Version + ` name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library description : lossless data-compression library
website : https://zlib.net website : https://zlib.net
status : not yet cured status : not yet cured
name : zstd-` + zstdMeta.Version + ` name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
description : a fast compression algorithm description : a fast compression algorithm
website : https://facebook.github.io/zstd website : https://facebook.github.io/zstd
status : internal/pkg (amd64) on satori status : internal/pkg (amd64) on satori
@@ -79,19 +70,19 @@ status : internal/pkg (amd64) on satori
{"status cache perm", []string{"zlib"}, map[string]string{ {"status cache perm", []string{"zlib"}, map[string]string{
"zlib": "\x00", "zlib": "\x00",
}, "", ` }, "", `
name : zlib-` + zlibMeta.Version + ` name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library description : lossless data-compression library
website : https://zlib.net website : https://zlib.net
`, func(cm *cache) error { `, func(cm *cache) error {
return &os.PathError{ return &os.PathError{
Op: "open", Op: "open",
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(zlib).Value())), Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
Err: syscall.EACCES, Err: syscall.EACCES,
} }
}}, }},
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), ` {"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
name : zlib-` + zlibMeta.Version + ` name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
description : lossless data-compression library description : lossless data-compression library
website : https://zlib.net website : https://zlib.net
status : not in report status : not in report
@@ -140,8 +131,8 @@ status : not in report
if tc.status != nil { if tc.status != nil {
for name, status := range tc.status { for name, status := range tc.status {
_, a := _t.Load(rosa.ArtifactH(unique.Make(name))) p, ok := rosa.ResolveName(name)
if a == nil { if !ok {
t.Fatalf("invalid name %q", name) t.Fatalf("invalid name %q", name)
} }
perm := os.FileMode(0400) perm := os.FileMode(0400)
@@ -152,7 +143,7 @@ status : not in report
return os.WriteFile(filepath.Join( return os.WriteFile(filepath.Join(
cm.base, cm.base,
"status", "status",
pkg.Encode(cache.Ident(a).Value()), pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
), unsafe.Slice(unsafe.StringData(status), len(status)), perm) ), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
}); err != nil { }); err != nil {
t.Fatalf("Do: error = %v", err) t.Fatalf("Do: error = %v", err)
+3 -3
View File
@@ -30,7 +30,7 @@ var (
// handleInfo writes constant system information. // handleInfo writes constant system information.
func handleInfo(w http.ResponseWriter, _ *http.Request) { func handleInfo(w http.ResponseWriter, _ *http.Request) {
infoPayloadOnce.Do(func() { infoPayloadOnce.Do(func() {
infoPayload.Count = len(rosa.Native().Collect()) infoPayload.Count = int(rosa.PresetUnexportedStart)
infoPayload.HakureiVersion = info.Version() infoPayload.HakureiVersion = info.Version()
}) })
// TODO(mae): cache entire response if no additional fields are planned // TODO(mae): cache entire response if no additional fields are planned
@@ -91,7 +91,7 @@ func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
if err != nil || i >= len(index.sorts[0]) || i < 0 { if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error( http.Error(
w, "index must be an integer between 0 and "+ w, "index must be an integer between 0 and "+
strconv.Itoa(len(index.sorts[0])-1), strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
http.StatusBadRequest, http.StatusBadRequest,
) )
return return
@@ -125,7 +125,7 @@ func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request)
if err != nil || i >= len(index.sorts[0]) || i < 0 { if err != nil || i >= len(index.sorts[0]) || i < 0 {
http.Error( http.Error(
w, "index must be an integer between 0 and "+ w, "index must be an integer between 0 and "+
strconv.Itoa(len(index.sorts[0])-1), strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
http.StatusBadRequest, http.StatusBadRequest,
) )
return return
+74 -4
View File
@@ -3,6 +3,7 @@ package pkgserver
import ( import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"slices"
"strconv" "strconv"
"testing" "testing"
@@ -31,7 +32,7 @@ func TestAPIInfo(t *testing.T) {
checkPayload(t, resp, struct { checkPayload(t, resp, struct {
Count int `json:"count"` Count int `json:"count"`
HakureiVersion string `json:"hakurei_version"` HakureiVersion string `json:"hakurei_version"`
}{len(rosa.Native().Collect()), info.Version()}) }{int(rosa.PresetUnexportedStart), info.Version()})
} }
func TestAPIGet(t *testing.T) { func TestAPIGet(t *testing.T) {
@@ -92,12 +93,11 @@ func TestAPIGet(t *testing.T) {
) )
}) })
count := len(rosa.Native().Collect())
t.Run("index", func(t *testing.T) { t.Run("index", func(t *testing.T) {
t.Parallel() t.Parallel()
checkValidate( checkValidate(
t, "limit=1&sort=0&index", 0, count-1, t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
"index must be an integer between 0 and "+strconv.Itoa(count-1), "index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
) )
}) })
@@ -108,4 +108,74 @@ func TestAPIGet(t *testing.T) {
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)), "sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
) )
}) })
checkWithSuffix := func(name, suffix string, want []*metadata) {
t.Run(name, func(t *testing.T) {
t.Parallel()
w := newRequest(suffix)
resp := w.Result()
checkStatus(t, resp, http.StatusOK)
checkAPIHeader(t, w.Header())
checkPayloadFunc(t, resp, func(got *struct {
Values []*metadata `json:"values"`
}) bool {
return slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
return (a.Version == b.Version ||
a.Version == rosa.Unversioned ||
b.Version == rosa.Unversioned) &&
a.HasReport == b.HasReport &&
a.Name == b.Name &&
a.Description == b.Description &&
a.Website == b.Website
})
})
})
}
checkWithSuffix("declarationAscending", "?limit=2&index=1&sort=0", []*metadata{
{
Metadata: rosa.GetMetadata(1),
Version: rosa.Std.Version(1),
},
{
Metadata: rosa.GetMetadata(2),
Version: rosa.Std.Version(2),
},
})
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
{
Metadata: rosa.GetMetadata(5),
Version: rosa.Std.Version(5),
},
{
Metadata: rosa.GetMetadata(6),
Version: rosa.Std.Version(6),
},
{
Metadata: rosa.GetMetadata(7),
Version: rosa.Std.Version(7),
},
})
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
},
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
},
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
},
})
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
{
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
},
})
} }
+16 -18
View File
@@ -23,7 +23,7 @@ const (
// packageIndex refers to metadata by name and various sort orders. // packageIndex refers to metadata by name and various sort orders.
type packageIndex struct { type packageIndex struct {
sorts [sortOrderEnd + 1][]*metadata sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
names map[string]*metadata names map[string]*metadata
search searchCache search searchCache
// Taken from [rosa.Report] if available. // Taken from [rosa.Report] if available.
@@ -32,11 +32,11 @@ type packageIndex struct {
// metadata holds [rosa.Metadata] extended with additional information. // metadata holds [rosa.Metadata] extended with additional information.
type metadata struct { type metadata struct {
handle rosa.ArtifactH p rosa.PArtifact
*rosa.Metadata *rosa.Metadata
// Copied from [rosa.Metadata], [rosa.Unversioned] is equivalent to the zero // Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
// value. Otherwise, the zero value is invalid. // to the zero value. Otherwise, the zero value is invalid.
Version string `json:"version,omitempty"` Version string `json:"version,omitempty"`
// Output data size, available if present in report. // Output data size, available if present in report.
Size int64 `json:"size,omitempty"` Size int64 `json:"size,omitempty"`
@@ -56,17 +56,15 @@ func (index *packageIndex) populate(report *rosa.Report) (err error) {
index.handleAccess = report.HandleAccess index.handleAccess = report.HandleAccess
} }
handles := rosa.Native().Collect() var work [rosa.PresetUnexportedStart]*metadata
work := make([]*metadata, len(handles))
index.names = make(map[string]*metadata) index.names = make(map[string]*metadata)
ir := pkg.NewIR() ir := pkg.NewIR()
for i, handle := range handles { for p := range rosa.PresetUnexportedStart {
meta, a := rosa.Native().Std().MustLoad(handle)
m := metadata{ m := metadata{
handle: handle, p: p,
Metadata: meta, Metadata: rosa.GetMetadata(p),
Version: meta.Version, Version: rosa.Std.Version(p),
} }
if m.Version == "" { if m.Version == "" {
return errors.New("invalid version from " + m.Name) return errors.New("invalid version from " + m.Name)
@@ -76,32 +74,32 @@ func (index *packageIndex) populate(report *rosa.Report) (err error) {
} }
if report != nil { if report != nil {
id := ir.Ident(a) id := ir.Ident(rosa.Std.Load(p))
m.ids = pkg.Encode(id.Value()) m.ids = pkg.Encode(id.Value())
m.status, m.Size = report.ArtifactOf(id) m.status, m.Size = report.ArtifactOf(id)
m.HasReport = m.Size >= 0 m.HasReport = m.Size >= 0
} }
work[i] = &m work[p] = &m
index.names[m.Name] = &m index.names[m.Name] = &m
} }
index.sorts[declarationAscending] = work index.sorts[declarationAscending] = work
index.sorts[declarationDescending] = slices.Clone(work) index.sorts[declarationDescending] = work
slices.Reverse(index.sorts[declarationDescending][:]) slices.Reverse(index.sorts[declarationDescending][:])
index.sorts[nameAscending] = slices.Clone(work) index.sorts[nameAscending] = work
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int { slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
return strings.Compare(a.Name, b.Name) return strings.Compare(a.Name, b.Name)
}) })
index.sorts[nameDescending] = slices.Clone(index.sorts[nameAscending]) index.sorts[nameDescending] = index.sorts[nameAscending]
slices.Reverse(index.sorts[nameDescending][:]) slices.Reverse(index.sorts[nameDescending][:])
index.sorts[sizeAscending] = slices.Clone(work) index.sorts[sizeAscending] = work
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int { slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
return cmp.Compare(a.Size, b.Size) return cmp.Compare(a.Size, b.Size)
}) })
index.sorts[sizeDescending] = slices.Clone(index.sorts[sizeAscending]) index.sorts[sizeDescending] = index.sorts[sizeAscending]
slices.Reverse(index.sorts[sizeDescending][:]) slices.Reverse(index.sorts[sizeDescending][:])
return return
+1 -1
View File
@@ -74,7 +74,7 @@ func (s *searchCache) clean() {
} }
func indexsum(in [][]int) int { func indexsum(in [][]int) int {
sum := 0 sum := 0
for i := range in { for i := 0; i < len(in); i++ {
sum += in[i][1] - in[i][0] sum += in[i][1] - in[i][0]
} }
return sum return sum
+2 -3
View File
@@ -4,12 +4,11 @@
<meta charset="UTF-8"> <meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<link rel="stylesheet" href="style.css"> <link rel="stylesheet" href="style.css">
<link rel="icon" href="https://hakurei.app/favicon.ico"/> <title>Hakurei PkgServer</title>
<title>Rosa OS Packages</title>
<script src="index.js"></script> <script src="index.js"></script>
</head> </head>
<body> <body>
<h1>Rosa OS Packages</h1> <h1>Hakurei PkgServer</h1>
<div class="top-controls" id="top-controls-regular"> <div class="top-controls" id="top-controls-regular">
<p>Showing entries <span id="entry-counter"></span>.</p> <p>Showing entries <span id="entry-counter"></span>.</p>
<span id="search-bar"> <span id="search-bar">
+173 -305
View File
@@ -14,7 +14,6 @@ package main
import ( import (
"context" "context"
"crypto/ed25519"
"crypto/sha512" "crypto/sha512"
"errors" "errors"
"fmt" "fmt"
@@ -32,11 +31,12 @@ import (
"syscall" "syscall"
"time" "time"
"unique" "unique"
"unsafe"
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/command" "hakurei.app/command"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/container/seccomp"
"hakurei.app/container/std"
"hakurei.app/ext" "hakurei.app/ext"
"hakurei.app/fhs" "hakurei.app/fhs"
"hakurei.app/internal/pkg" "hakurei.app/internal/pkg"
@@ -47,19 +47,6 @@ import (
"hakurei.app/cmd/mbf/internal/pkgserver/ui" "hakurei.app/cmd/mbf/internal/pkgserver/ui"
) )
// writeFileExcl is like [os.WriteFile], but sets [os.O_EXCL] instead.
func writeFileExcl(name string, data []byte, perm os.FileMode) error {
f, err := os.OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if err != nil {
return err
}
_, err = f.Write(data)
if err1 := f.Close(); err1 != nil && err == nil {
err = err1
}
return err
}
func main() { func main() {
container.TryArgv0(nil) container.TryArgv0(nil)
@@ -71,20 +58,6 @@ func main() {
log.Fatal("this program must not run as root") log.Fatal("this program must not run as root")
} }
defer func() {
r := recover()
if r == nil {
return
}
switch r.(type) {
case rosa.LoadError, pkg.IRStringError:
log.Fatal(r)
default:
panic(r)
}
}()
ctx, stop := signal.NotifyContext(context.Background(), ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP) syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
defer stop() defer stop()
@@ -98,32 +71,18 @@ func main() {
flagArch string flagArch string
flagCheck bool flagCheck bool
flagLTO bool flagLTO bool
flagPT bool
flagDry bool
flagPath string
flagSourcePath string
flagCrossOverride int flagCrossOverride int
addr net.UnixAddr addr net.UnixAddr
) )
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error { c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
if !rosa.Native().HasStageEarly() {
return pkg.UnsupportedArchError(runtime.GOARCH)
}
if flagPT {
log.Println("parsed in", rosa.ParseTime())
}
msg.SwapVerbose(!flagQuiet) msg.SwapVerbose(!flagQuiet)
cm.ctx, cm.msg = ctx, msg cm.ctx, cm.msg = ctx, msg
cm.base = os.ExpandEnv(cm.base) cm.base = os.ExpandEnv(cm.base)
if cm.base == "" { if cm.base == "" {
cm.base = "cache" cm.base = "cache"
} }
cm.mirror = os.ExpandEnv(cm.mirror)
azaleaPath := os.ExpandEnv(flagPath)
addr.Net = "unix" addr.Net = "unix"
addr.Name = os.ExpandEnv(addr.Name) addr.Name = os.ExpandEnv(addr.Name)
@@ -138,10 +97,10 @@ func main() {
if !flagLTO { if !flagLTO {
flags |= rosa.OptLLVMNoLTO flags |= rosa.OptLLVMNoLTO
} }
rosa.Native().DropCaches("", flags) rosa.DropCaches("", flags)
cross := flagArch != "" && flagArch != runtime.GOARCH cross := flagArch != "" && flagArch != runtime.GOARCH
if flagQEMU || cross { if flagQEMU || cross {
_, cm.qemu = rosa.Native().Std().MustLoad(rosa.H("qemu")) cm.qemu = rosa.Std.Load(rosa.QEMU)
} }
if cross { if cross {
@@ -149,29 +108,12 @@ func main() {
flags = flagCrossOverride flags = flagCrossOverride
} }
rosa.Native().DropCaches(flagArch, flags) rosa.DropCaches(flagArch, flags)
if !rosa.Native().HasStageEarly() { if !rosa.HasStage0() {
return pkg.UnsupportedArchError(flagArch) return pkg.UnsupportedArchError(flagArch)
} }
} }
if flagSourcePath != "" {
if err := rosa.Native().SetSource(os.DirFS(flagSourcePath)); err != nil {
return err
}
}
if azaleaPath != "" {
var root *os.Root
if a, err := check.NewAbs(azaleaPath); err != nil {
return err
} else if root, err = os.OpenRoot(a.String()); err != nil {
return err
} else if err = rosa.Native().RegisterFS(root.FS()); err != nil {
return err
}
}
return nil return nil
}).Flag( }).Flag(
&flagQuiet, &flagQuiet,
@@ -213,10 +155,6 @@ func main() {
&cm.base, &cm.base,
"d", command.StringFlag("$MBF_CACHE_DIR"), "d", command.StringFlag("$MBF_CACHE_DIR"),
"Directory to store cured artifacts", "Directory to store cured artifacts",
).Flag(
&cm.mirror,
"r", command.StringFlag("$MBF_REMOTE"),
"URL of mirror service",
).Flag( ).Flag(
&cm.idle, &cm.idle,
"sched-idle", command.BoolFlag(false), "sched-idle", command.BoolFlag(false),
@@ -232,22 +170,6 @@ func main() {
&addr.Name, &addr.Name,
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"), "socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
"Pathname of socket to bind to", "Pathname of socket to bind to",
).Flag(
&flagPT,
"parse-time", command.BoolFlag(false),
"Print duration of the initial azalea parse",
).Flag(
&flagDry,
"dry", command.BoolFlag(false),
"Do not destroy cache entries",
).Flag(
&flagSourcePath,
"source", command.StringFlag(""),
"Override hakurei source tree",
).Flag(
&flagPath,
"p", command.StringFlag("$AZALEA_PATH"),
"Load additional azalea files",
) )
c.NewCommand( c.NewCommand(
@@ -400,10 +322,7 @@ func main() {
) )
{ {
var ( var flagJobs int
flagJobs int
flagNoBlock bool
)
c.NewCommand("updates", command.UsageInternal, func([]string) error { c.NewCommand("updates", command.UsageInternal, func([]string) error {
var ( var (
errsMu sync.Mutex errsMu sync.Mutex
@@ -412,21 +331,16 @@ func main() {
n atomic.Uint64 n atomic.Uint64
) )
w := make(chan rosa.ArtifactH) w := make(chan rosa.PArtifact)
var wg sync.WaitGroup var wg sync.WaitGroup
for range max(flagJobs, 1) { for range max(flagJobs, 1) {
wg.Go(func() { wg.Go(func() {
for p := range w { for p := range w {
meta, _ := rosa.Native().Std().MustLoad(p) meta := rosa.GetMetadata(p)
if meta.ID == 0 { if meta.ID == 0 {
continue continue
} }
if !flagNoBlock && meta.Blocked != "" {
msg.Verbosef("%s is blocked: %s", meta.Name, meta.Blocked)
continue
}
v, err := meta.GetVersions(ctx) v, err := meta.GetVersions(ctx)
if err != nil { if err != nil {
errsMu.Lock() errsMu.Lock()
@@ -435,9 +349,12 @@ func main() {
continue continue
} }
if latest := meta.GetLatest(v); meta.Version != latest { if current, latest :=
rosa.Std.Version(p),
meta.GetLatest(v); current != latest {
n.Add(1) n.Add(1)
log.Printf("%s %s < %s", meta.Name, meta.Version, latest) log.Printf("%s %s < %s", meta.Name, current, latest)
continue continue
} }
@@ -447,9 +364,9 @@ func main() {
} }
done: done:
for _, p := range rosa.Native().CollectAll() { for i := range rosa.PresetEnd {
select { select {
case w <- p: case w <- rosa.PArtifact(i):
break break
case <-ctx.Done(): case <-ctx.Done():
break done break done
@@ -467,23 +384,9 @@ func main() {
&flagJobs, &flagJobs,
"j", command.IntFlag(32), "j", command.IntFlag(32),
"Maximum number of simultaneous connections", "Maximum number of simultaneous connections",
).Flag(
&flagNoBlock,
"ignore-block", command.BoolFlag(false),
"Inhibit update blocking",
) )
} }
c.NewCommand("blocked", command.UsageInternal, func([]string) error {
for _, p := range rosa.Native().CollectAll() {
meta, _ := rosa.Native().Std().Load(p)
if meta.Blocked != "" {
fmt.Printf("%s: %s\n", meta.Name, meta.Blocked)
}
}
return nil
})
c.NewCommand( c.NewCommand(
"daemon", "daemon",
"Service artifact IR with Rosa OS extensions", "Service artifact IR with Rosa OS extensions",
@@ -497,82 +400,20 @@ func main() {
}, },
) )
c.NewCommand(
"keygen",
"Create keypair for local cache",
func([]string) error {
pub, priv, err := ed25519.GenerateKey(nil)
if err != nil {
return err
}
return errors.Join(writeFileExcl(filepath.Join(
cm.base,
"ed25519.pub",
), pub, 0444), writeFileExcl(filepath.Join(
cm.base,
"ed25519",
), priv, 0400))
},
)
c.NewCommand(
"serve",
"Export local cache as mirror",
func(args []string) error {
const shutdownTimeout = 15 * time.Second
if len(args) != 1 {
return errors.New("serve requires 1 argument")
}
var key ed25519.PrivateKey
if p, err := os.ReadFile(filepath.Join(cm.base, "ed25519")); err != nil {
return err
} else if len(p) != ed25519.PrivateKeySize {
return errors.New("invalid private key")
} else {
key = p
}
var h http.Handler
if base, err := os.OpenRoot(cm.base); err != nil {
return err
} else {
h = rosa.NewMirror(msg, base.FS(), key)
}
server := http.Server{Addr: args[0], Handler: h}
go func() {
<-ctx.Done()
cc, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
defer cancel()
if err := server.Shutdown(cc); err != nil {
log.Fatal(err)
}
}()
msg.Verbosef("listening on %q", args[0])
err := server.ListenAndServe()
if errors.Is(err, http.ErrServerClosed) {
err = nil
}
return err
},
)
{ {
var ( var (
flagGentoo string flagGentoo string
flagChecksum string flagChecksum string
flagStage0 bool
) )
c.NewCommand( c.NewCommand(
"stage3", "stage3",
"Check for toolchain 3-stage non-determinism", "Check for toolchain 3-stage non-determinism",
func(args []string) (err error) { func(args []string) (err error) {
s := rosa.Std t := rosa.Std
if flagGentoo != "" { if flagGentoo != "" {
s -= 3 // magic number to discourage misuse t -= 3 // magic number to discourage misuse
var checksum pkg.Checksum var checksum pkg.Checksum
if len(flagChecksum) != 0 { if len(flagChecksum) != 0 {
@@ -580,7 +421,7 @@ func main() {
return return
} }
} }
rosa.Native().SetGentooStage3(flagGentoo, checksum) rosa.SetGentooStage3(flagGentoo, checksum)
} }
var ( var (
@@ -588,10 +429,10 @@ func main() {
checksum [2]unique.Handle[pkg.Checksum] checksum [2]unique.Handle[pkg.Checksum]
) )
_llvm := rosa.H("llvm")
if err = cm.Do(func(cache *pkg.Cache) (err error) { if err = cm.Do(func(cache *pkg.Cache) (err error) {
_, llvm := rosa.Native().New(s - 2).Load(_llvm) pathname, _, err = cache.Cure(
pathname, _, err = cache.Cure(llvm) (t - 2).Load(rosa.LLVM),
)
return return
}); err != nil { }); err != nil {
return return
@@ -599,16 +440,18 @@ func main() {
log.Println("stage1:", pathname) log.Println("stage1:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) { if err = cm.Do(func(cache *pkg.Cache) (err error) {
_, llvm := rosa.Native().New(s - 1).Load(_llvm) pathname, checksum[0], err = cache.Cure(
pathname, checksum[0], err = cache.Cure(llvm) (t - 1).Load(rosa.LLVM),
)
return return
}); err != nil { }); err != nil {
return return
} }
log.Println("stage2:", pathname) log.Println("stage2:", pathname)
if err = cm.Do(func(cache *pkg.Cache) (err error) { if err = cm.Do(func(cache *pkg.Cache) (err error) {
_, llvm := rosa.Native().New(s).Load(_llvm) pathname, checksum[1], err = cache.Cure(
pathname, checksum[1], err = cache.Cure(llvm) t.Load(rosa.LLVM),
)
return return
}); err != nil { }); err != nil {
return return
@@ -626,6 +469,19 @@ func main() {
"("+pkg.Encode(checksum[0].Value())+")", "("+pkg.Encode(checksum[0].Value())+")",
) )
} }
if flagStage0 {
if err = cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(
t.Load(rosa.Stage0),
)
return
}); err != nil {
return
}
log.Println(pathname)
}
return return
}, },
).Flag( ).Flag(
@@ -636,6 +492,10 @@ func main() {
&flagChecksum, &flagChecksum,
"checksum", command.StringFlag(""), "checksum", command.StringFlag(""),
"Checksum of Gentoo stage3 tarball", "Checksum of Gentoo stage3 tarball",
).Flag(
&flagStage0,
"stage0", command.BoolFlag(false),
"Create bootstrap stage0 tarball",
) )
} }
@@ -659,6 +519,10 @@ func main() {
if len(args) != 1 { if len(args) != 1 {
return errors.New("cure requires 1 argument") return errors.New("cure requires 1 argument")
} }
p, ok := rosa.ResolveName(args[0])
if !ok {
return fmt.Errorf("unknown artifact %q", args[0])
}
t := rosa.Std t := rosa.Std
if flagBoot { if flagBoot {
@@ -667,16 +531,11 @@ func main() {
t -= 1 t -= 1
} }
_, a := rosa.Native().New(t).Load(rosa.ArtifactH(unique.Make(args[0])))
if a == nil {
return fmt.Errorf("unknown artifact %q", args[0])
}
switch { switch {
default: default:
var pathname *check.Absolute var pathname *check.Absolute
err := cm.Do(func(cache *pkg.Cache) (err error) { err := cm.Do(func(cache *pkg.Cache) (err error) {
pathname, _, err = cache.Cure(a) pathname, _, err = cache.Cure(t.Load(p))
return return
}) })
if err != nil { if err != nil {
@@ -694,7 +553,7 @@ func main() {
0400, 0400,
); err != nil { ); err != nil {
return err return err
} else if err = pkg.Write( } else if _, err = pkg.Flatten(
os.DirFS(pathname.String()), os.DirFS(pathname.String()),
".", ".",
f, f,
@@ -718,7 +577,7 @@ func main() {
return err return err
} }
if err = pkg.NewIR().EncodeAll(f, a); err != nil { if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
_ = f.Close() _ = f.Close()
return err return err
} }
@@ -729,8 +588,8 @@ func main() {
return cm.Do(func(cache *pkg.Cache) error { return cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec( return cache.EnterExec(
ctx, ctx,
a, t.Load(p),
"", true, os.Stdin, os.Stdout, os.Stderr, true, os.Stdin, os.Stdout, os.Stderr,
rosa.AbsSystem.Append("bin", "mksh"), rosa.AbsSystem.Append("bin", "mksh"),
"sh", "sh",
) )
@@ -741,6 +600,7 @@ func main() {
if flagNoReply { if flagNoReply {
flags |= remoteNoReply flags |= remoteNoReply
} }
a := t.Load(p)
pathname, err := cureRemote(ctx, &addr, a, flags) pathname, err := cureRemote(ctx, &addr, a, flags)
if !flagNoReply && err == nil { if !flagNoReply && err == nil {
log.Println(pathname) log.Println(pathname)
@@ -760,7 +620,7 @@ func main() {
case flagFaults: case flagFaults:
var faults []pkg.Fault var faults []pkg.Fault
if err := cm.Do(func(cache *pkg.Cache) (err error) { if err := cm.Do(func(cache *pkg.Cache) (err error) {
faults, err = cache.ReadFaults(a) faults, err = cache.ReadFaults(t.Load(p))
return return
}); err != nil { }); err != nil {
return err return err
@@ -774,7 +634,7 @@ func main() {
case flagPop: case flagPop:
var faults []pkg.Fault var faults []pkg.Fault
if err := cm.Do(func(cache *pkg.Cache) (err error) { if err := cm.Do(func(cache *pkg.Cache) (err error) {
faults, err = cache.ReadFaults(a) faults, err = cache.ReadFaults(t.Load(p))
return return
}); err != nil { }); err != nil {
return err return err
@@ -840,9 +700,8 @@ func main() {
) )
} }
cleanC := c.New("clean", "Remove unused entries from the cache") c.NewCommand(
cleanC.NewCommand( "clear",
"fault",
"Remove all fault entries from the cache", "Remove all fault entries from the cache",
func([]string) error { func([]string) error {
return cm.Do(func(*pkg.Cache) error { return cm.Do(func(*pkg.Cache) error {
@@ -863,58 +722,6 @@ func main() {
}) })
}, },
) )
cleanC.NewCommand(
"checksum",
"Remove unreachable checksum entries",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
_, checksums, err := cache.Clean(flagDry, false)
log.Printf("destroyed %d entries", len(checksums))
return err
})
},
)
{
var flagDeep bool
cleanC.NewCommand(
"all",
"Remove identifiers not reachable by loaded packages",
func([]string) error {
return cm.Do(func(cache *pkg.Cache) error {
t := rosa.Native().Clone().Std()
handles := t.CollectAll()
flags := t.Flags()
a := t.Append(nil, handles...)
for arch := range rosa.Arches(nil) {
if arch == runtime.GOARCH {
continue
}
t.DropCaches(arch, rosa.OptLLVMNoLTO|rosa.OptSkipCheck)
a = t.Append(a, handles...)
t.DropCaches(arch, flags)
a = t.Append(a, handles...)
}
ids, checksums, err := cache.Clean(
flagDry,
!flagDeep,
a...,
)
log.Printf(
"destroyed %d identifier and %d checksum entries",
len(ids), len(checksums),
)
return err
})
},
).Flag(
&flagDeep,
"deep", command.BoolFlag(false),
"Include transitive inputs",
)
}
c.NewCommand( c.NewCommand(
"abort", "abort",
@@ -933,72 +740,132 @@ func main() {
"shell", "shell",
"Interactive shell in the specified Rosa OS environment", "Interactive shell in the specified Rosa OS environment",
func(args []string) error { func(args []string) error {
resolvconf := "nameserver 1.1.1.1\nnameserver 1.0.0.1\n" presets := make([]rosa.PArtifact, len(args)+3)
if p, err := os.ReadFile(fhs.AbsEtc.Append(
"resolv.conf",
).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) {
return err
}
} else {
resolvconf = unsafe.String(unsafe.SliceData(p), len(p))
}
handles := make([]rosa.ArtifactH, len(args), len(args)+3)
for i, arg := range args { for i, arg := range args {
handles[i] = rosa.ArtifactH(unique.Make(arg)) p, ok := rosa.ResolveName(arg)
if meta, _ := rosa.Native().Std().Load(handles[i]); meta == nil { if !ok {
return fmt.Errorf("unknown artifact %q", arg) return fmt.Errorf("unknown artifact %q", arg)
} }
presets[i] = p
} }
base := rosa.H("llvm") base := rosa.LLVM
if !flagWithToolchain { if !flagWithToolchain {
base = rosa.H("musl") base = rosa.Musl
} }
handles = append(handles, presets = append(presets,
base, base,
rosa.H("mksh"), rosa.Mksh,
rosa.H("toybox"), rosa.Toybox,
) )
root := make(pkg.Collect, 0, 6+len(args)) root := make(pkg.Collect, 0, 6+len(args))
root = append(root, rosa.NewEtc(false)) root = rosa.Std.AppendPresets(root, presets...)
root = rosa.Native().Std().Append(root, handles...)
return cm.Do(func(cache *pkg.Cache) error { if err := cm.Do(func(cache *pkg.Cache) error {
return cache.EnterExec( _, _, err := cache.Cure(&root)
ctx, return err
pkg.NewExec( }); err == nil {
"", return errors.New("unreachable")
rosa.Native().Arch(), } else if !pkg.IsCollected(err) {
new(pkg.Checksum), return err
1, }
flagNet,
false, type cureRes struct {
fhs.AbsRoot, pathname *check.Absolute
[]string{ checksum unique.Handle[pkg.Checksum]
}
cured := make(map[pkg.Artifact]cureRes)
for _, a := range root {
if err := cm.Do(func(cache *pkg.Cache) error {
pathname, checksum, err := cache.Cure(a)
if err == nil {
cured[a] = cureRes{pathname, checksum}
}
return err
}); err != nil {
return err
}
}
// explicitly open for direct error-free use from this point
if cm.c == nil {
if err := cm.open(); err != nil {
return err
}
}
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
*check.Absolute,
unique.Handle[pkg.Checksum],
) {
res := cured[a]
return res.pathname, res.checksum
}, func(i int, d pkg.Artifact) {
r := pkg.Encode(cm.c.Ident(d).Value())
if s, ok := d.(fmt.Stringer); ok {
if name := s.String(); name != "" {
r += "-" + name
}
}
msg.Verbosef("promoted layer %d as %s", i, r)
})
z := container.New(ctx, msg)
z.WaitDelay = 3 * time.Second
z.SeccompPresets = pkg.SeccompPresets
z.SeccompFlags |= seccomp.AllowMultiarch
z.ParentPerm = 0700
z.HostNet = flagNet
z.RetainSession = flagSession
z.Hostname = "localhost"
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
z.Stdin, z.Stdout, z.Stderr = os.Stdin, os.Stdout, os.Stderr
z.Quiet = !cm.verboseInit
if s, ok := os.LookupEnv("TERM"); ok {
z.Env = append(z.Env, "TERM="+s)
}
var tempdir *check.Absolute
if s, err := filepath.Abs(os.TempDir()); err != nil {
return err
} else if tempdir, err = check.NewAbs(s); err != nil {
return err
}
z.Dir = fhs.AbsRoot
z.Env = []string{
"SHELL=/system/bin/mksh", "SHELL=/system/bin/mksh",
"PATH=/system/bin", "PATH=/system/bin",
"HOME=/", "HOME=/",
}, }
fhs.AbsProc.Append("nonexistent"), z.Path = rosa.AbsSystem.Append("bin", "mksh")
nil, z.Args = []string{"mksh"}
pkg.Path(fhs.AbsRoot, true, root...), z.
pkg.Path( OverlayEphemeral(fhs.AbsRoot, layers...).
fhs.AbsEtc.Append("resolv.conf"), false, Place(
pkg.NewFile( fhs.AbsEtc.Append("hosts"),
"resolv.conf", []byte("127.0.0.1 localhost\n"),
unsafe.Slice(unsafe.StringData(resolvconf), len(resolvconf)), ).
), Place(
), fhs.AbsEtc.Append("passwd"),
), []byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
"localhost", "nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
flagSession, os.Stdin, os.Stdout, os.Stderr, ).
rosa.AbsSystem.Append("bin", "mksh"), Place(
"sh", fhs.AbsEtc.Append("group"),
) []byte("media_rw:x:1023:\nnobody:x:65534:\n"),
}) ).
Bind(tempdir, fhs.AbsTmp, std.BindWritable).
Proc(fhs.AbsProc).Dev(fhs.AbsDev, true)
if err := z.Start(); err != nil {
return err
}
if err := z.Serve(); err != nil {
return err
}
return z.Wait()
}, },
).Flag( ).Flag(
&flagNet, &flagNet,
@@ -1013,6 +880,7 @@ func main() {
"with-toolchain", command.BoolFlag(false), "with-toolchain", command.BoolFlag(false),
"Include the stage2 LLVM toolchain", "Include the stage2 LLVM toolchain",
) )
} }
c.Command( c.Command(
+5 -5
View File
@@ -9,7 +9,7 @@ import (
) )
func TestMain(m *testing.M) { func TestMain(m *testing.M) {
rosa.Native().DropCaches("", rosa.OptLLVMNoLTO) rosa.DropCaches("", rosa.OptLLVMNoLTO)
os.Exit(m.Run()) os.Exit(m.Run())
} }
@@ -35,10 +35,10 @@ func TestCureAll(t *testing.T) {
} }
}) })
for _, handle := range rosa.Native().Collect() { for i := range rosa.PresetEnd {
_, a := rosa.Native().Std().MustLoad(handle) p := rosa.PArtifact(i)
t.Run(handle.String(), func(t *testing.T) { t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
_, err := cureRemote(t.Context(), &addr, a, 0) _, err := cureRemote(t.Context(), &addr, rosa.Std.Load(p), 0)
if err != nil { if err != nil {
t.Error(err) t.Error(err)
} }
+2 -2
View File
@@ -508,8 +508,8 @@ func _main(s ...string) (exitCode int) {
if !z.AllowOrphan { if !z.AllowOrphan {
if err := z.Wait(); err != nil { if err := z.Wait(); err != nil {
exitError, ok := errors.AsType[*exec.ExitError](err) var exitError *exec.ExitError
if !ok || exitError == nil { if !errors.As(err, &exitError) || exitError == nil {
log.Println(err) log.Println(err)
return 5 return 5
} }
+2 -2
View File
@@ -91,8 +91,8 @@ func (n *node) MustParse(arguments []string, handleError func(error)) {
case ErrEmptyTree: case ErrEmptyTree:
os.Exit(1) os.Exit(1)
default: default:
flagError, ok := errors.AsType[FlagError](err) var flagError FlagError
if !ok { // returned by HandlerFunc if !errors.As(err, &flagError) { // returned by HandlerFunc
handleError(err) handleError(err)
os.Exit(1) os.Exit(1)
} }
+5 -2
View File
@@ -154,8 +154,11 @@ func (e *StartError) Error() string {
return e.Step return e.Step
} }
if se, ok := errors.AsType[*os.SyscallError](e.Err); ok && se != nil { {
return e.Step + " " + se.Error() var syscallError *os.SyscallError
if errors.As(e.Err, &syscallError) && syscallError != nil {
return e.Step + " " + syscallError.Error()
}
} }
return e.Step + ": " + e.Err.Error() return e.Step + ": " + e.Err.Error()
+2
View File
@@ -235,6 +235,8 @@ func checkOpBehaviour(t *testing.T, testCases []opBehaviourTestCase) {
}) })
} }
func sliceAddr[S any](s []S) *[]S { return &s }
func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile { func newCheckedFile(t *testing.T, name, wantData string, closeErr error) osFile {
f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr} f := &checkedOsFile{t: t, name: name, want: wantData, closeErr: closeErr}
// check happens in Close, and cleanup is not guaranteed to run, so relying // check happens in Close, and cleanup is not guaranteed to run, so relying
+8 -6
View File
@@ -46,8 +46,9 @@ func messageFromError(err error) (m string, ok bool) {
// While this is usable for pointer errors, such use should be avoided as nil // While this is usable for pointer errors, such use should be avoided as nil
// check is omitted. // check is omitted.
func messagePrefix[T error](prefix string, err error) (string, bool) { func messagePrefix[T error](prefix string, err error) (string, bool) {
if e, ok := errors.AsType[T](err); ok { var targetError T
return prefix + e.Error(), true if errors.As(err, &targetError) {
return prefix + targetError.Error(), true
} }
return zeroString, false return zeroString, false
} }
@@ -57,8 +58,9 @@ func messagePrefixP[V any, T interface {
*V *V
error error
}](prefix string, err error) (string, bool) { }](prefix string, err error) (string, bool) {
if e, ok := errors.AsType[T](err); ok && e != nil { var targetError T
return prefix + e.Error(), true if errors.As(err, &targetError) && targetError != nil {
return prefix + targetError.Error(), true
} }
return zeroString, false return zeroString, false
} }
@@ -107,8 +109,8 @@ func optionalErrorUnwrap(err error) error {
// errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback. // errnoFallback returns the concrete errno from an error, or a [os.PathError] fallback.
func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) { func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
errno, ok := errors.AsType[syscall.Errno](err) var errno syscall.Errno
if !ok { if !errors.As(err, &errno) {
return 0, &os.PathError{Op: op, Path: path, Err: err} return 0, &os.PathError{Op: op, Path: path, Err: err}
} }
return errno, nil return errno, nil
+8 -8
View File
@@ -95,7 +95,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -123,7 +123,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -152,7 +152,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -182,7 +182,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -213,7 +213,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -245,7 +245,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -279,7 +279,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
@@ -315,7 +315,7 @@ func TestInitEntrypoint(t *testing.T) {
Uid: 1 << 16, Uid: 1 << 16,
Gid: 1 << 15, Gid: 1 << 15,
Hostname: "hakurei-check", Hostname: "hakurei-check",
Ops: new(make(Ops, 1)), Ops: (*Ops)(sliceAddr(make(Ops, 1))),
SeccompRules: make([]std.NativeRule, 0), SeccompRules: make([]std.NativeRule, 0),
SeccompPresets: std.PresetStrict, SeccompPresets: std.PresetStrict,
RetainSession: true, RetainSession: true,
+1 -1
View File
@@ -39,7 +39,7 @@ func TestSyscall(t *testing.T) {
t.Errorf("Unmarshal: %v, want %v", got, tc.want) t.Errorf("Unmarshal: %v, want %v", got, tc.want)
} }
}) })
if _, ok := errors.AsType[ext.SyscallNameError](tc.err); ok { if errors.As(tc.err, new(ext.SyscallNameError)) {
return return
} }
Generated
+8 -8
View File
@@ -7,32 +7,32 @@
] ]
}, },
"locked": { "locked": {
"lastModified": 1780361225, "lastModified": 1772985280,
"narHash": "sha256-wnV9ttf4fPWNonBIQmvlrSlNpQYgx5HgWWd007mwIFA=", "narHash": "sha256-FdrNykOoY9VStevU4zjSUdvsL9SzJTcXt4omdEDZDLk=",
"owner": "nix-community", "owner": "nix-community",
"repo": "home-manager", "repo": "home-manager",
"rev": "e28654b71096e08c019d4861ca26acb646f583d8", "rev": "8f736f007139d7f70752657dff6a401a585d6cbc",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nix-community", "owner": "nix-community",
"ref": "release-26.05", "ref": "release-25.11",
"repo": "home-manager", "repo": "home-manager",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1780453794, "lastModified": 1772822230,
"narHash": "sha256-bXMRa9VTsHSPXL4Cw8R6JJLQeY3Y/IP4+YJCYVmQ7FY=", "narHash": "sha256-yf3iYLGbGVlIthlQIk5/4/EQDZNNEmuqKZkQssMljuw=",
"owner": "NixOS", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "6b316287bae2ee04c9b93c8c858d930fd07d7338", "rev": "71caefce12ba78d84fe618cf61644dce01cf3a96",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "NixOS", "owner": "NixOS",
"ref": "nixos-26.05", "ref": "nixos-25.11",
"repo": "nixpkgs", "repo": "nixpkgs",
"type": "github" "type": "github"
} }
+5 -4
View File
@@ -2,10 +2,10 @@
description = "hakurei container tool and nixos module"; description = "hakurei container tool and nixos module";
inputs = { inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-26.05"; nixpkgs.url = "github:NixOS/nixpkgs/nixos-25.11";
home-manager = { home-manager = {
url = "github:nix-community/home-manager/release-26.05"; url = "github:nix-community/home-manager/release-25.11";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
}; };
@@ -37,7 +37,7 @@
inherit (pkgs) inherit (pkgs)
runCommandLocal runCommandLocal
callPackage callPackage
nixfmt nixfmt-rfc-style
deadnix deadnix
statix statix
; ;
@@ -57,7 +57,7 @@
sharefs = callPackage ./cmd/sharefs/test { inherit system self; }; sharefs = callPackage ./cmd/sharefs/test { inherit system self; };
formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt ]; } '' formatting = runCommandLocal "check-formatting" { nativeBuildInputs = [ nixfmt-rfc-style ]; } ''
cd ${./.} cd ${./.}
echo "running nixfmt..." echo "running nixfmt..."
@@ -139,6 +139,7 @@
GOCACHE="$(mktemp -d)" \ GOCACHE="$(mktemp -d)" \
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \ PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
DESTDIR="$out" \ DESTDIR="$out" \
HAKUREI_VERSION="v${hakurei.version}" \
./all.sh ./all.sh
''; '';
} }
-15
View File
@@ -2,7 +2,6 @@ package hst
import ( import (
"encoding/json" "encoding/json"
"fmt"
"strings" "strings"
"syscall" "syscall"
"time" "time"
@@ -69,8 +68,6 @@ const (
// FDevice mount /dev/ from the init mount namespace as is in the container // FDevice mount /dev/ from the init mount namespace as is in the container
// mount namespace. // mount namespace.
FDevice FDevice
// FCoverRun covers /run/ in the container mount namespace early.
FCoverRun
// FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity. // FShareRuntime shares XDG_RUNTIME_DIR between containers under the same identity.
FShareRuntime FShareRuntime
@@ -103,8 +100,6 @@ func (flags Flags) String() string {
return "mapuid" return "mapuid"
case FDevice: case FDevice:
return "device" return "device"
case FCoverRun:
return "cover_run"
case FShareRuntime: case FShareRuntime:
return "runtime" return "runtime"
case FShareTmpdir: case FShareTmpdir:
@@ -166,10 +161,6 @@ type ContainerConfig struct {
Flags Flags `json:"-"` Flags Flags `json:"-"`
} }
func (c *ContainerConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// ContainerConfigF is [ContainerConfig] stripped of its methods. // ContainerConfigF is [ContainerConfig] stripped of its methods.
// //
// The [ContainerConfig.Flags] field does not survive a [json] round trip. // The [ContainerConfig.Flags] field does not survive a [json] round trip.
@@ -200,8 +191,6 @@ type containerConfigJSON = struct {
// Corresponds to [FDevice]. // Corresponds to [FDevice].
Device bool `json:"device,omitempty"` Device bool `json:"device,omitempty"`
// Corresponds to [FCoverRun].
CoverRun bool `json:"cover_run,omitempty"`
// Corresponds to [FShareRuntime]. // Corresponds to [FShareRuntime].
ShareRuntime bool `json:"share_runtime,omitempty"` ShareRuntime bool `json:"share_runtime,omitempty"`
@@ -225,7 +214,6 @@ func (c *ContainerConfig) MarshalJSON() ([]byte, error) {
Multiarch: c.Flags&FMultiarch != 0, Multiarch: c.Flags&FMultiarch != 0,
MapRealUID: c.Flags&FMapRealUID != 0, MapRealUID: c.Flags&FMapRealUID != 0,
Device: c.Flags&FDevice != 0, Device: c.Flags&FDevice != 0,
CoverRun: c.Flags&FCoverRun != 0,
ShareRuntime: c.Flags&FShareRuntime != 0, ShareRuntime: c.Flags&FShareRuntime != 0,
ShareTmpdir: c.Flags&FShareTmpdir != 0, ShareTmpdir: c.Flags&FShareTmpdir != 0,
}) })
@@ -269,9 +257,6 @@ func (c *ContainerConfig) UnmarshalJSON(data []byte) error {
if v.Device { if v.Device {
c.Flags |= FDevice c.Flags |= FDevice
} }
if v.CoverRun {
c.Flags |= FCoverRun
}
if v.ShareRuntime { if v.ShareRuntime {
c.Flags |= FShareRuntime c.Flags |= FShareRuntime
} }
+3 -3
View File
@@ -21,8 +21,8 @@ func TestFlagsString(t *testing.T) {
}{ }{
{"none", 0, "none"}, {"none", 0, "none"},
{"none high", hst.FAll + 1, "none"}, {"none high", hst.FAll + 1, "none"},
{"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir"}, {"all", hst.FAll, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
{"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, cover_run, runtime, tmpdir"}, {"all high", math.MaxUint, "multiarch, compat, devel, userns, net, abstract, tty, mapuid, device, runtime, tmpdir"},
} }
for _, tc := range testCases { for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) { t.Run(tc.name, func(t *testing.T) {
@@ -53,7 +53,7 @@ func TestContainerConfig(t *testing.T) {
{"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID}, {"hostnet hostabstract mapuid", &hst.ContainerConfig{Flags: hst.FHostNet | hst.FHostAbstract | hst.FMapRealUID},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`}, `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"host_net":true,"host_abstract":true,"map_real_uid":true}`},
{"all", &hst.ContainerConfig{Flags: hst.FAll}, {"all", &hst.ContainerConfig{Flags: hst.FAll},
`{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true,"cover_run":true,"share_runtime":true,"share_tmpdir":true}`}, `{"env":null,"filesystem":null,"shell":null,"home":null,"args":null,"seccomp_compat":true,"devel":true,"userns":true,"host_net":true,"host_abstract":true,"tty":true,"multiarch":true,"map_real_uid":true,"device":true,"share_runtime":true,"share_tmpdir":true}`},
} }
for _, tc := range testCases { for _, tc := range testCases {
-5
View File
@@ -1,7 +1,6 @@
package hst package hst
import ( import (
"fmt"
"strconv" "strconv"
"strings" "strings"
) )
@@ -62,10 +61,6 @@ type BusConfig struct {
Filter bool `json:"filter"` Filter bool `json:"filter"`
} }
func (c *BusConfig) GoString() string {
return fmt.Sprintf("&%#v", *c)
}
// Interfaces iterates over all interface strings specified in [BusConfig]. // Interfaces iterates over all interface strings specified in [BusConfig].
func (c *BusConfig) Interfaces(yield func(string) bool) { func (c *BusConfig) Interfaces(yield func(string) bool) {
if c == nil { if c == nil {
+6 -9
View File
@@ -6,7 +6,6 @@ import (
"fmt" "fmt"
"os" "os"
"reflect" "reflect"
"strings"
"hakurei.app/check" "hakurei.app/check"
) )
@@ -37,8 +36,6 @@ type Ops interface {
Bind(source, target *check.Absolute, flags int) Ops Bind(source, target *check.Absolute, flags int) Ops
// Overlay appends an op that mounts the overlay pseudo filesystem. // Overlay appends an op that mounts the overlay pseudo filesystem.
Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops Overlay(target, state, work *check.Absolute, layers ...*check.Absolute) Ops
// OverlayEphemeral appends a MountOverlayOp with an ephemeral upperdir and workdir.
OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) Ops
// OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly. // OverlayReadonly appends an op that mounts the overlay pseudo filesystem readonly.
OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) Ops
@@ -81,17 +78,17 @@ type FSImplError struct{ Value FilesystemConfig }
func (f FSImplError) Error() string { func (f FSImplError) Error() string {
implType := reflect.TypeOf(f.Value) implType := reflect.TypeOf(f.Value)
var buf strings.Builder var name string
for implType != nil && implType.Kind() == reflect.Pointer { for implType != nil && implType.Kind() == reflect.Ptr {
buf.WriteByte('*') name += "*"
implType = implType.Elem() implType = implType.Elem()
} }
if implType != nil { if implType != nil {
buf.WriteString(implType.Name()) name += implType.Name()
} else { } else {
buf.WriteString("nil") name += "nil"
} }
return "implementation " + buf.String() + " not supported" return fmt.Sprintf("implementation %s not supported", name)
} }
// FilesystemConfigJSON is the [json] adapter for [FilesystemConfig]. // FilesystemConfigJSON is the [json] adapter for [FilesystemConfig].
+4 -9
View File
@@ -3,7 +3,6 @@ package hst_test
import ( import (
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"os" "os"
"reflect" "reflect"
"strings" "strings"
@@ -104,7 +103,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("marshal", func(t *testing.T) { t.Run("marshal", func(t *testing.T) {
t.Parallel() t.Parallel()
wantErr := tc.wantErr wantErr := tc.wantErr
if _, ok := errors.AsType[hst.FSTypeError](wantErr); ok { if errors.As(wantErr, new(hst.FSTypeError)) {
// for unsupported implementation tc // for unsupported implementation tc
wantErr = hst.FSImplError{Value: stubFS{"cat"}} wantErr = hst.FSImplError{Value: stubFS{"cat"}}
} }
@@ -140,7 +139,7 @@ func TestFilesystemConfigJSON(t *testing.T) {
t.Run("unmarshal", func(t *testing.T) { t.Run("unmarshal", func(t *testing.T) {
t.Parallel() t.Parallel()
if tc.data == "\x00" && tc.sData == "\x00" { if tc.data == "\x00" && tc.sData == "\x00" {
if _, ok := errors.AsType[hst.FSImplError](tc.wantErr); ok { if errors.As(tc.wantErr, new(hst.FSImplError)) {
// this error is only returned on marshal // this error is only returned on marshal
return return
} }
@@ -284,11 +283,11 @@ func checkFs(t *testing.T, testCases []fsTestCase) {
if !reflect.DeepEqual(ops, &tc.ops) { if !reflect.DeepEqual(ops, &tc.ops) {
gotString := new(strings.Builder) gotString := new(strings.Builder)
for _, op := range *ops { for _, op := range *ops {
gotString.WriteString("\n" + fmt.Sprintf("%#v", op)) gotString.WriteString("\n" + op.String())
} }
wantString := new(strings.Builder) wantString := new(strings.Builder)
for _, op := range tc.ops { for _, op := range tc.ops {
wantString.WriteString("\n" + fmt.Sprintf("%#v", op)) wantString.WriteString("\n" + op.String())
} }
t.Errorf("Apply: %s, want %s", gotString, wantString) t.Errorf("Apply: %s, want %s", gotString, wantString)
} }
@@ -340,10 +339,6 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)} return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
} }
func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops { func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)} return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
} }
+6 -1
View File
@@ -43,13 +43,18 @@ func (e *FSEphemeral) Apply(z *ApplyState) {
return return
} }
size := e.Size
if size < 0 {
size = 0
}
perm := e.Perm perm := e.Perm
if perm == 0 { if perm == 0 {
perm = fsEphemeralDefaultPerm perm = fsEphemeralDefaultPerm
} }
if e.Write { if e.Write {
z.Tmpfs(e.Target, max(e.Size, 0), perm) z.Tmpfs(e.Target, size, perm)
} else { } else {
z.Readonly(e.Target, perm) z.Readonly(e.Target, perm)
} }
+6 -24
View File
@@ -2,7 +2,6 @@ package hst
import ( import (
"encoding/gob" "encoding/gob"
"slices"
"strings" "strings"
"hakurei.app/check" "hakurei.app/check"
@@ -41,7 +40,7 @@ func (o *FSOverlay) Valid() bool {
} }
if o.Upper != nil { // rw if o.Upper != nil { // rw
return o.Work != nil || len(o.Lower) > 0 return o.Work != nil && len(o.Lower) > 0
} else { // ro } else { // ro
return len(o.Lower) >= 2 return len(o.Lower) >= 2
} }
@@ -59,11 +58,8 @@ func (o *FSOverlay) Host() []*check.Absolute {
return nil return nil
} }
p := make([]*check.Absolute, 0, 2+len(o.Lower)) p := make([]*check.Absolute, 0, 2+len(o.Lower))
if o.Upper != nil { if o.Upper != nil && o.Work != nil {
p = append(p, o.Upper) p = append(p, o.Upper, o.Work)
if o.Work != nil {
p = append(p, o.Work)
}
} }
p = append(p, o.Lower...) p = append(p, o.Lower...)
return p return p
@@ -74,18 +70,11 @@ func (o *FSOverlay) Apply(z *ApplyState) {
return return
} }
if o.Upper != nil { if o.Upper != nil && o.Work != nil {
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
if o.Target.Is(fhs.AbsRoot) { if o.Target.Is(fhs.AbsRoot) {
z.NoRemountRoot = true z.NoRemountRoot = true
} }
if o.Work != nil {
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
} else {
z.OverlayEphemeral(o.Target, slices.Concat(
o.Lower,
[]*check.Absolute{o.Upper})...,
)
}
} else { } else {
z.OverlayReadonly(o.Target, o.Lower...) z.OverlayReadonly(o.Target, o.Lower...)
} }
@@ -101,19 +90,12 @@ func (o *FSOverlay) String() string {
lower[i] = check.EscapeOverlayDataSegment(a.String()) lower[i] = check.EscapeOverlayDataSegment(a.String())
} }
if o.Upper != nil { if o.Upper != nil && o.Work != nil {
if o.Work != nil {
return "w*" + strings.Join(append([]string{ return "w*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String()), check.EscapeOverlayDataSegment(o.Target.String()),
check.EscapeOverlayDataSegment(o.Upper.String()), check.EscapeOverlayDataSegment(o.Upper.String()),
check.EscapeOverlayDataSegment(o.Work.String())}, check.EscapeOverlayDataSegment(o.Work.String())},
lower...), check.SpecialOverlayPath) lower...), check.SpecialOverlayPath)
}
return "e*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String()),
check.EscapeOverlayDataSegment(o.Upper.String())},
lower...), check.SpecialOverlayPath)
} else { } else {
return "*" + strings.Join(append([]string{ return "*" + strings.Join(append([]string{
check.EscapeOverlayDataSegment(o.Target.String())}, check.EscapeOverlayDataSegment(o.Target.String())},
+1 -13
View File
@@ -5,7 +5,6 @@ import (
"hakurei.app/check" "hakurei.app/check"
"hakurei.app/container" "hakurei.app/container"
"hakurei.app/fhs"
"hakurei.app/hst" "hakurei.app/hst"
) )
@@ -15,7 +14,7 @@ func TestFSOverlay(t *testing.T) {
checkFs(t, []fsTestCase{ checkFs(t, []fsTestCase{
{"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"}, {"nil", (*hst.FSOverlay)(nil), false, nil, nil, nil, "<invalid>"},
{"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"}, {"nil lower", &hst.FSOverlay{Target: m("/etc"), Lower: []*check.Absolute{nil}}, false, nil, nil, nil, "<invalid>"},
{"zero lower", &hst.FSOverlay{Target: m("/etc"), Work: m("/")}, false, nil, nil, nil, "<invalid>"}, {"zero lower", &hst.FSOverlay{Target: m("/etc"), Upper: m("/"), Work: m("/")}, false, nil, nil, nil, "<invalid>"},
{"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"}, {"zero lower ro", &hst.FSOverlay{Target: m("/etc")}, false, nil, nil, nil, "<invalid>"},
{"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"}, {"short lower", &hst.FSOverlay{Target: m("/etc"), Lower: ms("/etc")}, false, nil, nil, nil, "<invalid>"},
@@ -63,16 +62,5 @@ func TestFSOverlay(t *testing.T) {
Work: m("/tmp/work"), Work: m("/tmp/work"),
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"), }}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"}, "w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
{"ephemeral", &hst.FSOverlay{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1"),
Upper: m("/tmp/upper"),
}, true, container.Ops{&container.MountOverlayOp{
Target: m("/"),
Lower: ms("/tmp/.src0", "/tmp/.src1", "/tmp/upper"),
Upper: fhs.AbsRoot,
}}, m("/"), ms("/tmp/upper", "/tmp/.src0", "/tmp/.src1"),
"e*/:/tmp/upper:/tmp/.src0:/tmp/.src1"},
}) })
} }
-1
View File
@@ -245,7 +245,6 @@ func TestTemplate(t *testing.T) {
"multiarch": true, "multiarch": true,
"map_real_uid": true, "map_real_uid": true,
"device": true, "device": true,
"cover_run": true,
"share_runtime": true, "share_runtime": true,
"share_tmpdir": true "share_tmpdir": true
} }
+2 -2
View File
@@ -80,7 +80,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
continue continue
} }
if found := bytes.Contains([]byte("-_/.\\*"), []byte{b}); found { // - // _/.\* if ib := bytes.IndexByte([]byte("-_/.\\*"), b); ib != -1 { // - // _/.\*
goto opt goto opt
} else if b >= '0' && b <= '9' { // 0-9 } else if b >= '0' && b <= '9' { // 0-9
goto opt goto opt
@@ -101,7 +101,7 @@ func unescapeValue(v []byte) (val []byte, errno ParseError) {
break break
} }
if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil { if c, err := hex.Decode(val[i:i+1], v[iu+1:iu+3]); err != nil {
if _, ok := errors.AsType[hex.InvalidByteError](err); ok { if errors.As(err, new(hex.InvalidByteError)) {
errno = ErrBadValHexByte errno = ErrBadValHexByte
break break
} }
-17
View File
@@ -2,7 +2,6 @@ package kobject
import ( import (
"errors" "errors"
"maps"
"strconv" "strconv"
"strings" "strings"
"unsafe" "unsafe"
@@ -29,22 +28,6 @@ type Event struct {
Subsystem string `json:"subsystem"` Subsystem string `json:"subsystem"`
} }
// Clone returns a copy of e.
func (e *Event) Clone() Event {
v := *e
v.Env = maps.Clone(e.Env)
return v
}
// makeColdboot allocates a new [Object] from e in [StateColdboot].
func (e *Event) makeColdboot() *Object {
return &Object{
State: StateColdboot,
DevPath: e.DevPath,
Subsystem: e.Subsystem,
}
}
// Populate populates e with the contents of a [uevent.Message]. // Populate populates e with the contents of a [uevent.Message].
// //
// The ACTION and DEVPATH environment variables are ignored and assumed to be // The ACTION and DEVPATH environment variables are ignored and assumed to be
-491
View File
@@ -1,491 +0,0 @@
// Package kobject interprets uevent messages from a NETLINK_KOBJECT_UEVENT socket.
package kobject
import (
"context"
"fmt"
"maps"
"slices"
"strconv"
"sync"
"hakurei.app/internal/report"
"hakurei.app/internal/uevent"
)
const (
// StateColdboot denotes an [Object] populated by a coldboot event. It is
// eligible for all event actions.
StateColdboot = iota
// StateNew denotes an [Object] previously populated by a [uevent.KOBJ_ADD]
// event, but has not yet been targeted by a [uevent.KOBJ_BIND] event, or
// has been targeted by a [uevent.KOBJ_UNBIND] event.
StateNew
// StateBound denotes an [Object] that has been targeted by a
// [uevent.KOBJ_BIND] and has not been targeted by a [uevent.KOBJ_UNBIND]
// after that.
StateBound
)
// Object represents a kernel object.
type Object struct {
// Origin of the object.
State int `json:"state,omitempty"`
// Set by [uevent.KOBJ_OFFLINE] and [uevent.KOBJ_ONLINE].
Offline bool `json:"offline,omitempty"`
// alloc_uevent_skb: devpath
DevPath string `json:"devpath"`
// registered per-driver (optional)
ModAlias string `json:"modalias,omitempty"`
// dev_driver_uevent: drv->name (optional)
Driver string `json:"driver,omitempty"`
// SUBSYSTEM value set by the kernel.
Subsystem string `json:"subsystem"`
// Uninterpreted environment variable pairs. An entry missing a separator
// gains the value "\x00".
Env map[string]string `json:"env"`
}
// Clone returns the address of a copy of o.
func (o *Object) Clone() *Object {
v := *o
v.Env = maps.Clone(o.Env)
return &v
}
// GoString returns compound literal for the underlying value.
func (o *Object) GoString() string {
return fmt.Sprintf("&%#v", *o)
}
// merge merges uninterpreted environment variable pairs from an [Event].
func (o *Object) merge(env map[string]string) {
for k, v := range env {
if v == "\x00" {
continue
}
switch k {
case "MODALIAS":
o.ModAlias = v
continue
case "DRIVER":
o.Driver = v
continue
default:
if o.Env == nil {
o.Env = make(map[string]string)
}
o.Env[k] = v
}
}
}
// update updates o with pairs from env, optionally stripping visited pairs.
func (o *Object) update(env map[string]string, strip bool) {
for k := range o.Env {
if v, ok := env[k]; ok {
if strip {
delete(env, k)
}
o.Env[k] = v
}
}
}
// A pendingIterator is a callback currently iterating through objects targeted
// by ongoing events.
type pendingIterator struct {
f func(o *Object, act uevent.KobjectAction) bool
done chan<- struct{}
}
// State processes a stream of [Event] populated from [uevent.Message] received
// from a NETLINK_KOBJECT_UEVENT socket and presents an efficient representation
// of kernel state.
type State struct {
// Next expected SEQNUM.
seq uint64
// DevPath to environment variables.
uevent map[string]*Object
// Synchronises access to uevent and its objects.
ueventMu sync.RWMutex
// Alive iterators.
iter []*pendingIterator
// Synchronises access to iter.
iterMu sync.Mutex
// UUID for synthetic [uevent.Coldboot] events.
coldboot uevent.UUID
// Called on [uevent.KOBJ_CHANGE] with stripped environment variables.
handleChange func(o *Object, env map[string]string)
// Reports errors populating [Event] from [uevent.Message]. A user-supplied
// nil value is replaced with a noop.
reportErr func(error)
}
// New returns the address of a new [State].
func New(
coldboot uevent.UUID,
handleChange func(o *Object, env map[string]string),
reportErr func(error),
) *State {
return &State{
uevent: make(map[string]*Object),
coldboot: coldboot,
handleChange: handleChange,
reportErr: reportErr,
}
}
// deleteIter removes an iterator from s. Must be called after acquiring iterMu.
func (s *State) deleteIter(p *pendingIterator) {
s.iter = slices.DeleteFunc(s.iter, func(v *pendingIterator) bool {
return p == v
})
}
// dispatchIter broadcasts an [Object] to all alive iterators.
func (s *State) dispatchIter(o *Object, act uevent.KobjectAction) {
s.iterMu.Lock()
defer s.iterMu.Unlock()
for _, p := range s.iter {
if !p.f(o, act) {
s.deleteIter(p)
close(p.done)
}
}
}
// Range calls f on all current and upcoming [Object] values tracked by s until
// f returns false or the context is cancelled. f must not retain o or modify
// the value it points to.
func (s *State) Range(
ctx context.Context,
f func(o *Object, act uevent.KobjectAction) bool,
) {
done := make(chan struct{})
p := pendingIterator{f, done}
s.iterMu.Lock()
s.ueventMu.RLock()
for _, o := range s.uevent {
if !f(o, uevent.KOBJ_ADD) {
s.ueventMu.RUnlock()
s.iterMu.Unlock()
return
}
}
s.ueventMu.RUnlock()
s.iter = append(s.iter, &p)
s.iterMu.Unlock()
select {
case <-ctx.Done():
s.iterMu.Lock()
s.deleteIter(&p)
s.iterMu.Unlock()
return
case <-done:
// deregistered by dispatchIter
return
}
}
// An EventError describes a malformed or inconsistent [Event].
type EventError struct {
Kind int `json:"fault"`
E Event `json:"event"`
O *Object `json:"object,omitempty"`
}
var _ report.RepresentableError = EventError{}
func (EventError) Representable() {}
const (
// EUnexpectedColdboot is reported for a coldboot event with action other
// than the expected [uevent.KOBJ_ADD].
EUnexpectedColdboot = iota
// EDuplicateAdd is reported for a [uevent.KOBJ_ADD] event on a
// still-existing entry that was not the result of a coldboot.
EDuplicateAdd
// EBadTarget is reported for an event on a nonexistent [Object]. This is
// generally only possible before coldboot completes.
EBadTarget
// ERemoveState is reported for a [uevent.KOBJ_REMOVE] event targeting an
// entry in a state other than [StateColdboot] and [StateNew].
ERemoveState
// EUnexpectedOffline is reported for a [uevent.KOBJ_OFFLINE] or
// [uevent.KOBJ_ONLINE] event targeting an already offline or online object.
EUnexpectedOffline
// EBindState is reported for a [uevent.KOBJ_BIND] event targeting an entry
// in a state other than [StateColdboot] and [StateNew].
EBindState
// EUnbindState is reported for a [uevent.KOBJ_UNBIND] event targeting an
// entry in a state other than [StateBound].
EUnbindState
// EMalformedMove is reported for a [uevent.KOBJ_MOVE] event missing the
// DEVPATH_OLD environment variable.
EMalformedMove
)
func (e EventError) Error() string {
switch e.Kind {
case EUnexpectedColdboot:
return "unexpected " + e.E.Action.String() + " coldboot event"
case EDuplicateAdd:
return "duplicate add event on devpath " + strconv.Quote(e.E.DevPath)
case EBadTarget:
return "unexpected " + e.E.Action.String() + " event on devpath " +
strconv.Quote(e.E.DevPath)
case ERemoveState:
if e.O == nil {
return "invalid remove event error"
}
return "remove event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EUnexpectedOffline:
if e.O == nil {
return "invalid unexpected offline error"
}
if e.O.Offline {
return "offline event targeting devpath " + strconv.Quote(e.E.DevPath)
}
return "online event targeting devpath " + strconv.Quote(e.E.DevPath)
case EBindState:
if e.O == nil {
return "invalid bind state error"
}
return "bind event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EUnbindState:
if e.O == nil {
return "invalid unbind state error"
}
return "unbind event targeting devpath " + strconv.Quote(e.E.DevPath) +
" in state " + strconv.Itoa(e.O.State)
case EMalformedMove:
return "move event targeting devpath " + strconv.Quote(e.E.DevPath) +
" missing DEVPATH_OLD"
default:
return "invalid event error kind " + strconv.Itoa(e.Kind)
}
}
// NewError returns a new [EventError] for e and o.
func (e *Event) NewError(kind int, o *Object) error {
if o != nil {
o = o.Clone()
}
return EventError{kind, e.Clone(), o}
}
// processEvent merges an event into s.
func (s *State) processEvent(e *Event) {
s.ueventMu.Lock()
defer s.ueventMu.Unlock()
coldboot := e.Synth != nil
if e.Action != uevent.KOBJ_ADD && coldboot {
s.reportErr(e.NewError(EUnexpectedColdboot, nil))
return
}
switch act := e.Action; act {
case uevent.KOBJ_ADD:
if e.Synth == nil {
if o, ok := s.uevent[e.DevPath]; ok {
s.reportErr(e.NewError(EDuplicateAdd, o))
o.merge(e.Env)
s.dispatchIter(o, act)
return
}
}
o := e.makeColdboot()
if !coldboot {
o.State = StateNew
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o, act)
return
case uevent.KOBJ_REMOVE:
if o, ok := s.uevent[e.DevPath]; !ok {
s.reportErr(e.NewError(EBadTarget, nil))
return
} else if o.State != StateColdboot && o.State != StateNew {
s.reportErr(e.NewError(ERemoveState, o))
}
delete(s.uevent, e.DevPath)
return
case uevent.KOBJ_CHANGE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// this suffers from the coldboot race window similar to KOBJ_MOVE,
// however this action combines driver-specific and change-specific
// environment variables and combines them with environment
// variables meant to convey state of the kobject, and it is not
// possible to reliably separate them, so this fallback avoids the
// race at the cost of including some garbage in tracked state
o = e.makeColdboot()
o.merge(e.Env)
s.uevent[e.DevPath] = o
s.dispatchIter(o, act)
return
}
o.update(e.Env, true)
if s.handleChange != nil {
s.handleChange(o, e.Env)
}
s.dispatchIter(o, act)
return
case uevent.KOBJ_MOVE:
var o *Object
if old, ok := e.Env["DEVPATH_OLD"]; !ok {
s.reportErr(e.NewError(EMalformedMove, nil))
// not reached
o = e.makeColdboot()
} else if o, ok = s.uevent[old]; !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// this generally happens during coldboot, dropping the event here
// may cause inconsistent state if the coldboot event for this
// object was generated before the bind event
delete(e.Env, "DEVPATH_OLD")
o = e.makeColdboot()
} else {
delete(s.uevent, old)
delete(e.Env, "DEVPATH_OLD")
}
o.merge(e.Env)
s.uevent[e.DevPath] = o
o.DevPath = e.DevPath
s.dispatchIter(o, act)
return
case uevent.KOBJ_ONLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if !o.Offline {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = false
s.dispatchIter(o, act)
return
case uevent.KOBJ_OFFLINE:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
o.merge(e.Env)
}
if o.Offline {
s.reportErr(e.NewError(EUnexpectedOffline, o))
}
o.Offline = true
s.dispatchIter(o, act)
return
case uevent.KOBJ_BIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE
o = e.makeColdboot()
s.uevent[e.DevPath] = o
}
if o.State != StateColdboot && o.State != StateNew {
s.reportErr(e.NewError(EBindState, o))
}
o.State = StateBound
o.merge(e.Env)
s.dispatchIter(o, act)
return
case uevent.KOBJ_UNBIND:
o, ok := s.uevent[e.DevPath]
if !ok {
s.reportErr(e.NewError(EBadTarget, nil))
// coldboot race window similar to an unexpected KOBJ_MOVE, but does
// not result in inconsistent state if dropped
return
}
if o.State != StateBound {
s.reportErr(e.NewError(EUnbindState, o))
}
o.State = StateNew
o.Driver = ""
s.dispatchIter(o, act)
return
default: // not reached
s.reportErr(fmt.Errorf("invalid action %d", e.Action))
return
}
}
// BadSequenceError is reported for an unexpected SEQNUM.
type BadSequenceError struct{ Got, Want uint64 }
func (e BadSequenceError) Error() string {
return "SEQNUM=" + strconv.FormatUint(e.Got, 10) +
", want " + strconv.FormatUint(e.Want, 10)
}
// Consume receives uevent messages and updates s to reflect state of kernel.
func (s *State) Consume(ctx context.Context, events <-chan *uevent.Message) {
if s.uevent == nil {
s.uevent = make(map[string]*Object)
}
if s.reportErr == nil {
s.reportErr = func(error) {}
}
var e Event
for {
select {
case <-ctx.Done():
return
case m, ok := <-events:
if !ok {
return
}
e.Populate(s.reportErr, m)
// skip external synthetic event
if e.Synth != nil && *e.Synth != s.coldboot {
continue
}
if s.seq == 0 {
s.seq = e.Sequence
}
if s.seq != e.Sequence {
s.reportErr(BadSequenceError{e.Sequence, s.seq})
}
s.seq++
s.processEvent(&e)
}
}
}
File diff suppressed because it is too large Load Diff
-266
View File
@@ -1,266 +0,0 @@
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXPWRBN:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXPWRBN:","SEQNUM=777"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXPWRBN:00/wakeup/wakeup7","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00/wakeup/wakeup7","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=778"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00/LNXCPU:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00/LNXCPU:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXCPU:","SEQNUM=779"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/ACPI0010:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:ACPI0010:PNP0A05:","SEQNUM=780"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0103:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0103:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0103:","SEQNUM=781"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=782"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=783"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/PNP0A06:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A06:","SEQNUM=784"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/QEMU0002:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/QEMU0002:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:QEMU0002:","SEQNUM=785"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=786"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00/wakeup/wakeup0","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:00/wakeup/wakeup0","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=787"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0303:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0303:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0303:","SEQNUM=788"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0400:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0400:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0400:","SEQNUM=789"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0501:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0501:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0501:","SEQNUM=790"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00/device:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00/device:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=791"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0700:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0700:","SEQNUM=792"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0B00:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0B00:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0B00:","SEQNUM=793"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0F13:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/PNP0F13:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0F13:","SEQNUM=794"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=795"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/wakeup/wakeup1","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:01/wakeup/wakeup1","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=796"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=797"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03/wakeup/wakeup2","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:03/wakeup/wakeup2","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=798"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=799"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04/wakeup/wakeup3","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:04/wakeup/wakeup3","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=800"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=801"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05/wakeup/wakeup4","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:05/wakeup/wakeup4","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=802"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=803"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06/wakeup/wakeup5","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:06/wakeup/wakeup5","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=804"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=805"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=806"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:09","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:09","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=807"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0a","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0a","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=808"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0b","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0b","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=809"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0c","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0c","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=810"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0d","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0d","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=811"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0e","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0e","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=812"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0f","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:0f","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=813"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:10","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:10","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=814"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:11","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:11","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=815"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:12","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:12","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=816"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:13","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:13","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=817"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:14","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:14","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=818"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:15","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:15","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=819"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:16","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:16","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=820"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:17","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:17","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=821"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:18","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:18","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=822"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:19","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:19","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=823"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1a","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1a","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=824"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1b","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1b","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=825"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1c","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1c","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=826"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1d","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1d","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=827"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1e","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1e","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=828"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1f","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:1f","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=829"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:20","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:20","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=830"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:21","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:21","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=831"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:22","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:22","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=832"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0A03:","SEQNUM=833"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/wakeup/wakeup6","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/wakeup/wakeup6","SUBSYSTEM=wakeup","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=834"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=835"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=836"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:02","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:02","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=837"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:03","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:03","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=838"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:04","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0C0F:04","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0C0F:","SEQNUM=839"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYBUS:","SEQNUM=840"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:01","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:01","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYBUS:","SEQNUM=841"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00","SUBSYSTEM=acpi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:LNXSYSTM:","SEQNUM=842"]}
{"action":"add","devpath":"/devices/breakpoint","env":["ACTION=add","DEVPATH=/devices/breakpoint","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=843"]}
{"action":"add","devpath":"/devices/cpu","env":["ACTION=add","DEVPATH=/devices/cpu","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=844"]}
{"action":"add","devpath":"/devices/kprobe","env":["ACTION=add","DEVPATH=/devices/kprobe","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=845"]}
{"action":"add","devpath":"/devices/msr","env":["ACTION=add","DEVPATH=/devices/msr","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=846"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:00.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:00.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=60000","PCI_ID=8086:1237","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:00.0","MODALIAS=pci:v00008086d00001237sv00001AF4sd00001100bc06sc00i00","SEQNUM=847"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=60100","PCI_ID=8086:7000","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.0","MODALIAS=pci:v00008086d00007000sv00001AF4sd00001100bc06sc01i00","SEQNUM=848"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/ata_port/ata1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/ata_port/ata1","SUBSYSTEM=ata_port","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=849"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/host0/scsi_host/host0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/host0/scsi_host/host0","SUBSYSTEM=scsi_host","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=850"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/host0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/host0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_host","SEQNUM=851"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/ata_link/link1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/ata_link/link1","SUBSYSTEM=ata_link","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=852"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.0/ata_device/dev1.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.0/ata_device/dev1.0","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=853"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.1/ata_device/dev1.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata1/link1/dev1.1/ata_device/dev1.1","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=854"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/ata_port/ata2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/ata_port/ata2","SUBSYSTEM=ata_port","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=855"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/scsi_host/host1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/scsi_host/host1","SUBSYSTEM=scsi_host","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=856"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/bsg/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/bsg/1:0:0:0","SUBSYSTEM=bsg","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=251","MINOR=0","DEVNAME=bsg/1:0:0:0","SEQNUM=857"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/scsi_device/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0/scsi_device/1:0:0:0","SUBSYSTEM=scsi_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=858"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0/1:0:0:0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_device","MODALIAS=scsi:t-0x05","SEQNUM=859"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1/target1:0:0","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_target","SEQNUM=860"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/host1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/host1","SUBSYSTEM=scsi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=scsi_host","SEQNUM=861"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/ata_link/link2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/ata_link/link2","SUBSYSTEM=ata_link","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=862"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.0/ata_device/dev2.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.0/ata_device/dev2.0","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=863"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.1/ata_device/dev2.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1/ata2/link2/dev2.1/ata_device/dev2.1","SUBSYSTEM=ata_device","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=864"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.1","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=ata_piix","PCI_CLASS=10180","PCI_ID=8086:7010","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.1","MODALIAS=pci:v00008086d00007010sv00001AF4sd00001100bc01sc01i80","SEQNUM=865"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:01.3","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:01.3","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=68000","PCI_ID=8086:7113","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:01.3","MODALIAS=pci:v00008086d00007113sv00001AF4sd00001100bc06sc80i00","SEQNUM=866"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:02.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:02.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","PCI_CLASS=20000","PCI_ID=8086:100E","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:02.0","MODALIAS=pci:v00008086d0000100Esv00001AF4sd00001100bc02sc00i00","SEQNUM=867"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0","SUBSYSTEM=pci","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=virtio-pci","PCI_CLASS=10000","PCI_ID=1AF4:1001","PCI_SUBSYS_ID=1AF4:0002","PCI_SLOT_NAME=0000:00:03.0","MODALIAS=pci:v00001AF4d00001001sv00001AF4sd00000002bc01sc00i00","SEQNUM=868"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0/virtio0/block/vda","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0/virtio0/block/vda","SUBSYSTEM=block","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=254","MINOR=0","DEVNAME=vda","DEVTYPE=disk","DISKSEQ=1","SEQNUM=869"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:03.0/virtio0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:03.0/virtio0","SUBSYSTEM=virtio","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=virtio_blk","MODALIAS=virtio:d00000002v00001AF4","SEQNUM=870"]}
{"action":"add","devpath":"/devices/pci0000:00/QEMU0002:00","env":["ACTION=add","DEVPATH=/devices/pci0000:00/QEMU0002:00","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:QEMU0002:","SEQNUM=871"]}
{"action":"add","devpath":"/devices/pci0000:00/pci_bus/0000:00","env":["ACTION=add","DEVPATH=/devices/pci0000:00/pci_bus/0000:00","SUBSYSTEM=pci_bus","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=872"]}
{"action":"add","devpath":"/devices/platform/PNP0103:00","env":["ACTION=add","DEVPATH=/devices/platform/PNP0103:00","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=acpi:PNP0103:","SEQNUM=873"]}
{"action":"add","devpath":"/devices/platform/pcspkr","env":["ACTION=add","DEVPATH=/devices/platform/pcspkr","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=platform:pcspkr","SEQNUM=874"]}
{"action":"add","devpath":"/devices/platform/reg-dummy/regulator/regulator.0","env":["ACTION=add","DEVPATH=/devices/platform/reg-dummy/regulator/regulator.0","SUBSYSTEM=regulator","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=875"]}
{"action":"add","devpath":"/devices/platform/reg-dummy","env":["ACTION=add","DEVPATH=/devices/platform/reg-dummy","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=reg-dummy","MODALIAS=platform:reg-dummy","SEQNUM=876"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.1/tty/ttyS1","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.1/tty/ttyS1","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=65","DEVNAME=ttyS1","SEQNUM=877"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.1","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.1","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=878"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.2/tty/ttyS2","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.2/tty/ttyS2","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=66","DEVNAME=ttyS2","SEQNUM=879"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.2","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.2","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=880"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.3/tty/ttyS3","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.3/tty/ttyS3","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=67","DEVNAME=ttyS3","SEQNUM=881"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0/serial8250:0.3","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0/serial8250:0.3","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=882"]}
{"action":"add","devpath":"/devices/platform/serial8250/serial8250:0","env":["ACTION=add","DEVPATH=/devices/platform/serial8250/serial8250:0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=ctrl","DRIVER=ctrl","SEQNUM=883"]}
{"action":"add","devpath":"/devices/platform/serial8250","env":["ACTION=add","DEVPATH=/devices/platform/serial8250","SUBSYSTEM=platform","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=serial8250","MODALIAS=platform:serial8250","SEQNUM=884"]}
{"action":"add","devpath":"/devices/pnp0/00:00","env":["ACTION=add","DEVPATH=/devices/pnp0/00:00","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=885"]}
{"action":"add","devpath":"/devices/pnp0/00:01","env":["ACTION=add","DEVPATH=/devices/pnp0/00:01","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=886"]}
{"action":"add","devpath":"/devices/pnp0/00:02","env":["ACTION=add","DEVPATH=/devices/pnp0/00:02","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=887"]}
{"action":"add","devpath":"/devices/pnp0/00:03","env":["ACTION=add","DEVPATH=/devices/pnp0/00:03","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=888"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0/00:04:0.0/tty/ttyS0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0/00:04:0.0/tty/ttyS0","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=64","DEVNAME=ttyS0","SEQNUM=889"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0/00:04:0.0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0/00:04:0.0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=port","DRIVER=port","SEQNUM=890"]}
{"action":"add","devpath":"/devices/pnp0/00:04/00:04:0","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04/00:04:0","SUBSYSTEM=serial-base","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DEVTYPE=ctrl","DRIVER=ctrl","SEQNUM=891"]}
{"action":"add","devpath":"/devices/pnp0/00:04","env":["ACTION=add","DEVPATH=/devices/pnp0/00:04","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=serial","SEQNUM=892"]}
{"action":"add","devpath":"/devices/pnp0/00:05","env":["ACTION=add","DEVPATH=/devices/pnp0/00:05","SUBSYSTEM=pnp","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=893"]}
{"action":"add","devpath":"/devices/software","env":["ACTION=add","DEVPATH=/devices/software","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=894"]}
{"action":"add","devpath":"/devices/system/clockevents/broadcast","env":["ACTION=add","DEVPATH=/devices/system/clockevents/broadcast","SUBSYSTEM=clockevents","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=895"]}
{"action":"add","devpath":"/devices/system/clockevents/clockevent0","env":["ACTION=add","DEVPATH=/devices/system/clockevents/clockevent0","SUBSYSTEM=clockevents","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=896"]}
{"action":"add","devpath":"/devices/system/clocksource/clocksource0","env":["ACTION=add","DEVPATH=/devices/system/clocksource/clocksource0","SUBSYSTEM=clocksource","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=897"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:00","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:00","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=898"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:01","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:01","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=899"]}
{"action":"add","devpath":"/devices/system/container/PNP0A06:02","env":["ACTION=add","DEVPATH=/devices/system/container/PNP0A06:02","SUBSYSTEM=container","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=900"]}
{"action":"add","devpath":"/devices/system/cpu/cpu0","env":["ACTION=add","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=901"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=902"]}
{"action":"add","devpath":"/devices/system/memory/memory0","env":["ACTION=add","DEVPATH=/devices/system/memory/memory0","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=903"]}
{"action":"add","devpath":"/devices/system/memory/memory1","env":["ACTION=add","DEVPATH=/devices/system/memory/memory1","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=904"]}
{"action":"add","devpath":"/devices/system/memory/memory2","env":["ACTION=add","DEVPATH=/devices/system/memory/memory2","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=905"]}
{"action":"add","devpath":"/devices/system/memory/memory3","env":["ACTION=add","DEVPATH=/devices/system/memory/memory3","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=906"]}
{"action":"add","devpath":"/devices/system/memory/memory4","env":["ACTION=add","DEVPATH=/devices/system/memory/memory4","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=907"]}
{"action":"add","devpath":"/devices/system/memory/memory5","env":["ACTION=add","DEVPATH=/devices/system/memory/memory5","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=908"]}
{"action":"add","devpath":"/devices/system/memory/memory6","env":["ACTION=add","DEVPATH=/devices/system/memory/memory6","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=909"]}
{"action":"add","devpath":"/devices/system/memory/memory7","env":["ACTION=add","DEVPATH=/devices/system/memory/memory7","SUBSYSTEM=memory","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=910"]}
{"action":"add","devpath":"/devices/system/node/node0","env":["ACTION=add","DEVPATH=/devices/system/node/node0","SUBSYSTEM=node","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=911"]}
{"action":"add","devpath":"/devices/tracepoint","env":["ACTION=add","DEVPATH=/devices/tracepoint","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=912"]}
{"action":"add","devpath":"/devices/uprobe","env":["ACTION=add","DEVPATH=/devices/uprobe","SUBSYSTEM=event_source","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=913"]}
{"action":"add","devpath":"/devices/virtual/bdi/254:0","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/254:0","SUBSYSTEM=bdi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=914"]}
{"action":"add","devpath":"/devices/virtual/devlink/:ata2--scsi:1:0:0:0","env":["ACTION=add","DEVPATH=/devices/virtual/devlink/:ata2--scsi:1:0:0:0","SUBSYSTEM=devlink","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=915"]}
{"action":"add","devpath":"/devices/virtual/dmi/id","env":["ACTION=add","DEVPATH=/devices/virtual/dmi/id","SUBSYSTEM=dmi","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MODALIAS=dmi:bvnSeaBIOS:bvrrel-1.17.0-0-gb52ca86e094d-prebuilt.qemu.org:bd04/01/2014:br0.0:svnQEMU:pnStandardPC(i440FX+PIIX,1996):pvrpc-i440fx-10.1:cvnQEMU:ct1:cvrpc-i440fx-10.1:sku:","SEQNUM=916"]}
{"action":"add","devpath":"/devices/virtual/mem/full","env":["ACTION=add","DEVPATH=/devices/virtual/mem/full","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=7","DEVNAME=full","DEVMODE=0666","SEQNUM=917"]}
{"action":"add","devpath":"/devices/virtual/mem/kmsg","env":["ACTION=add","DEVPATH=/devices/virtual/mem/kmsg","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=11","DEVNAME=kmsg","DEVMODE=0644","SEQNUM=918"]}
{"action":"add","devpath":"/devices/virtual/mem/mem","env":["ACTION=add","DEVPATH=/devices/virtual/mem/mem","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=1","DEVNAME=mem","SEQNUM=919"]}
{"action":"add","devpath":"/devices/virtual/mem/null","env":["ACTION=add","DEVPATH=/devices/virtual/mem/null","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=3","DEVNAME=null","DEVMODE=0666","SEQNUM=920"]}
{"action":"add","devpath":"/devices/virtual/mem/port","env":["ACTION=add","DEVPATH=/devices/virtual/mem/port","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=4","DEVNAME=port","SEQNUM=921"]}
{"action":"add","devpath":"/devices/virtual/mem/random","env":["ACTION=add","DEVPATH=/devices/virtual/mem/random","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=8","DEVNAME=random","DEVMODE=0666","SEQNUM=922"]}
{"action":"add","devpath":"/devices/virtual/mem/urandom","env":["ACTION=add","DEVPATH=/devices/virtual/mem/urandom","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=9","DEVNAME=urandom","DEVMODE=0666","SEQNUM=923"]}
{"action":"add","devpath":"/devices/virtual/mem/zero","env":["ACTION=add","DEVPATH=/devices/virtual/mem/zero","SUBSYSTEM=mem","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=1","MINOR=5","DEVNAME=zero","DEVMODE=0666","SEQNUM=924"]}
{"action":"add","devpath":"/devices/virtual/memory_tiering/memory_tier4","env":["ACTION=add","DEVPATH=/devices/virtual/memory_tiering/memory_tier4","SUBSYSTEM=memory_tiering","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=925"]}
{"action":"add","devpath":"/devices/virtual/misc/cpu_dma_latency","env":["ACTION=add","DEVPATH=/devices/virtual/misc/cpu_dma_latency","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=259","DEVNAME=cpu_dma_latency","SEQNUM=926"]}
{"action":"add","devpath":"/devices/virtual/misc/hpet","env":["ACTION=add","DEVPATH=/devices/virtual/misc/hpet","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=228","DEVNAME=hpet","SEQNUM=927"]}
{"action":"add","devpath":"/devices/virtual/misc/snapshot","env":["ACTION=add","DEVPATH=/devices/virtual/misc/snapshot","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=231","DEVNAME=snapshot","SEQNUM=928"]}
{"action":"add","devpath":"/devices/virtual/misc/udmabuf","env":["ACTION=add","DEVPATH=/devices/virtual/misc/udmabuf","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=258","DEVNAME=udmabuf","SEQNUM=929"]}
{"action":"add","devpath":"/devices/virtual/misc/userfaultfd","env":["ACTION=add","DEVPATH=/devices/virtual/misc/userfaultfd","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=257","DEVNAME=userfaultfd","SEQNUM=930"]}
{"action":"add","devpath":"/devices/virtual/misc/vga_arbiter","env":["ACTION=add","DEVPATH=/devices/virtual/misc/vga_arbiter","SUBSYSTEM=misc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=10","MINOR=256","DEVNAME=vga_arbiter","SEQNUM=931"]}
{"action":"add","devpath":"/devices/virtual/net/lo","env":["ACTION=add","DEVPATH=/devices/virtual/net/lo","SUBSYSTEM=net","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","INTERFACE=lo","IFINDEX=1","SEQNUM=932"]}
{"action":"add","devpath":"/devices/virtual/thermal/cooling_device0","env":["ACTION=add","DEVPATH=/devices/virtual/thermal/cooling_device0","SUBSYSTEM=thermal","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=933"]}
{"action":"add","devpath":"/devices/virtual/tty/console","env":["ACTION=add","DEVPATH=/devices/virtual/tty/console","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=1","DEVNAME=console","SEQNUM=934"]}
{"action":"add","devpath":"/devices/virtual/tty/ptmx","env":["ACTION=add","DEVPATH=/devices/virtual/tty/ptmx","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=2","DEVNAME=ptmx","DEVMODE=0666","SEQNUM=935"]}
{"action":"add","devpath":"/devices/virtual/tty/tty","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=5","MINOR=0","DEVNAME=tty","DEVMODE=0666","SEQNUM=936"]}
{"action":"add","devpath":"/devices/virtual/tty/tty0","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty0","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=0","DEVNAME=tty0","SEQNUM=937"]}
{"action":"add","devpath":"/devices/virtual/tty/tty1","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty1","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=1","DEVNAME=tty1","SEQNUM=938"]}
{"action":"add","devpath":"/devices/virtual/tty/tty10","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty10","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=10","DEVNAME=tty10","SEQNUM=939"]}
{"action":"add","devpath":"/devices/virtual/tty/tty11","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty11","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=11","DEVNAME=tty11","SEQNUM=940"]}
{"action":"add","devpath":"/devices/virtual/tty/tty12","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty12","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=12","DEVNAME=tty12","SEQNUM=941"]}
{"action":"add","devpath":"/devices/virtual/tty/tty13","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty13","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=13","DEVNAME=tty13","SEQNUM=942"]}
{"action":"add","devpath":"/devices/virtual/tty/tty14","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty14","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=14","DEVNAME=tty14","SEQNUM=943"]}
{"action":"add","devpath":"/devices/virtual/tty/tty15","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty15","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=15","DEVNAME=tty15","SEQNUM=944"]}
{"action":"add","devpath":"/devices/virtual/tty/tty16","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty16","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=16","DEVNAME=tty16","SEQNUM=945"]}
{"action":"add","devpath":"/devices/virtual/tty/tty17","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty17","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=17","DEVNAME=tty17","SEQNUM=946"]}
{"action":"add","devpath":"/devices/virtual/tty/tty18","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty18","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=18","DEVNAME=tty18","SEQNUM=947"]}
{"action":"add","devpath":"/devices/virtual/tty/tty19","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty19","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=19","DEVNAME=tty19","SEQNUM=948"]}
{"action":"add","devpath":"/devices/virtual/tty/tty2","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty2","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=2","DEVNAME=tty2","SEQNUM=949"]}
{"action":"add","devpath":"/devices/virtual/tty/tty20","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty20","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=20","DEVNAME=tty20","SEQNUM=950"]}
{"action":"add","devpath":"/devices/virtual/tty/tty21","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty21","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=21","DEVNAME=tty21","SEQNUM=951"]}
{"action":"add","devpath":"/devices/virtual/tty/tty22","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty22","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=22","DEVNAME=tty22","SEQNUM=952"]}
{"action":"add","devpath":"/devices/virtual/tty/tty23","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty23","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=23","DEVNAME=tty23","SEQNUM=953"]}
{"action":"add","devpath":"/devices/virtual/tty/tty24","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty24","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=24","DEVNAME=tty24","SEQNUM=954"]}
{"action":"add","devpath":"/devices/virtual/tty/tty25","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty25","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=25","DEVNAME=tty25","SEQNUM=955"]}
{"action":"add","devpath":"/devices/virtual/tty/tty26","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty26","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=26","DEVNAME=tty26","SEQNUM=956"]}
{"action":"add","devpath":"/devices/virtual/tty/tty27","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty27","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=27","DEVNAME=tty27","SEQNUM=957"]}
{"action":"add","devpath":"/devices/virtual/tty/tty28","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty28","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=28","DEVNAME=tty28","SEQNUM=958"]}
{"action":"add","devpath":"/devices/virtual/tty/tty29","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty29","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=29","DEVNAME=tty29","SEQNUM=959"]}
{"action":"add","devpath":"/devices/virtual/tty/tty3","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty3","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=3","DEVNAME=tty3","SEQNUM=960"]}
{"action":"add","devpath":"/devices/virtual/tty/tty30","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty30","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=30","DEVNAME=tty30","SEQNUM=961"]}
{"action":"add","devpath":"/devices/virtual/tty/tty31","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty31","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=31","DEVNAME=tty31","SEQNUM=962"]}
{"action":"add","devpath":"/devices/virtual/tty/tty32","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty32","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=32","DEVNAME=tty32","SEQNUM=963"]}
{"action":"add","devpath":"/devices/virtual/tty/tty33","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty33","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=33","DEVNAME=tty33","SEQNUM=964"]}
{"action":"add","devpath":"/devices/virtual/tty/tty34","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty34","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=34","DEVNAME=tty34","SEQNUM=965"]}
{"action":"add","devpath":"/devices/virtual/tty/tty35","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty35","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=35","DEVNAME=tty35","SEQNUM=966"]}
{"action":"add","devpath":"/devices/virtual/tty/tty36","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty36","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=36","DEVNAME=tty36","SEQNUM=967"]}
{"action":"add","devpath":"/devices/virtual/tty/tty37","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty37","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=37","DEVNAME=tty37","SEQNUM=968"]}
{"action":"add","devpath":"/devices/virtual/tty/tty38","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty38","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=38","DEVNAME=tty38","SEQNUM=969"]}
{"action":"add","devpath":"/devices/virtual/tty/tty39","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty39","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=39","DEVNAME=tty39","SEQNUM=970"]}
{"action":"add","devpath":"/devices/virtual/tty/tty4","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty4","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=4","DEVNAME=tty4","SEQNUM=971"]}
{"action":"add","devpath":"/devices/virtual/tty/tty40","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty40","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=40","DEVNAME=tty40","SEQNUM=972"]}
{"action":"add","devpath":"/devices/virtual/tty/tty41","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty41","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=41","DEVNAME=tty41","SEQNUM=973"]}
{"action":"add","devpath":"/devices/virtual/tty/tty42","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty42","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=42","DEVNAME=tty42","SEQNUM=974"]}
{"action":"add","devpath":"/devices/virtual/tty/tty43","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty43","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=43","DEVNAME=tty43","SEQNUM=975"]}
{"action":"add","devpath":"/devices/virtual/tty/tty44","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty44","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=44","DEVNAME=tty44","SEQNUM=976"]}
{"action":"add","devpath":"/devices/virtual/tty/tty45","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty45","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=45","DEVNAME=tty45","SEQNUM=977"]}
{"action":"add","devpath":"/devices/virtual/tty/tty46","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty46","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=46","DEVNAME=tty46","SEQNUM=978"]}
{"action":"add","devpath":"/devices/virtual/tty/tty47","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty47","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=47","DEVNAME=tty47","SEQNUM=979"]}
{"action":"add","devpath":"/devices/virtual/tty/tty48","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty48","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=48","DEVNAME=tty48","SEQNUM=980"]}
{"action":"add","devpath":"/devices/virtual/tty/tty49","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty49","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=49","DEVNAME=tty49","SEQNUM=981"]}
{"action":"add","devpath":"/devices/virtual/tty/tty5","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty5","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=5","DEVNAME=tty5","SEQNUM=982"]}
{"action":"add","devpath":"/devices/virtual/tty/tty50","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty50","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=50","DEVNAME=tty50","SEQNUM=983"]}
{"action":"add","devpath":"/devices/virtual/tty/tty51","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty51","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=51","DEVNAME=tty51","SEQNUM=984"]}
{"action":"add","devpath":"/devices/virtual/tty/tty52","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty52","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=52","DEVNAME=tty52","SEQNUM=985"]}
{"action":"add","devpath":"/devices/virtual/tty/tty53","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty53","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=53","DEVNAME=tty53","SEQNUM=986"]}
{"action":"add","devpath":"/devices/virtual/tty/tty54","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty54","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=54","DEVNAME=tty54","SEQNUM=987"]}
{"action":"add","devpath":"/devices/virtual/tty/tty55","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty55","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=55","DEVNAME=tty55","SEQNUM=988"]}
{"action":"add","devpath":"/devices/virtual/tty/tty56","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty56","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=56","DEVNAME=tty56","SEQNUM=989"]}
{"action":"add","devpath":"/devices/virtual/tty/tty57","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty57","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=57","DEVNAME=tty57","SEQNUM=990"]}
{"action":"add","devpath":"/devices/virtual/tty/tty58","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty58","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=58","DEVNAME=tty58","SEQNUM=991"]}
{"action":"add","devpath":"/devices/virtual/tty/tty59","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty59","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=59","DEVNAME=tty59","SEQNUM=992"]}
{"action":"add","devpath":"/devices/virtual/tty/tty6","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty6","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=6","DEVNAME=tty6","SEQNUM=993"]}
{"action":"add","devpath":"/devices/virtual/tty/tty60","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty60","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=60","DEVNAME=tty60","SEQNUM=994"]}
{"action":"add","devpath":"/devices/virtual/tty/tty61","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty61","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=61","DEVNAME=tty61","SEQNUM=995"]}
{"action":"add","devpath":"/devices/virtual/tty/tty62","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty62","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=62","DEVNAME=tty62","SEQNUM=996"]}
{"action":"add","devpath":"/devices/virtual/tty/tty63","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty63","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=63","DEVNAME=tty63","SEQNUM=997"]}
{"action":"add","devpath":"/devices/virtual/tty/tty7","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty7","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=7","DEVNAME=tty7","SEQNUM=998"]}
{"action":"add","devpath":"/devices/virtual/tty/tty8","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty8","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=8","DEVNAME=tty8","SEQNUM=999"]}
{"action":"add","devpath":"/devices/virtual/tty/tty9","env":["ACTION=add","DEVPATH=/devices/virtual/tty/tty9","SUBSYSTEM=tty","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=4","MINOR=9","DEVNAME=tty9","SEQNUM=1000"]}
{"action":"add","devpath":"/devices/virtual/vc/vcs","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcs","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=0","DEVNAME=vcs","SEQNUM=1001"]}
{"action":"add","devpath":"/devices/virtual/vc/vcs1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcs1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=1","DEVNAME=vcs1","SEQNUM=1002"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsa","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsa","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=128","DEVNAME=vcsa","SEQNUM=1003"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsa1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsa1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=129","DEVNAME=vcsa1","SEQNUM=1004"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsu","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsu","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=64","DEVNAME=vcsu","SEQNUM=1005"]}
{"action":"add","devpath":"/devices/virtual/vc/vcsu1","env":["ACTION=add","DEVPATH=/devices/virtual/vc/vcsu1","SUBSYSTEM=vc","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","MAJOR=7","MINOR=65","DEVNAME=vcsu1","SEQNUM=1006"]}
{"action":"add","devpath":"/devices/virtual/vtconsole/vtcon0","env":["ACTION=add","DEVPATH=/devices/virtual/vtconsole/vtcon0","SUBSYSTEM=vtconsole","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1007"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-auth-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-auth-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1008"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-delete-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-delete-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1009"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-reset-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-reset-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1010"]}
{"action":"add","devpath":"/devices/virtual/workqueue/nvme-wq","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/nvme-wq","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1011"]}
{"action":"add","devpath":"/devices/virtual/workqueue/scsi_tmf_0","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/scsi_tmf_0","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1012"]}
{"action":"add","devpath":"/devices/virtual/workqueue/scsi_tmf_1","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/scsi_tmf_1","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1013"]}
{"action":"add","devpath":"/devices/virtual/workqueue/writeback","env":["ACTION=add","DEVPATH=/devices/virtual/workqueue/writeback","SUBSYSTEM=workqueue","SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed","SEQNUM=1014"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1015"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1016"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1017"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1018"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1019"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1020"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1021"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1022"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1023"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","SEQNUM=1024"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1025"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1026"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1027"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","SEQNUM=1028"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1029"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1030"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:07/wakeup/wakeup8","SUBSYSTEM=wakeup","SEQNUM=1031"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1032"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:04.0/virtio1","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:04.0/virtio1","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1033"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:04.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:04.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:04.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1034"]}
{"action":"add","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=add","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1035"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1036"]}
{"action":"add","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=add","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1037"]}
{"action":"bind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=bind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","DRIVER=virtio-pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1038"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0/virtio2","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0/virtio2","SUBSYSTEM=virtio","MODALIAS=virtio:d00000019v00001AF4","SEQNUM=1039"]}
{"action":"unbind","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=unbind","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","SEQNUM=1040"]}
{"action":"remove","devpath":"/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","env":["ACTION=remove","DEVPATH=/devices/LNXSYSTM:00/LNXSYBUS:00/PNP0A03:00/device:08/wakeup/wakeup9","SUBSYSTEM=wakeup","SEQNUM=1041"]}
{"action":"remove","devpath":"/devices/pci0000:00/0000:00:05.0","env":["ACTION=remove","DEVPATH=/devices/pci0000:00/0000:00:05.0","SUBSYSTEM=pci","PCI_CLASS=40100","PCI_ID=1AF4:1059","PCI_SUBSYS_ID=1AF4:1100","PCI_SLOT_NAME=0000:00:05.0","MODALIAS=pci:v00001AF4d00001059sv00001AF4sd00001100bc04sc01i00","SEQNUM=1042"]}
-1
View File
@@ -1 +0,0 @@
{"action":"move","devpath":"/devices/virtual/net/_lo","env":["ACTION=move","DEVPATH=/devices/virtual/net/_lo","SUBSYSTEM=net","DEVPATH_OLD=/devices/virtual/net/lo","INTERFACE=_lo","IFINDEX=1","SEQNUM=1043"]}
-8
View File
@@ -1,8 +0,0 @@
{"action":"remove","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=remove","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1044"]}
{"action":"offline","devpath":"/devices/system/cpu/cpu0","env":["ACTION=offline","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1045"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1046"]}
{"action":"online","devpath":"/devices/system/cpu/cpu0","env":["ACTION=online","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1047"]}
{"action":"remove","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=remove","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1048"]}
{"action":"offline","devpath":"/devices/system/cpu/cpu0","env":["ACTION=offline","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1049"]}
{"action":"add","devpath":"/devices/system/machinecheck/machinecheck0","env":["ACTION=add","DEVPATH=/devices/system/machinecheck/machinecheck0","SUBSYSTEM=machinecheck","SEQNUM=1050"]}
{"action":"online","devpath":"/devices/system/cpu/cpu0","env":["ACTION=online","DEVPATH=/devices/system/cpu/cpu0","SUBSYSTEM=cpu","DRIVER=processor","MODALIAS=cpu:type:x86,ven0002fam000Fmod006B:feature:,0000,0002,0003,0004,0005,0006,0007,0008,0009,000B,000C,000D,000E,000F,0010,0011,0013,0017,0018,0019,001A,001C,0020,0022,0023,0024,0025,0026,0027,0028,0029,002B,002C,002D,002E,002F,0030,0031,0034,0037,0038,003D,0064,006E,0070,0074,0075,0076,0079,007A,007F,0080,008D,0095,009F,00C0,00C1,00C8,00ED,00F3,010F,0115,0165,016C,0282\n","SEQNUM=1051"]}
-19
View File
@@ -1,19 +0,0 @@
{"action":"add","devpath":"/devices/virtual/misc/loop-control","env":["ACTION=add","DEVPATH=/devices/virtual/misc/loop-control","SUBSYSTEM=misc","MAJOR=10","MINOR=237","DEVNAME=loop-control","SEQNUM=1052"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:0","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:0","SUBSYSTEM=bdi","SEQNUM=1053"]}
{"action":"add","devpath":"/devices/virtual/block/loop0","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=2","SEQNUM=1054"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:1","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:1","SUBSYSTEM=bdi","SEQNUM=1055"]}
{"action":"add","devpath":"/devices/virtual/block/loop1","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop1","SUBSYSTEM=block","MAJOR=7","MINOR=1","DEVNAME=loop1","DEVTYPE=disk","DISKSEQ=3","SEQNUM=1056"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:2","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:2","SUBSYSTEM=bdi","SEQNUM=1057"]}
{"action":"add","devpath":"/devices/virtual/block/loop2","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop2","SUBSYSTEM=block","MAJOR=7","MINOR=2","DEVNAME=loop2","DEVTYPE=disk","DISKSEQ=4","SEQNUM=1058"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:3","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:3","SUBSYSTEM=bdi","SEQNUM=1059"]}
{"action":"add","devpath":"/devices/virtual/block/loop3","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop3","SUBSYSTEM=block","MAJOR=7","MINOR=3","DEVNAME=loop3","DEVTYPE=disk","DISKSEQ=5","SEQNUM=1060"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:4","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:4","SUBSYSTEM=bdi","SEQNUM=1061"]}
{"action":"add","devpath":"/devices/virtual/block/loop4","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop4","SUBSYSTEM=block","MAJOR=7","MINOR=4","DEVNAME=loop4","DEVTYPE=disk","DISKSEQ=6","SEQNUM=1062"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:5","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:5","SUBSYSTEM=bdi","SEQNUM=1063"]}
{"action":"add","devpath":"/devices/virtual/block/loop5","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop5","SUBSYSTEM=block","MAJOR=7","MINOR=5","DEVNAME=loop5","DEVTYPE=disk","DISKSEQ=7","SEQNUM=1064"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:6","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:6","SUBSYSTEM=bdi","SEQNUM=1065"]}
{"action":"add","devpath":"/devices/virtual/block/loop6","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop6","SUBSYSTEM=block","MAJOR=7","MINOR=6","DEVNAME=loop6","DEVTYPE=disk","DISKSEQ=8","SEQNUM=1066"]}
{"action":"add","devpath":"/devices/virtual/bdi/7:7","env":["ACTION=add","DEVPATH=/devices/virtual/bdi/7:7","SUBSYSTEM=bdi","SEQNUM=1067"]}
{"action":"add","devpath":"/devices/virtual/block/loop7","env":["ACTION=add","DEVPATH=/devices/virtual/block/loop7","SUBSYSTEM=block","MAJOR=7","MINOR=7","DEVNAME=loop7","DEVTYPE=disk","DISKSEQ=9","SEQNUM=1068"]}
{"action":"add","devpath":"/module/loop","env":["ACTION=add","DEVPATH=/module/loop","SUBSYSTEM=module","SEQNUM=1069"]}
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1070"]}
-2
View File
@@ -1,2 +0,0 @@
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1071"]}
{"action":"change","devpath":"/devices/virtual/block/loop0","env":["ACTION=change","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","DISK_MEDIA_CHANGE=1","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=10","SEQNUM=1072"]}
-18
View File
@@ -1,18 +0,0 @@
{"action":"remove","devpath":"/devices/virtual/misc/loop-control","env":["ACTION=remove","DEVPATH=/devices/virtual/misc/loop-control","SUBSYSTEM=misc","MAJOR=10","MINOR=237","DEVNAME=loop-control","SEQNUM=1073"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:0","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:0","SUBSYSTEM=bdi","SEQNUM=1074"]}
{"action":"remove","devpath":"/devices/virtual/block/loop0","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop0","SUBSYSTEM=block","MAJOR=7","MINOR=0","DEVNAME=loop0","DEVTYPE=disk","DISKSEQ=11","SEQNUM=1075"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:1","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:1","SUBSYSTEM=bdi","SEQNUM=1076"]}
{"action":"remove","devpath":"/devices/virtual/block/loop1","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop1","SUBSYSTEM=block","MAJOR=7","MINOR=1","DEVNAME=loop1","DEVTYPE=disk","DISKSEQ=3","SEQNUM=1077"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:2","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:2","SUBSYSTEM=bdi","SEQNUM=1078"]}
{"action":"remove","devpath":"/devices/virtual/block/loop2","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop2","SUBSYSTEM=block","MAJOR=7","MINOR=2","DEVNAME=loop2","DEVTYPE=disk","DISKSEQ=4","SEQNUM=1079"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:3","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:3","SUBSYSTEM=bdi","SEQNUM=1080"]}
{"action":"remove","devpath":"/devices/virtual/block/loop3","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop3","SUBSYSTEM=block","MAJOR=7","MINOR=3","DEVNAME=loop3","DEVTYPE=disk","DISKSEQ=5","SEQNUM=1081"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:4","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:4","SUBSYSTEM=bdi","SEQNUM=1082"]}
{"action":"remove","devpath":"/devices/virtual/block/loop4","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop4","SUBSYSTEM=block","MAJOR=7","MINOR=4","DEVNAME=loop4","DEVTYPE=disk","DISKSEQ=6","SEQNUM=1083"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:5","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:5","SUBSYSTEM=bdi","SEQNUM=1084"]}
{"action":"remove","devpath":"/devices/virtual/block/loop5","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop5","SUBSYSTEM=block","MAJOR=7","MINOR=5","DEVNAME=loop5","DEVTYPE=disk","DISKSEQ=7","SEQNUM=1085"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:6","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:6","SUBSYSTEM=bdi","SEQNUM=1086"]}
{"action":"remove","devpath":"/devices/virtual/block/loop6","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop6","SUBSYSTEM=block","MAJOR=7","MINOR=6","DEVNAME=loop6","DEVTYPE=disk","DISKSEQ=8","SEQNUM=1087"]}
{"action":"remove","devpath":"/devices/virtual/bdi/7:7","env":["ACTION=remove","DEVPATH=/devices/virtual/bdi/7:7","SUBSYSTEM=bdi","SEQNUM=1088"]}
{"action":"remove","devpath":"/devices/virtual/block/loop7","env":["ACTION=remove","DEVPATH=/devices/virtual/block/loop7","SUBSYSTEM=block","MAJOR=7","MINOR=7","DEVNAME=loop7","DEVTYPE=disk","DISKSEQ=9","SEQNUM=1089"]}
{"action":"remove","devpath":"/module/loop","env":["ACTION=remove","DEVPATH=/module/loop","SUBSYSTEM=module","SEQNUM=1090"]}
+1 -1
View File
@@ -40,7 +40,7 @@ func TestTransform(t *testing.T) {
const maxChunkWords = 8 << 10 const maxChunkWords = 8 << 10
buf := make([]byte, 2*maxChunkWords*8) buf := make([]byte, 2*maxChunkWords*8)
for i := range uint64(2 * maxChunkWords) { for i := uint64(0); i < 2*maxChunkWords; i++ {
binary.LittleEndian.PutUint64(buf[i*8:], i) binary.LittleEndian.PutUint64(buf[i*8:], i)
} }
if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil { if err := lockedfile.Write(path, bytes.NewReader(buf[:8]), 0666); err != nil {
+2 -1
View File
@@ -58,7 +58,8 @@ func (k *outcome) finalise(
supp := make([]string, len(config.Groups)) supp := make([]string, len(config.Groups))
for i, name := range config.Groups { for i, name := range config.Groups {
if gid, err := k.lookupGroupId(name); err != nil { if gid, err := k.lookupGroupId(name); err != nil {
if unknownGroupError, ok := errors.AsType[user.UnknownGroupError](err); ok { var unknownGroupError user.UnknownGroupError
if errors.As(err, &unknownGroupError) {
return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError) return newWithMessageError(fmt.Sprintf("unknown group %q", name), unknownGroupError)
} else { } else {
return &hst.AppError{Step: "look up group by name", Err: err, Msg: err.Error()} return &hst.AppError{Step: "look up group by name", Err: err, Msg: err.Error()}
+6 -4
View File
@@ -51,16 +51,18 @@ func (h *Hsu) ID() (int, error) {
cmd.Stderr = os.Stderr // pass through fatal messages cmd.Stderr = os.Stderr // pass through fatal messages
cmd.Env = make([]string, 0) cmd.Env = make([]string, 0)
cmd.Dir = fhs.Root cmd.Dir = fhs.Root
var p []byte var (
p []byte
exitError *exec.ExitError
)
const step = "obtain uid from hsu" const step = "obtain uid from hsu"
if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil { if p, h.idErr = h.k.cmdOutput(cmd); h.idErr == nil {
h.id, h.idErr = strconv.Atoi(string(p)) h.id, h.idErr = strconv.Atoi(string(p))
if h.idErr != nil { if h.idErr != nil {
h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"} h.idErr = &hst.AppError{Step: step, Err: h.idErr, Msg: "invalid uid string from hsu"}
} }
} else if exitError, ok := errors.AsType[*exec.ExitError](h.idErr); ok && } else if errors.As(h.idErr, &exitError) && exitError != nil && exitError.ExitCode() == 1 {
exitError != nil &&
exitError.ExitCode() == 1 {
// hsu prints an error message in this case // hsu prints an error message in this case
h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess} h.idErr = &hst.AppError{Step: step, Err: ErrHsuAccess}
} else if errors.Is(h.idErr, os.ErrNotExist) { } else if errors.Is(h.idErr, os.ErrNotExist) {
+3 -3
View File
@@ -328,11 +328,11 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
} }
if err := k.sys.Revert((*system.Criteria)(&ec)); err != nil { if err := k.sys.Revert((*system.Criteria)(&ec)); err != nil {
joinError, ok := errors.AsType[interface { var joinError interface {
Unwrap() []error Unwrap() []error
error error
}](err) }
if !ok || joinError == nil { if !errors.As(err, &joinError) || joinError == nil {
perror(err, "revert system setup") perror(err, "revert system setup")
} else { } else {
for _, v := range joinError.Unwrap() { for _, v := range joinError.Unwrap() {
-1
View File
@@ -136,7 +136,6 @@ func TestOutcomeRun(t *testing.T) {
Tmpfs(fhs.AbsDevShm, 0, 01777). Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp // spRuntimeOp
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable). Bind(m("/tmp/hakurei.0/runtime/9"), m("/run/user/1971"), std.BindWritable).
+2 -2
View File
@@ -390,8 +390,8 @@ func shimEntrypoint(k syscallDispatcher) {
if err := k.containerWait(z); err != nil { if err := k.containerWait(z); err != nil {
sp.destroy() sp.destroy()
exitError, ok := errors.AsType[*exec.ExitError](err) var exitError *exec.ExitError
if !ok { if !errors.As(err, &exitError) {
if errors.Is(err, context.Canceled) { if errors.Is(err, context.Canceled) {
k.exit(hst.ExitCancel) k.exit(hst.ExitCancel)
} }
-1
View File
@@ -71,7 +71,6 @@ func TestShimEntrypoint(t *testing.T) {
Tmpfs(fhs.AbsDevShm, 0, 01777). Tmpfs(fhs.AbsDevShm, 0, 01777).
// spRuntimeOp // spRuntimeOp
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable). Bind(m("/tmp/hakurei.10/runtime/9999"), m("/run/user/1000"), std.BindWritable).
-4
View File
@@ -382,10 +382,6 @@ func (p opsAdapter) Overlay(target, state, work *check.Absolute, layers ...*chec
return opsAdapter{p.Ops.Overlay(target, state, work, layers...)} return opsAdapter{p.Ops.Overlay(target, state, work, layers...)}
} }
func (p opsAdapter) OverlayEphemeral(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayEphemeral(target, layers...)}
}
func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops { func (p opsAdapter) OverlayReadonly(target *check.Absolute, layers ...*check.Absolute) hst.Ops {
return opsAdapter{p.Ops.OverlayReadonly(target, layers...)} return opsAdapter{p.Ops.OverlayReadonly(target, layers...)}
} }
-3
View File
@@ -113,9 +113,6 @@ func (s *spRuntimeOp) toContainer(state *outcomeStateParams) error {
} }
if state.Container.Flags&hst.FCoverRun != 0 {
state.params.Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755)
}
state.params.Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755) state.params.Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755)
if state.Container.Flags&hst.FShareRuntime != 0 { if state.Container.Flags&hst.FShareRuntime != 0 {
_, runtimeDirInst := s.commonPaths(state.outcomeState) _, runtimeDirInst := s.commonPaths(state.outcomeState)
-4
View File
@@ -40,7 +40,6 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Ops: new(container.Ops). Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable), Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{ }, paramsWantEnv(config, map[string]string{
@@ -68,7 +67,6 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Ops: new(container.Ops). Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable), Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{ }, paramsWantEnv(config, map[string]string{
@@ -96,7 +94,6 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Ops: new(container.Ops). Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable), Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{ }, paramsWantEnv(config, map[string]string{
@@ -120,7 +117,6 @@ func TestSpRuntimeOp(t *testing.T) {
// this op configures the container state and does not make calls during toContainer // this op configures the container state and does not make calls during toContainer
}, &container.Params{ }, &container.Params{
Ops: new(container.Ops). Ops: new(container.Ops).
Tmpfs(fhs.AbsRun, xdgRuntimeDirSize, 0755).
Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755). Tmpfs(fhs.AbsRunUser, xdgRuntimeDirSize, 0755).
Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable), Bind(m("/proc/nonexistent/tmp/hakurei.0/runtime/9"), m("/run/user/1000"), std.BindWritable),
}, paramsWantEnv(config, map[string]string{ }, paramsWantEnv(config, map[string]string{
+4 -4
View File
@@ -176,8 +176,8 @@ func marshalValueAppendRaw(data []byte, v reflect.Value) ([]byte, error) {
case reflect.Struct: case reflect.Struct:
data = SPA_TYPE_Struct.append(data) data = SPA_TYPE_Struct.append(data)
var err error var err error
for _, field := range v.Fields() { for i := 0; i < v.NumField(); i++ {
data, err = marshalValueAppend(data, field) data, err = marshalValueAppend(data, v.Field(i))
if err != nil { if err != nil {
return data, err return data, err
} }
@@ -370,8 +370,8 @@ func unmarshalValue(data []byte, v reflect.Value, wireSizeP *Word) error {
} }
var fieldWireSize Word var fieldWireSize Word
for _, field := range v.Fields() { for i := 0; i < v.NumField(); i++ {
if err := unmarshalValue(data, field, &fieldWireSize); err != nil { if err := unmarshalValue(data, v.Field(i), &fieldWireSize); err != nil {
return err return err
} }
// bounds check completed in successful call to unmarshalValue // bounds check completed in successful call to unmarshalValue
-405
View File
@@ -1,405 +0,0 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"fmt"
"io"
"io/fs"
"os"
"path/filepath"
"unsafe"
"hakurei.app/check"
)
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// An ArchiveHeader represents a single header in an archive.
type ArchiveHeader struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Size uint64 // size of data segment
}
// Writer implements sequential writing of an archive. [Writer.WriteHeader]
// begins a new file with the provided [ArchiveHeader], and then Writer can be
// treated as an [io.Writer] to supply that file's data.
//
// It is the caller's responsibility to write entries in lexical order.
type Writer struct {
// Underlying writer.
w io.Writer
// Current header.
h ArchiveHeader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Current position in data segment.
n uint64
}
// NewWriter returns the address of a new [Writer] writing to w.
func NewWriter(w io.Writer) *Writer { return &Writer{w: w} }
var zero [wordSize]byte
// padSize returns the padding size for aligning sz.
func padSize[T int | uint64](sz T) T {
return (wordSize - (sz)%wordSize) % wordSize
}
// flush concludes writing to the current file and writes padding.
func (aw *Writer) flush() error {
if aw.h.Size > aw.n {
return fmt.Errorf("missed writing %d bytes", aw.h.Size-aw.n)
} else if aw.h.Size < aw.n {
return fmt.Errorf("wrote %d bytes beyond end of file", aw.n-aw.h.Size)
}
if psz := padSize(aw.h.Size); psz != 0 {
if _, err := aw.w.Write(zero[:psz]); err != nil {
return err
}
}
aw.n = 0
return nil
}
// WriteHeader writes h and begins accepting its corresponding file.
func (aw *Writer) WriteHeader(h *ArchiveHeader) error {
if err := aw.flush(); err != nil {
return err
}
aw.h = *h
binary.LittleEndian.PutUint32(aw.buf[:], uint32(aw.h.Mode))
binary.LittleEndian.PutUint32(aw.buf[wordSize/2:], uint32(len(aw.h.Path)))
binary.LittleEndian.PutUint64(aw.buf[wordSize:], aw.h.Size)
if _, err := aw.w.Write(aw.buf[:]); err != nil {
return err
} else if _, err = aw.w.Write(
unsafe.Slice(unsafe.StringData(aw.h.Path), len(aw.h.Path)),
); err != nil {
return err
} else if psz := padSize(len(aw.h.Path)); psz != 0 {
if _, err = aw.w.Write(zero[:psz]); err != nil {
return err
}
}
return nil
}
// Write writes p to the underlying writer and records the new position. Invalid
// positions are reported by WriteHeader and Close.
func (aw *Writer) Write(p []byte) (n int, err error) {
n, err = aw.w.Write(p)
aw.n += uint64(n)
return
}
// Close concludes writing to the archive stream.
func (aw *Writer) Close() (err error) {
err = aw.flush()
aw.w = nil
return
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Reader implements sequential reading of an archive. [Reader.Next] advances to
// the next file in the archive (including the first), and then Reader can be
// treated as an [io.Reader] to access the file's data.
type Reader struct {
// Underlying reader.
r io.Reader
// Fixed-size header segment.
buf [wordSize * 2]byte
// Remaining bytes in current data segment.
n, pad uint64
}
// NewReader returns the address of a new [Reader] reading from r.
func NewReader(r io.Reader) *Reader { return &Reader{r: r} }
// Next advances ar to the next entry. Remaining bytes of the current data
// segment are discarded. Advancing beyond the final entry returns [io.EOF].
func (ar *Reader) Next() (*ArchiveHeader, error) {
if dsz := int64(ar.n + ar.pad); dsz > 0 {
if n, err := io.CopyN(io.Discard, ar.r, dsz); err != nil {
if errors.Is(err, io.EOF) && n != dsz {
err = io.ErrUnexpectedEOF
}
return nil, err
}
}
if _, err := io.ReadFull(ar.r, ar.buf[:]); err != nil {
return nil, err
}
h := ArchiveHeader{
Mode: fs.FileMode(binary.LittleEndian.Uint32(ar.buf[:])),
Size: binary.LittleEndian.Uint64(ar.buf[wordSize:]),
}
pathSize := int(binary.LittleEndian.Uint32(ar.buf[wordSize/2:]))
pPathSize := alignSize(pathSize)
buf := make([]byte, pPathSize)
if _, err := io.ReadFull(ar.r, buf); err != nil {
if errors.Is(err, io.EOF) {
err = io.ErrUnexpectedEOF
}
return nil, err
}
h.Path = unsafe.String(unsafe.SliceData(buf), pathSize)
if !filepath.IsLocal(h.Path) {
return &h, ErrInsecurePath
}
ar.n = h.Size
ar.pad = padSize(h.Size)
return &h, nil
}
// Read implements [io.Reader] for the data segment of the current entry.
func (ar *Reader) Read(p []byte) (n int, err error) {
if uint64(len(p)) > ar.n {
p = p[:ar.n]
}
if len(p) > 0 {
n, err = ar.r.Read(p)
ar.n -= uint64(n)
}
switch err {
case io.EOF:
if ar.n > 0 {
return n, io.ErrUnexpectedEOF
}
case nil:
if ar.n == 0 {
return n, io.EOF
}
}
return
}
// Write writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Write(fsys fs.FS, root string, w io.Writer) error {
aw := NewWriter(w)
if err := fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
h := ArchiveHeader{
Path: path,
Mode: fi.Mode(),
}
if h.Mode.IsRegular() {
h.Size = uint64(fi.Size())
if err = aw.WriteHeader(&h); err != nil {
return err
}
var r fs.File
r, err = fsys.Open(path)
if err != nil {
return err
}
_, err = io.Copy(aw, r)
if _err := r.Close(); err == nil {
err = _err
}
return err
} else if h.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
h.Size = uint64(len(newpath))
if err = aw.WriteHeader(&h); err != nil {
return err
}
_, err = aw.Write(unsafe.Slice(unsafe.StringData(newpath), len(newpath)))
return err
} else if !h.Mode.IsDir() {
return InvalidFileModeError(h.Mode)
}
return aw.WriteHeader(&h)
}); err != nil {
return err
}
return aw.Close()
}
// SumFS saves checksum of the archive of fsys to the value pointed to by buf.
func SumFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if err := Write(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// SumDir saves checksum of the archive of directory at pathname to the value
// pointed to by buf.
func SumDir(buf *Checksum, pathname *check.Absolute) error {
return SumFS(buf, os.DirFS(pathname.String()), ".")
}
// archiveArtifact is an [Artifact] unpacking an archive supported by [Reader]
// backed by a [FileArtifact].
type archiveArtifact struct {
// Caller-supplied backing archive.
f Artifact
}
// NewArchive returns a new [Artifact] backed by the supplied [Artifact]. The
// source [Artifact] must be a [FileArtifact] and produce a stream compatible
// with [Reader].
func NewArchive(a Artifact) Artifact {
return archiveArtifact{a}
}
// Kind returns the hardcoded [Kind] constant.
func (archiveArtifact) Kind() Kind { return KindArchive }
// Params is a noop.
func (archiveArtifact) Params(*IContext) {}
func init() {
register(KindArchive, func(r *IRReader) Artifact {
a := NewArchive(r.Next())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a archiveArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: [Reader] is fully sequential.
func (archiveArtifact) IsExclusive() bool { return false }
// Cure cures the [Artifact], producing a directory located at work.
func (a archiveArtifact) Cure(t *TContext) (err error) {
var r io.ReadCloser
if r, err = t.Open(a.f); err != nil {
return
}
defer func() {
closeErr := r.Close()
if err == nil {
err = closeErr
}
}()
type dirTargetPerm struct {
path string
mode fs.FileMode
}
var madeDirectories []dirTargetPerm
if err = os.MkdirAll(t.GetWorkDir().String(), 0700); err != nil {
return
}
var root *os.Root
if root, err = os.OpenRoot(t.GetWorkDir().String()); err != nil {
return
}
defer func() {
closeErr := root.Close()
if err == nil {
err = closeErr
}
}()
var header *ArchiveHeader
ar := NewReader(r)
for header, err = ar.Next(); err == nil; header, err = ar.Next() {
if header.Mode.IsRegular() {
var f *os.File
if f, err = root.OpenFile(
header.Path,
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
header.Mode.Perm(),
); err != nil {
return
}
if _, err = io.Copy(f, ar); err != nil {
_ = f.Close()
return
} else if err = f.Close(); err != nil {
return
}
} else if header.Mode&fs.ModeSymlink != 0 {
var p []byte
if p, err = io.ReadAll(ar); err != nil {
return
}
if err = root.Symlink(
unsafe.String(unsafe.SliceData(p), len(p)),
header.Path,
); err != nil {
return
}
} else if header.Mode.IsDir() {
if header.Path == "." {
continue
}
madeDirectories = append(madeDirectories, dirTargetPerm{
path: header.Path,
mode: header.Mode,
})
if err = root.Mkdir(header.Path, 0700); err != nil {
return
}
} else {
return InvalidFileModeError(header.Mode)
}
}
if errors.Is(err, io.EOF) {
err = nil
}
if err == nil {
for _, e := range madeDirectories {
if err = root.Chmod(e.path, e.mode.Perm()); err != nil {
return
}
}
} else {
return
}
return
}
-240
View File
@@ -1,240 +0,0 @@
package pkg_test
import (
"bytes"
"io"
"io/fs"
"maps"
"reflect"
"testing"
"testing/fstest"
"unsafe"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestArchive(t *testing.T) {
t.Parallel()
type entry struct {
path string
mode fs.FileMode
data string
}
testCases := []struct {
name string
fsys fs.FS
entries []entry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"block", fs.ModeDir | 0700, ""},
{"block/uevent", 0600, ""},
{"devices", fs.ModeDir | 0700, ""},
{"devices/empty", fs.ModeDir | 0700, ""},
{"devices/sub", fs.ModeDir | 0700, ""},
{"devices/sub/uevent", 0600, "add"},
{"devices/uevent", 0600, "add"},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []entry{
{".", fs.ModeDir | 0700, ""},
{"checksum", fs.ModeDir | 0700, ""},
{"identifier", fs.ModeDir | 0700, ""},
{"work", fs.ModeDir | 0700, ""},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []entry{
{".", fs.ModeDir | 0500, ""},
{"lib", fs.ModeDir | 0500, ""},
{"lib/check", 0400, ""},
{"lib/pkgconfig", fs.ModeDir | 0500, ""},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if err := pkg.Write(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
r := pkg.NewReader(bytes.NewReader(buf.Bytes()))
var got []entry
for {
h, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
t.Fatalf("Next: error = %v", err)
}
var data []byte
if data, err = io.ReadAll(r); err != nil {
t.Fatalf("Read: error = %v", err)
}
got = append(got, entry{
path: h.Path,
mode: h.Mode,
data: unsafe.String(unsafe.SliceData(data), len(data)),
})
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Reader: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.SumFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("SumFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("SumFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
var archiveTestdata = fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600},
}
func TestArchiveArtifact(t *testing.T) {
t.Parallel()
want := maps.Clone(archiveTestdata)
want["."].Mode = fs.ModeDir | 0500
checkWithCache(t, []cacheTestCase{
{"unpack", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
t.Fatal(err)
}
cureMany(t, c, []cureStep{
{"sample", pkg.NewArchive(
pkg.NewFile("", buf.Bytes()),
), ignorePathname, expectsFS(want), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F": {Mode: fs.ModeDir | 0500},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/block/uevent": {Mode: 0600},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/empty": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub": {Mode: fs.ModeDir | 0700},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F/devices/uevent": {Mode: 0600, Data: []byte("add")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/3oYyAbRJ_we7AgWo1BRcRcnxXFk3mAQ0Qui2nGQMi8GIJNJQtvUC6P2IeoA5mbjD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CBPcoVHuVUTVRCMbRl8J30RSSzm_tyfuXaZ-HlZsanY1sY50meOVmgaWDrGKbx9F")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
func BenchmarkArchiveRead(b *testing.B) {
var buf bytes.Buffer
if err := pkg.Write(archiveTestdata, ".", &buf); err != nil {
b.Fatal(err)
}
testdata := buf.Bytes()
for b.Loop() {
r := pkg.NewReader(bytes.NewReader(testdata))
for {
_, err := r.Next()
if err != nil {
if err == io.EOF {
break
}
b.Fatal(err)
}
}
}
}
func BenchmarkArchiveWrite(b *testing.B) {
for b.Loop() {
if err := pkg.Write(archiveTestdata, ".", io.Discard); err != nil {
b.Fatal(err)
}
}
}
-152
View File
@@ -1,152 +0,0 @@
package pkg
import (
"errors"
"os"
"unique"
)
// Clean destroys checksum backing entries without any identifier or substitute
// entry referring to it. If at least one keep [Artifact] is specified,
// identifier and substitute entries not kept alive by them are destroyed first.
func (c *Cache) Clean(dry, inputs bool, keep ...Artifact) (
[]unique.Handle[ID],
[]unique.Handle[Checksum],
error,
) {
c.identMu.Lock()
defer c.identMu.Unlock()
c.checksumMu.Lock()
defer c.checksumMu.Unlock()
dents, err := os.ReadDir(c.base.Append(dirChecksum).String())
if err != nil {
return nil, nil, err
}
checksums := make(map[unique.Handle[Checksum]]string, len(dents))
var buf Checksum
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
checksums[unique.Make(buf)] = name
}
type identPair struct {
id unique.Handle[ID]
name string
}
dents, err = os.ReadDir(c.base.Append(dirIdentifier).String())
if err != nil {
return nil, nil, err
}
keepIdents := make(map[unique.Handle[ID]]struct{})
if inputs {
for _, id := range Inputs((*Collect)(&keep)) {
keepIdents[id] = struct{}{}
}
} else {
for _, a := range keep {
keepIdents[c.Ident(a)] = struct{}{}
}
}
idents := make([]identPair, 0, len(dents))
for _, dent := range dents {
name := dent.Name()
if err = Decode(&buf, name); err != nil {
return nil, nil, err
}
id := unique.Make(ID(buf))
if _, ok := keepIdents[id]; len(keep) == 0 || ok {
if err = readlinkChecksum(c.base.Append(
dirIdentifier,
name,
), &buf); err != nil {
return nil, nil, err
}
delete(checksums, unique.Make(buf))
continue
}
c.msg.Verbosef("arranging for destruction of %s...", name)
idents = append(idents, identPair{id, name})
}
destroyedIdents := make([]unique.Handle[ID], 0, len(idents))
for _, pair := range idents {
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
pair.name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirIdentifier,
pair.name,
).String()); err != nil {
return destroyedIdents, nil, err
}
}
destroyedIdents = append(destroyedIdents, pair.id)
}
destroyedChecksums := make([]unique.Handle[Checksum], 0, len(checksums))
for checksum, name := range checksums {
if err = c.parent.Err(); err != nil {
return destroyedIdents, destroyedChecksums, err
}
c.msg.Verbosef("destroying checksum %s...", name)
if !dry {
if err = errors.Join(removeAll(c.base.Append(
dirChecksum,
name,
))); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
destroyedChecksums = append(destroyedChecksums, checksum)
}
dents, err = os.ReadDir(c.base.Append(dirSubstitute).String())
if err != nil {
return destroyedIdents, destroyedChecksums, err
}
for _, dent := range dents {
name := dent.Name()
if err = readlinkChecksum(c.base.Append(
dirSubstitute,
name,
), &buf); err != nil {
return destroyedIdents, destroyedChecksums, err
}
if _, ok := checksums[unique.Make(buf)]; !ok {
continue
}
c.msg.Verbosef("destroying substitute %s...", name)
if !dry {
if err = os.Remove(c.base.Append(
dirStatus,
name,
).String()); err != nil && !errors.Is(err, os.ErrNotExist) {
return destroyedIdents, nil, err
}
if err = os.Remove(c.base.Append(
dirSubstitute,
name,
).String()); err != nil {
return destroyedIdents, destroyedChecksums, err
}
}
}
return destroyedIdents, destroyedChecksums, nil
}
-293
View File
@@ -1,293 +0,0 @@
package pkg_test
import (
"bytes"
"crypto/sha512"
"io/fs"
"log"
"os"
"slices"
"strings"
"testing"
"unique"
"hakurei.app/internal/pkg"
"hakurei.app/message"
)
// formatHandles returns a user-facing string representing h.
func formatHandles[T pkg.ID | pkg.Checksum](handles ...unique.Handle[T]) string {
var buf strings.Builder
for _, h := range handles {
buf.WriteString(pkg.Encode(pkg.Checksum(h.Value())))
buf.WriteString(", ")
}
return strings.TrimSuffix(buf.String(), ", ")
}
func TestClean(t *testing.T) {
t.Parallel()
ic := pkg.NewIR()
testCases := []struct {
name string
a []pkg.Artifact
keep []pkg.Artifact
inputs bool
want expectsFS
wantIdents []unique.Handle[pkg.ID]
wantChecksums []unique.Handle[pkg.Checksum]
}{
{"simple", []pkg.Artifact{
pkg.NewFile("file", nil),
}, nil, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/pPRjw2XYgjB5k8dYedwxTBMgHh4_v2JM_G2Vd-skQbAGOOgPsl3CGSUbEF7om_MO": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, nil, nil},
{"keep", []pkg.Artifact{
pkg.NewFile("removed-file", []byte("removed file")),
}, []pkg.Artifact{
pkg.NewFile("file", []byte("\xfd")),
}, false, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk": {Mode: 0400, Data: []byte("\xfd")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/FMwSBYw22KqM8jZryfY2ChHXpLuVDdWYyNOYdHvIVYk8ujY6UnGRm5brr2sTTfpD": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/KgZ-FjbGuU-XP2QEHInpgv-2Zn0cTH5NqFMgTU0XrSdKmSwyC-3baVs1BMCP5spk")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("removed-file", []byte("removed file"))),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("removed file"))),
}},
{"inputs anchored substitute", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
{"inputs", []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("destroyed")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
},
}, []pkg.Artifact{
&stubArtifactF{
kind: pkg.KindExec,
params: []byte("kept"),
deps: []pkg.Artifact{
pkg.NewFile("kept-input", []byte("kept")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("kept")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), []byte{0}, 0444)
},
},
}, true, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W": {Mode: fs.ModeDir | 0500},
"checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W/result": {Mode: 0444, Data: []byte("\x00")},
"checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv": {Mode: 0400, Data: []byte("kept")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/Ef8KX6s_rS_nLgze0rj90zKyrGAvOnzyU0DL7nrYQWEG_f4a9pmUKI6HBEMD8AE8": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/H-eSiCo227-xdqyNl2R-5G3eqXPtbb8XegAB70I5OQb2majeZXJoCxTq9wJy5qqv")},
"identifier/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"lock": {Mode: 0644},
"variant": {Mode: 0400},
"status": {Mode: fs.ModeDir | 0700},
"status/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"status/xoIGLemzLF227e-w_AJcf_1Sgqh2gs3KFgqvOIWUQE-9P_y2vHBMBytL4GRGQqTb": {Mode: 0400, Data: []byte(statusHeader + "kept")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/4bjS-QjGcSV4nth-W6Vg3-wolKmKgiq4Ld2oRIWcOfy6Wi41XXLAWPoo8FcDx6BH": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/CyDnDvF-LaeGPcSW70tPosNCoclByWkTjznUUF1DcgzlIwkN9yzz1ZFME1TlPj6W")},
"fault": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []unique.Handle[pkg.ID]{
ic.Ident(pkg.NewFile("destroyed-input", []byte("destroyed"))),
ic.Ident(&stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
}),
}, []unique.Handle[pkg.Checksum]{
unique.Make(expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}.hash()),
unique.Make(sha512.Sum384([]byte("destroyed"))),
}},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
base := makeBase(t)
msg := message.New(log.New(os.Stderr, "clean: ", 0))
msg.SwapVerbose(testing.Verbose())
c, err := pkg.Open(t.Context(), msg, 0, 0, 0, base)
if err != nil {
t.Fatal(err)
}
t.Cleanup(c.Close)
all := pkg.Collect(slices.Concat(tc.a, tc.keep))
if _, _, err = c.Cure(&all); !pkg.IsCollected(err) {
t.Fatal(err)
}
var (
idents []unique.Handle[pkg.ID]
checksums []unique.Handle[pkg.Checksum]
)
idents, checksums, err = c.Clean(false, tc.inputs, tc.keep...)
if err != nil {
t.Fatalf("Clean: error = %v", err)
}
var buf [2]pkg.Checksum
slices.SortFunc(idents, func(a, b unique.Handle[pkg.ID]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
slices.SortFunc(checksums, func(a, b unique.Handle[pkg.Checksum]) int {
buf[0], buf[1] = a.Value(), b.Value()
return bytes.Compare(buf[0][:], buf[1][:])
})
if !slices.Equal(idents, tc.wantIdents) {
t.Errorf(
"Clean: idents = %s, want %s",
formatHandles(idents...), formatHandles(tc.wantIdents...),
)
}
if !slices.Equal(checksums, tc.wantChecksums) {
t.Errorf(
"Clean: checksums = %s, want %s",
formatHandles(checksums...), formatHandles(tc.wantChecksums...),
)
}
want := tc.want.hash()
var checksum pkg.Checksum
if err = pkg.SumDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err)
} else if checksum != want {
t.Error(expectsFrom(base.String()))
}
})
}
}
-119
View File
@@ -1,119 +0,0 @@
package pkg
import (
"compress/bzip2"
"compress/gzip"
"fmt"
"io"
"os"
)
const (
// Gzip denotes a stream compressed via [gzip].
Gzip = iota
// Bzip2 denotes a stream compressed via [bzip2].
Bzip2
)
// A decompressArtifact is a [FileArtifact] decompressing a backing
// [FileArtifact] stream.
type decompressArtifact struct {
// Caller-supplied backing stream.
f Artifact
// Compression on top of the stream.
compress uint32
}
var _ FileArtifact = new(decompressArtifact)
// decompressArtifactNamed embeds decompressArtifact for a [fmt.Stringer] stream.
type decompressArtifactNamed struct {
decompressArtifact
// Copied from decompressArtifact.f.
name string
}
var _ fmt.Stringer = new(decompressArtifactNamed)
// NewDecompress returns a [FileArtifact] decompressing the supplied [Artifact].
func NewDecompress(a Artifact, compress uint32) FileArtifact {
da := decompressArtifact{a, compress}
if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" {
return &decompressArtifactNamed{da, name}
}
}
return &da
}
// String returns the name of the underlying [Artifact] prefixed with decompress.
func (a *decompressArtifactNamed) String() string { return "decompress-" + a.name }
// Kind returns the hardcoded [Kind] constant.
func (a *decompressArtifact) Kind() Kind { return KindDecompress }
// Params writes value of compression enum.
func (a *decompressArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compress) }
func init() {
register(KindDecompress, func(r *IRReader) Artifact {
a := NewDecompress(r.Next(), r.ReadUint32())
if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum)
}
return a
})
}
// Dependencies returns a slice containing the backing file.
func (a *decompressArtifact) Dependencies() []Artifact {
return []Artifact{a.f}
}
// IsExclusive returns false: decompressor is fully sequential.
func (a *decompressArtifact) IsExclusive() bool { return false }
// compoundCloser is an [io.ReadCloser] with an additional [io.Closer] attached.
type compoundCloser struct {
io.ReadCloser
c io.Closer
}
// Close closes [io.ReadCloser] and the additional [io.Closer]. It returns the
// non-nil error returned by the underlying [io.ReadCloser], otherwise it
// returns the error returned by the additional [io.Closer].
func (c compoundCloser) Close() error {
err := c.ReadCloser.Close()
if _err := c.c.Close(); err == nil {
err = _err
}
return err
}
// Cure returns a decompressor [io.ReadCloser].
func (a *decompressArtifact) Cure(r *RContext) (io.ReadCloser, error) {
sr, err := r.Open(a.f)
if err != nil {
return nil, err
}
br := r.cache.getReaderRC(sr)
var dr io.ReadCloser
switch a.compress {
case Gzip:
if dr, err = gzip.NewReader(br); err != nil {
_ = br.Close()
return nil, err
}
return compoundCloser{dr, br}, nil
case Bzip2:
return struct {
io.Reader
io.Closer
}{bzip2.NewReader(br), br}, nil
default:
return nil, os.ErrInvalid
}
}
-70
View File
@@ -1,70 +0,0 @@
package pkg_test
import (
"bytes"
"compress/gzip"
"crypto/sha512"
"io/fs"
"net/http"
"testing"
"testing/fstest"
"hakurei.app/check"
"hakurei.app/internal/pkg"
)
func TestDecompress(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
gw := gzip.NewWriter(&buf)
if _, err := gw.Write([]byte{0}); err != nil {
t.Fatal(err)
} else if err = gw.Close(); err != nil {
t.Fatal(err)
}
testdata := buf.String()
var transport http.Transport
client := http.Client{Transport: &transport}
transport.RegisterProtocol("file", http.NewFileTransportFS(fstest.MapFS{
"testdata": {Data: []byte(testdata), Mode: 0400},
}))
testdataChecksum := func() pkg.Checksum {
h := sha512.New384()
h.Write([]byte(testdata))
return (pkg.Checksum)(h.Sum(nil))
}()
checkWithCache(t, []cacheTestCase{
{"decompress", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
cureMany(t, c, []cureStep{
{"close", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
pkg.Checksum{0xfd},
), pkg.Gzip), nil, nil, &pkg.ChecksumMismatchError{
Got: testdataChecksum,
Want: pkg.Checksum{0xfd},
}},
{"gzip", pkg.NewDecompress(pkg.NewHTTPGet(
&client,
"file:///testdata",
testdataChecksum,
), pkg.Gzip), ignorePathname, expectsChecksum(sha512.Sum384([]byte{0})), nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/" + pkg.Encode(sha512.Sum384([]byte{0})): {Mode: 0400, Data: []byte{0}},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/QpjkahDrz7pz-tv0eAGNXR6x9NAtTjWCK5Hr7G1cIZj9rT7bLYJWUQeLD4wamAlF": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/vsAhtPNo4waRNOASwrQwcIPTqb3SBuJOXw2G4T1mNmVZM-wrQTRllmgXqcIIoRcX")},
"substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}},
})
}
+203
View File
@@ -0,0 +1,203 @@
package pkg
import (
"crypto/sha512"
"encoding/binary"
"errors"
"io"
"io/fs"
"math"
"os"
"path/filepath"
"syscall"
"hakurei.app/check"
)
// FlatEntry is a directory entry to be encoded for [Flatten].
type FlatEntry struct {
Mode fs.FileMode // file mode bits
Path string // pathname of the file
Data []byte // file content or symlink destination
}
/*
| mode uint32 | path_sz uint32 |
| data_sz uint64 |
| path string |
| data []byte |
*/
// Encode encodes the entry for transmission or hashing.
func (ent *FlatEntry) Encode(w io.Writer) (n int, err error) {
pPathSize := alignSize(len(ent.Path))
if pPathSize > math.MaxUint32 {
return 0, syscall.E2BIG
}
pDataSize := alignSize(len(ent.Data))
payload := make([]byte, wordSize*2+pPathSize+pDataSize)
binary.LittleEndian.PutUint32(payload, uint32(ent.Mode))
binary.LittleEndian.PutUint32(payload[wordSize/2:], uint32(len(ent.Path)))
binary.LittleEndian.PutUint64(payload[wordSize:], uint64(len(ent.Data)))
copy(payload[wordSize*2:], ent.Path)
copy(payload[wordSize*2+pPathSize:], ent.Data)
return w.Write(payload)
}
// ErrInsecurePath is returned by [FlatEntry.Decode] if validation is requested
// and a nonlocal path is encountered in the stream.
var ErrInsecurePath = errors.New("insecure file path")
// Decode decodes the entry from its representation produced by Encode.
func (ent *FlatEntry) Decode(r io.Reader, validate bool) (n int, err error) {
var nr int
header := make([]byte, wordSize*2)
nr, err = r.Read(header)
n += nr
if err != nil {
if errors.Is(err, io.EOF) && n != 0 {
err = io.ErrUnexpectedEOF
}
return
}
ent.Mode = fs.FileMode(binary.LittleEndian.Uint32(header))
pathSize := int(binary.LittleEndian.Uint32(header[wordSize/2:]))
pPathSize := alignSize(pathSize)
dataSize := int(binary.LittleEndian.Uint64(header[wordSize:]))
pDataSize := alignSize(dataSize)
buf := make([]byte, pPathSize+pDataSize)
nr, err = r.Read(buf)
n += nr
if err != nil {
if errors.Is(err, io.EOF) {
if nr != len(buf) {
err = io.ErrUnexpectedEOF
return
}
} else {
return
}
}
ent.Path = string(buf[:pathSize])
if ent.Mode.IsDir() {
ent.Data = nil
} else {
ent.Data = buf[pPathSize : pPathSize+dataSize]
}
if validate && !filepath.IsLocal(ent.Path) {
err = ErrInsecurePath
}
return
}
// DirScanner provides an efficient interface for reading a stream of encoded
// [FlatEntry]. Successive calls to the Scan method will step through the
// entries in the stream.
type DirScanner struct {
// Underlying reader to scan [FlatEntry] representations from.
r io.Reader
// First non-EOF I/O error, returned by the Err method.
err error
// Entry to store results in. Its address is returned by the Entry method
// and is updated on every call to Scan.
ent FlatEntry
// Validate pathnames during decoding.
validate bool
}
// NewDirScanner returns the address of a new instance of [DirScanner] reading
// from r. The caller must no longer read from r after this function returns.
func NewDirScanner(r io.Reader, validate bool) *DirScanner {
return &DirScanner{r: r, validate: validate}
}
// Err returns the first non-EOF I/O error.
func (s *DirScanner) Err() error {
if errors.Is(s.err, io.EOF) {
return nil
}
return s.err
}
// Entry returns the address to the [FlatEntry] value storing the last result.
func (s *DirScanner) Entry() *FlatEntry { return &s.ent }
// Scan advances to the next [FlatEntry].
func (s *DirScanner) Scan() bool {
if s.err != nil {
return false
}
var n int
n, s.err = s.ent.Decode(s.r, s.validate)
if errors.Is(s.err, io.EOF) {
return n != 0
}
return s.err == nil
}
// Flatten writes a deterministic representation of the contents of fsys to w.
// The resulting data can be hashed to produce a deterministic checksum for the
// directory.
func Flatten(fsys fs.FS, root string, w io.Writer) (n int, err error) {
var nr int
err = fs.WalkDir(fsys, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
var fi fs.FileInfo
fi, err = d.Info()
if err != nil {
return err
}
ent := FlatEntry{
Path: path,
Mode: fi.Mode(),
}
if ent.Mode.IsRegular() {
if ent.Data, err = fs.ReadFile(fsys, path); err != nil {
return err
}
} else if ent.Mode&fs.ModeSymlink != 0 {
var newpath string
if newpath, err = fs.ReadLink(fsys, path); err != nil {
return err
}
ent.Data = []byte(newpath)
} else if !ent.Mode.IsDir() {
return InvalidFileModeError(ent.Mode)
}
nr, err = ent.Encode(w)
n += nr
return err
})
return
}
// HashFS returns a checksum produced by hashing the result of [Flatten].
func HashFS(buf *Checksum, fsys fs.FS, root string) error {
h := sha512.New384()
if _, err := Flatten(fsys, root, h); err != nil {
return err
}
h.Sum(buf[:0])
return nil
}
// HashDir returns a checksum produced by hashing the result of [Flatten].
func HashDir(buf *Checksum, pathname *check.Absolute) error {
return HashFS(buf, os.DirFS(pathname.String()), ".")
}
+134
View File
@@ -0,0 +1,134 @@
package pkg_test
import (
"bytes"
"io/fs"
"reflect"
"testing"
"testing/fstest"
"hakurei.app/internal/pkg"
)
func TestFlatten(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
fsys fs.FS
entries []pkg.FlatEntry
sum pkg.Checksum
err error
}{
{"bad type", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"invalid": {Mode: fs.ModeCharDevice | 0400},
}, nil, pkg.Checksum{}, pkg.InvalidFileModeError(
fs.ModeCharDevice | 0400,
)},
{"coldboot", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"devices": {Mode: fs.ModeDir | 0700},
"devices/uevent": {Mode: 0600, Data: []byte("add")},
"devices/empty": {Mode: fs.ModeDir | 0700},
"devices/sub": {Mode: fs.ModeDir | 0700},
"devices/sub/uevent": {Mode: 0600, Data: []byte("add")},
"block": {Mode: fs.ModeDir | 0700},
"block/uevent": {Mode: 0600, Data: []byte{}},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "block"},
{Mode: 0600, Path: "block/uevent", Data: []byte{}},
{Mode: fs.ModeDir | 0700, Path: "devices"},
{Mode: fs.ModeDir | 0700, Path: "devices/empty"},
{Mode: fs.ModeDir | 0700, Path: "devices/sub"},
{Mode: 0600, Path: "devices/sub/uevent", Data: []byte("add")},
{Mode: 0600, Path: "devices/uevent", Data: []byte("add")},
}, pkg.MustDecode("mEy_Lf5KotThm7OwMx7yTKZh5HCCyaB41pVAvI9uDMgVQFM91iosBLYsRm8bDsX8"), nil},
{"empty", fstest.MapFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"identifier": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0700, Path: "."},
{Mode: fs.ModeDir | 0700, Path: "checksum"},
{Mode: fs.ModeDir | 0700, Path: "identifier"},
{Mode: fs.ModeDir | 0700, Path: "work"},
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C"), nil},
{"sample directory step garbage", fstest.MapFS{
".": {Mode: fs.ModeDir | 0500},
"lib": {Mode: fs.ModeDir | 0500},
"lib/check": {Mode: 0400, Data: []byte{}},
"lib/pkgconfig": {Mode: fs.ModeDir | 0500},
}, []pkg.FlatEntry{
{Mode: fs.ModeDir | 0500, Path: "."},
{Mode: fs.ModeDir | 0500, Path: "lib"},
{Mode: 0400, Path: "lib/check", Data: []byte{}},
{Mode: fs.ModeDir | 0500, Path: "lib/pkgconfig"},
}, pkg.MustDecode("CUx-3hSbTWPsbMfDhgalG4Ni_GmR9TnVX8F99tY_P5GtkYvczg9RrF5zO0jX9XYT"), nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
t.Run("roundtrip", func(t *testing.T) {
t.Parallel()
var buf bytes.Buffer
if _, err := pkg.Flatten(
tc.fsys,
".",
&buf,
); !reflect.DeepEqual(err, tc.err) {
t.Fatalf("Flatten: error = %v, want %v", err, tc.err)
} else if tc.err != nil {
return
}
s := pkg.NewDirScanner(bytes.NewReader(buf.Bytes()), true)
var got []pkg.FlatEntry
for s.Scan() {
got = append(got, *s.Entry())
}
if err := s.Err(); err != nil {
t.Fatalf("Err: error = %v", err)
}
if !reflect.DeepEqual(got, tc.entries) {
t.Fatalf("Scan: %#v, want %#v", got, tc.entries)
}
})
if tc.err != nil {
return
}
t.Run("hash", func(t *testing.T) {
t.Parallel()
var got pkg.Checksum
if err := pkg.HashFS(&got, tc.fsys, "."); err != nil {
t.Fatalf("HashFS: error = %v", err)
} else if got != tc.sum {
t.Fatalf("HashFS: %v", &pkg.ChecksumMismatchError{
Got: got,
Want: tc.sum,
})
}
})
})
}
}
-14
View File
@@ -6,7 +6,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"maps"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -123,15 +122,6 @@ func RegisterArch(arch string, e container.BinfmtEntry) {
binfmt[arch] = e binfmt[arch] = e
} }
// Arch returns a snapshot of currently registered [KindExec] and [KindExecNet]
// binfmt entries.
func Arch() map[string]container.BinfmtEntry {
binfmtMu.RLock()
r := maps.Clone(binfmt)
binfmtMu.RUnlock()
return r
}
const ( const (
// ExecTimeoutDefault replaces out of range [NewExec] timeout values. // ExecTimeoutDefault replaces out of range [NewExec] timeout values.
ExecTimeoutDefault = 15 * time.Minute ExecTimeoutDefault = 15 * time.Minute
@@ -591,7 +581,6 @@ var (
func (c *Cache) EnterExec( func (c *Cache) EnterExec(
ctx context.Context, ctx context.Context,
a Artifact, a Artifact,
hostname string,
retainSession bool, retainSession bool,
stdin io.Reader, stdin io.Reader,
stdout, stderr io.Writer, stdout, stderr io.Writer,
@@ -672,9 +661,6 @@ func (c *Cache) EnterExec(
z.Env = append(z.Env, "TERM="+s) z.Env = append(z.Env, "TERM="+s)
} }
} }
if hostname != "" {
z.Hostname = hostname
}
if err = z.Start(); err != nil { if err = z.Start(); err != nil {
return return
+1 -1
View File
@@ -183,10 +183,10 @@ func TestExec(t *testing.T) {
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}}, "checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400, Data: []byte{}},
"identifier": {Mode: fs.ModeDir | 0700}, "identifier": {Mode: fs.ModeDir | 0700},
"identifier/IY91PCtOpCYy21AaIK0c9f8-Z6fb2_2ewoHWkt4dxoLf0GOrWqS8yAGFLV84b1Dw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/QwS7SmiatdqryQYgESdGw7Yw2PcpNf0vNfpvUA0t92BTlKiUjfCrXyMW17G2X77X": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "identifier/QwS7SmiatdqryQYgESdGw7Yw2PcpNf0vNfpvUA0t92BTlKiUjfCrXyMW17G2X77X": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")}, "identifier/_gAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/" + expected.Offline: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)}, "identifier/" + expected.Offline: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/" + expected.OfflineS: {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantOfflineEncode)},
"identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")}, "identifier/vjz1MHPcGBKV7sjcs8jQP3cqxJ1hgPTiQBMCEHP9BGXjGxd-tJmEmXKaStObo5gK": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"substitute": {Mode: fs.ModeDir | 0700}, "substitute": {Mode: fs.ModeDir | 0700},
@@ -2,7 +2,6 @@ package expected
const ( const (
Offline = "q5ktDTq0miP-VvB2blxqXQeaRXCUWgP_KbC18KNtUDtyoaI_h5mHmGuPMArVEBDs" Offline = "q5ktDTq0miP-VvB2blxqXQeaRXCUWgP_KbC18KNtUDtyoaI_h5mHmGuPMArVEBDs"
OfflineS = "IY91PCtOpCYy21AaIK0c9f8-Z6fb2_2ewoHWkt4dxoLf0GOrWqS8yAGFLV84b1Dw"
OvlRoot = "NacZGXwuRkTvcHaG08a22ujJ8qCWN0RSoFlRSR5FSt0ZcBbJ28FRvkYsHEtX7G8i" OvlRoot = "NacZGXwuRkTvcHaG08a22ujJ8qCWN0RSoFlRSR5FSt0ZcBbJ28FRvkYsHEtX7G8i"
Layers = "WBJDrATtX6rIE5yAu8ePX3WmDF0Tt9kFiue0m3cRnyRoVx1my8a67fh3CAW486oP" Layers = "WBJDrATtX6rIE5yAu8ePX3WmDF0Tt9kFiue0m3cRnyRoVx1my8a67fh3CAW486oP"
Net = "CmYtj2sNB3LHtqiDuck_Lz3MjLLIiwyP8N4NDitQ1Icvv__LVP9p8tm-sHeQaKKp" Net = "CmYtj2sNB3LHtqiDuck_Lz3MjLLIiwyP8N4NDitQ1Icvv__LVP9p8tm-sHeQaKKp"
@@ -2,7 +2,6 @@ package expected
const ( const (
Offline = "WapqyoPxbWSnq07dWHt71mHaJXq99pAjJfFlELlJljSiZMhTFqqlzU1_mN86shSj" Offline = "WapqyoPxbWSnq07dWHt71mHaJXq99pAjJfFlELlJljSiZMhTFqqlzU1_mN86shSj"
OfflineS = "ibQZHcdXgNQ1OiMX1FrburBbGPVvKEHvPilbQCkm_0oV0BQCHomyyTbYNrFMGIwl"
OvlRoot = "V9anFOiRvjGfAeBhLl14AL8TKdWZyD0WTPYe4fS9mOBw8iW5Lmarvt6TG6MV8uWm" OvlRoot = "V9anFOiRvjGfAeBhLl14AL8TKdWZyD0WTPYe4fS9mOBw8iW5Lmarvt6TG6MV8uWm"
Layers = "tKx7JNRoSBdK_7MdzI-nwTNV2wmiPzwYdcd17oLmXKL_iLmUzUiA79qTqdrTasrv" Layers = "tKx7JNRoSBdK_7MdzI-nwTNV2wmiPzwYdcd17oLmXKL_iLmUzUiA79qTqdrTasrv"
Net = "aXyDLzBCJ9XltXZIfetEVsEkrqHfcXuD5XE_FcUnYbN3emwL55N6P8LlHzNfGnM5" Net = "aXyDLzBCJ9XltXZIfetEVsEkrqHfcXuD5XE_FcUnYbN3emwL55N6P8LlHzNfGnM5"
@@ -2,7 +2,6 @@ package expected
const ( const (
Offline = "Z6yXE5gOJScL3srmnVMWgCXccDiUNZ5snSrf6RkXuU1_U0rX_kGVwsfHUgNG_awd" Offline = "Z6yXE5gOJScL3srmnVMWgCXccDiUNZ5snSrf6RkXuU1_U0rX_kGVwsfHUgNG_awd"
OfflineS = "zN16xv6LKRJRipUJwupyxg2rZcvf-qpsMn_qCxUmgxlTSuNwYI70ZEb7dHW5k0gO"
OvlRoot = "zYXJHFRLuxvUhuisZEXgGgVvdQd6piMfp5jmtT6jdVjvC2gICXquOq-UTwlrSD5I" OvlRoot = "zYXJHFRLuxvUhuisZEXgGgVvdQd6piMfp5jmtT6jdVjvC2gICXquOq-UTwlrSD5I"
Layers = "_F8EDazHbcLeT0sVSQXRN_kn9IjduqJcDYgzXpsT-hpKU4EBcZ0PISN2zchpqMbm" Layers = "_F8EDazHbcLeT0sVSQXRN_kn9IjduqJcDYgzXpsT-hpKU4EBcZ0PISN2zchpqMbm"
Net = "CA_FAaSIYJgapBEHV40doxpH23PdUEy_6s1TZc7wfSPN0XYqwGpMceXXDSabGveO" Net = "CA_FAaSIYJgapBEHV40doxpH23PdUEy_6s1TZc7wfSPN0XYqwGpMceXXDSabGveO"
+1 -14
View File
@@ -8,7 +8,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"io" "io"
"iter"
"slices" "slices"
"strconv" "strconv"
"sync" "sync"
@@ -21,7 +20,7 @@ import (
const wordSize = 8 const wordSize = 8
// alignSize returns the padded size for aligning sz. // alignSize returns the padded size for aligning sz.
func alignSize[T int | uint64](sz T) T { func alignSize(sz int) int {
return sz + (wordSize-(sz)%wordSize)%wordSize return sz + (wordSize-(sz)%wordSize)%wordSize
} }
@@ -66,18 +65,6 @@ func NewIR() *IRCache {
return &IRCache{zeroIRCache()} return &IRCache{zeroIRCache()}
} }
// Inputs returns an iterator over direct and transitive inputs of an [Artifact]
// in randomised order.
func Inputs(a Artifact) iter.Seq2[Artifact, unique.Handle[ID]] {
ic := NewIR()
ic.Ident(a)
return func(yield func(Artifact, unique.Handle[ID]) bool) {
ic.artifact.Range(func(key, value any) bool {
return yield(key.(Artifact), value.(unique.Handle[ID]))
})
}
}
// IContext is passed to [Artifact.Params] and provides methods for writing // IContext is passed to [Artifact.Params] and provides methods for writing
// values to the IR writer. It does not expose the underlying [io.Writer]. // values to the IR writer. It does not expose the underlying [io.Writer].
// //
+21 -17
View File
@@ -27,14 +27,16 @@ func TestIRRoundtrip(t *testing.T) {
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
)}, )},
{"http get tar", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( {"http get tar", pkg.NewHTTPGetTar(
nil, "file:///testdata", nil, "file:///testdata",
pkg.Checksum(bytes.Repeat([]byte{0xff}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xff}, len(pkg.Checksum{}))),
), pkg.Bzip2))}, pkg.TarBzip2,
{"http get tar unaligned", pkg.NewTar(pkg.NewHTTPGet( )},
{"http get tar unaligned", pkg.NewHTTPGetTar(
nil, "https://hakurei.app", nil, "https://hakurei.app",
pkg.Checksum(bytes.Repeat([]byte{0xfe}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfe}, len(pkg.Checksum{}))),
))}, pkg.TarUncompressed,
)},
{"exec offline", pkg.NewExec( {"exec offline", pkg.NewExec(
"exec-offline", "", nil, 0, false, false, "exec-offline", "", nil, 0, false, false,
@@ -45,13 +47,15 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte( pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file", "stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet( ))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
nil, "file:///hakurei.tar", nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
nil, "file:///testtool.tar.gz", nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
), pkg.Gzip))), pkg.TarGzip,
)),
)}, )},
{"exec net", pkg.NewExec( {"exec net", pkg.NewExec(
@@ -65,13 +69,15 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte( pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file", "stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet( ))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
nil, "file:///hakurei.tar", nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
nil, "file:///testtool.tar.gz", nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfc}, len(pkg.Checksum{}))),
), pkg.Gzip))), pkg.TarGzip,
)),
)}, )},
{"exec measured", pkg.NewExec( {"exec measured", pkg.NewExec(
@@ -85,21 +91,19 @@ func TestIRRoundtrip(t *testing.T) {
pkg.MustPath("/file", false, pkg.NewFile("file", []byte( pkg.MustPath("/file", false, pkg.NewFile("file", []byte(
"stub file", "stub file",
))), pkg.MustPath("/.hakurei", false, pkg.NewTar(pkg.NewHTTPGet( ))), pkg.MustPath("/.hakurei", false, pkg.NewHTTPGetTar(
nil, "file:///hakurei.tar", nil, "file:///hakurei.tar",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
))), pkg.MustPath("/opt", false, pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( pkg.TarUncompressed,
)), pkg.MustPath("/opt", false, pkg.NewHTTPGetTar(
nil, "file:///testtool.tar.gz", nil, "file:///testtool.tar.gz",
pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))), pkg.Checksum(bytes.Repeat([]byte{0xfd}, len(pkg.Checksum{}))),
), pkg.Gzip))), pkg.TarGzip,
)),
)}, )},
{"file anonymous", pkg.NewFile("", []byte{0})}, {"file anonymous", pkg.NewFile("", []byte{0})},
{"file", pkg.NewFile("stub", []byte("stub"))}, {"file", pkg.NewFile("stub", []byte("stub"))},
{"decompress", pkg.NewDecompress(pkg.NewFile("", []byte{0}), pkg.Bzip2)},
{"archive", pkg.NewArchive(pkg.NewFile("", []byte{0}))},
} }
testCasesCache := make([]cacheTestCase, len(testCases)) testCasesCache := make([]cacheTestCase, len(testCases))
for i, tc := range testCases { for i, tc := range testCases {
+34 -280
View File
@@ -155,7 +155,7 @@ type TContext struct {
// Target [Artifact] encoded identifier. // Target [Artifact] encoded identifier.
ids string ids string
// Pathname status was created at. // Pathname status was created at.
statusPath, statusSPath *check.Absolute statusPath *check.Absolute
// File statusHeader and logs are written to. // File statusHeader and logs are written to.
status *os.File status *os.File
// Error value during prepareStatus. // Error value during prepareStatus.
@@ -187,7 +187,7 @@ func makeStatusHeader(extension string) string {
var statusHeader = makeStatusHeader("") var statusHeader = makeStatusHeader("")
// prepareStatus initialises the status file once. // prepareStatus initialises the status file once.
func (t *TContext) prepareStatus(writeHeader bool) error { func (t *TContext) prepareStatus() error {
if t.statusPath != nil || t.status != nil { if t.statusPath != nil || t.status != nil {
return t.statusErr return t.statusErr
} }
@@ -204,16 +204,14 @@ func (t *TContext) prepareStatus(writeHeader bool) error {
return t.statusErr return t.statusErr
} }
if writeHeader {
_, t.statusErr = t.status.WriteString(statusHeader) _, t.statusErr = t.status.WriteString(statusHeader)
}
return t.statusErr return t.statusErr
} }
// GetStatusWriter returns a [io.Writer] for build logs. The caller must not // GetStatusWriter returns a [io.Writer] for build logs. The caller must not
// seek this writer before the position it was first returned in. // seek this writer before the position it was first returned in.
func (t *TContext) GetStatusWriter() (io.Writer, error) { func (t *TContext) GetStatusWriter() (io.Writer, error) {
err := t.prepareStatus(true) err := t.prepareStatus()
return t.status, err return t.status, err
} }
@@ -238,8 +236,8 @@ func (t *TContext) destroy(errP *error) {
if chmodErr != nil || removeErr != nil { if chmodErr != nil || removeErr != nil {
*errP = errors.Join(*errP, chmodErr, removeErr) *errP = errors.Join(*errP, chmodErr, removeErr)
} else if errors.Is(*errP, os.ErrExist) { } else if errors.Is(*errP, os.ErrExist) {
if linkError, ok := errors.AsType[*os.LinkError](*errP); ok && var linkError *os.LinkError
linkError != nil && if errors.As(*errP, &linkError) && linkError != nil &&
linkError.Op == "rename" { linkError.Op == "rename" {
// two artifacts may be backed by the same file // two artifacts may be backed by the same file
*errP = nil *errP = nil
@@ -260,11 +258,6 @@ func (t *TContext) destroy(errP *error) {
), 10), ), 10),
).String(), ).String(),
)) ))
if t.statusSPath != nil {
t.cache.checksumMu.Lock()
*errP = errors.Join(*errP, os.Remove(t.statusSPath.String()))
t.cache.checksumMu.Unlock()
}
} }
t.status = nil t.status = nil
} }
@@ -334,28 +327,6 @@ type FContext struct {
deps map[Artifact]cureRes deps map[Artifact]cureRes
} }
// linkSubstitute links status for substitute if populated.
func (f *FContext) linkSubstitute(ids, substitutes string) (err error) {
if f.status == nil || ids == substitutes {
return
}
statusS := f.cache.base.Append(
dirStatus,
substitutes,
)
f.cache.checksumMu.Lock()
err = os.Link(f.cache.base.Append(
dirStatus,
ids,
).String(), statusS.String())
f.cache.checksumMu.Unlock()
if err == nil {
f.statusSPath = statusS
}
return
}
// InvalidLookupError is the identifier of non-dependency [Artifact] looked up // InvalidLookupError is the identifier of non-dependency [Artifact] looked up
// via [FContext.GetArtifact] by a misbehaving [Artifact] implementation. // via [FContext.GetArtifact] by a misbehaving [Artifact] implementation.
type InvalidLookupError ID type InvalidLookupError ID
@@ -532,10 +503,6 @@ const (
KindExecNet KindExecNet
// KindFile is the kind of [Artifact] returned by [NewFile]. // KindFile is the kind of [Artifact] returned by [NewFile].
KindFile KindFile
// KindDecompress is the kind of [Artifact] returned by [NewDecompress].
KindDecompress
// KindArchive is the kind of [Artifact] returned by [NewArchive].
KindArchive
// _kindEnd is the total number of kinds and does not denote a kind. // _kindEnd is the total number of kinds and does not denote a kind.
_kindEnd _kindEnd
@@ -694,20 +661,6 @@ type pendingCure struct {
cancel context.CancelFunc cancel context.CancelFunc
} }
// An External cache provides prepared [Artifact] cure outcomes.
type External interface {
// Artifact returns the address of the [Checksum] of the cure outcome of
// an [Artifact] corresponding to id, or nil if this [Artifact] is not
// available in the external cache.
Artifact(ctx context.Context, id unique.Handle[ID]) (*Checksum, error)
// Checksum returns an [Artifact] producing the specified checksum.
Checksum(checksum unique.Handle[Checksum]) Artifact
// Status returns [io.ReadCloser] of the status file of an [Artifact]
// corresponding to id, or nil if this [Artifact] is not available or a
// status file is not present.
Status(r *RContext, id unique.Handle[ID]) (io.ReadCloser, error)
}
// Cache is a support layer that implementations of [Artifact] can use to store // Cache is a support layer that implementations of [Artifact] can use to store
// cured [Artifact] data in a content addressed fashion. // cured [Artifact] data in a content addressed fashion.
type Cache struct { type Cache struct {
@@ -758,11 +711,6 @@ type Cache struct {
// Buffered I/O free list, must not be accessed directly. // Buffered I/O free list, must not be accessed directly.
brPool, bwPool sync.Pool brPool, bwPool sync.Pool
// Optional external cache implementation.
extern External
// Synchronises access to extern.
externMu sync.RWMutex
// Unlocks the on-filesystem cache. Must only be called from Close. // Unlocks the on-filesystem cache. Must only be called from Close.
unlock func() unlock func()
// Whether [Cache] is considered closed. // Whether [Cache] is considered closed.
@@ -842,38 +790,6 @@ func (c *Cache) getReader(r io.Reader) *bufio.Reader {
// putReader adds br to brPool. // putReader adds br to brPool.
func (c *Cache) putReader(br *bufio.Reader) { c.brPool.Put(br) } func (c *Cache) putReader(br *bufio.Reader) { c.brPool.Put(br) }
// bufioReadCloser is the concrete type of value returned by Cache.getReaderRC.
type bufioReadCloser struct {
// Saved close error.
closeErr error
// Synchronises calls to Close.
closeOnce sync.Once
// For backing freelist.
c *Cache
// Underlying reader.
r io.ReadCloser
// Allocated from c.
*bufio.Reader
}
// Close closes the underlying reader, saves its return value, and returns the
// [bufio.Reader] instance to the backing [Cache].
func (brc *bufioReadCloser) Close() error {
brc.closeOnce.Do(func() {
br := brc.Reader
brc.Reader = nil
brc.c.putReader(br)
brc.closeErr = brc.r.Close()
})
return brc.closeErr
}
// getReaderRC is like getReader, but returns an [io.ReadCloser].
func (c *Cache) getReaderRC(r io.ReadCloser) io.ReadCloser {
return &bufioReadCloser{c: c, r: r, Reader: c.getReader(r)}
}
// getWriter is like [bufio.NewWriter] but for bwPool. // getWriter is like [bufio.NewWriter] but for bwPool.
func (c *Cache) getWriter(w io.Writer) *bufio.Writer { func (c *Cache) getWriter(w io.Writer) *bufio.Writer {
bw := c.bwPool.Get().(*bufio.Writer) bw := c.bwPool.Get().(*bufio.Writer)
@@ -895,35 +811,6 @@ func (e *ChecksumMismatchError) Error() string {
" instead of " + Encode(e.Want) " instead of " + Encode(e.Want)
} }
// LinknamePrefixError describes a malformed linkname to a [Checksum].
type LinknamePrefixError string
func (e LinknamePrefixError) Error() string {
return "linkname " + strconv.Quote(string(e)) + " missing prefix"
}
// readlinkChecksum reads a symbolic link to a dirChecksum entry and saves the
// decoded [Checksum] to the value pointed to by buf. The checksumLinknamePrefix
// is required.
func readlinkChecksum(a *check.Absolute, buf *Checksum) error {
linkname, err := os.Readlink(a.String())
if err != nil {
return nil
}
if !strings.HasPrefix(linkname, checksumLinknamePrefix) {
return LinknamePrefixError(linkname)
}
return Decode(buf, linkname[len(checksumLinknamePrefix):])
}
// SetExternal sets e as the [External] implementation of c.
func (c *Cache) SetExternal(e External) {
c.externMu.Lock()
c.extern = e
c.externMu.Unlock()
}
// ScrubError describes the outcome of a [Cache.Scrub] call where errors were // ScrubError describes the outcome of a [Cache.Scrub] call where errors were
// found and removed from the underlying storage of [Cache]. // found and removed from the underlying storage of [Cache].
type ScrubError struct { type ScrubError struct {
@@ -974,42 +861,36 @@ func (e *ScrubError) Unwrap() []error {
// Error returns a multi-line representation of [ScrubError]. // Error returns a multi-line representation of [ScrubError].
func (e *ScrubError) Error() string { func (e *ScrubError) Error() string {
var segments []string var segments []string
var buf strings.Builder
if len(e.ChecksumMismatches) > 0 { if len(e.ChecksumMismatches) > 0 {
buf.Reset() s := "checksum mismatches:\n"
buf.WriteString("checksum mismatches:\n")
for _, m := range e.ChecksumMismatches { for _, m := range e.ChecksumMismatches {
buf.WriteString(m.Error() + "\n") s += m.Error() + "\n"
} }
segments = append(segments, buf.String()) segments = append(segments, s)
} }
if len(e.DanglingIdentifiers) > 0 { if len(e.DanglingIdentifiers) > 0 {
buf.Reset() s := "dangling identifiers:\n"
buf.WriteString("dangling identifiers:\n")
for _, id := range e.DanglingIdentifiers { for _, id := range e.DanglingIdentifiers {
buf.WriteString(Encode(id) + "\n") s += Encode(id) + "\n"
} }
segments = append(segments, buf.String()) segments = append(segments, s)
} }
if len(e.DanglingStatus) > 0 { if len(e.DanglingStatus) > 0 {
buf.Reset() s := "dangling status:\n"
buf.WriteString("dangling status:\n")
for _, id := range e.DanglingStatus { for _, id := range e.DanglingStatus {
buf.WriteString(Encode(id) + "\n") s += Encode(id) + "\n"
} }
segments = append(segments, buf.String()) segments = append(segments, s)
} }
if len(e.Errs) > 0 { if len(e.Errs) > 0 {
buf.Reset() s := "errors during scrub:\n"
buf.WriteString("errors during scrub:\n")
for pathname, errs := range e.errs { for pathname, errs := range e.errs {
buf.WriteString(" " + pathname.Value() + ":\n") s += " " + pathname.Value() + ":\n"
for _, err := range errs { for _, err := range errs {
buf.WriteString(" " + err.Error() + "\n") s += " " + err.Error() + "\n"
} }
} }
segments = append(segments, buf.String()) segments = append(segments, s)
} }
return strings.Join(segments, "\n") return strings.Join(segments, "\n")
} }
@@ -1099,7 +980,7 @@ func (c *Cache) Scrub(checks int) error {
pathname := dir.Append(ent.Name()) pathname := dir.Append(ent.Name())
if ent.IsDir() { if ent.IsDir() {
if err := SumDir(got, pathname); err != nil { if err := HashDir(got, pathname); err != nil {
addErr(pathname, err) addErr(pathname, err)
return true return true
} }
@@ -1199,28 +1080,19 @@ func (c *Cache) Scrub(checks int) error {
got := p.Get().(*Checksum) got := p.Get().(*Checksum)
defer p.Put(got) defer p.Put(got)
var ok bool
for _, name := range [...]string{
dirIdentifier,
dirSubstitute,
} {
if _, err := os.Stat(c.base.Append( if _, err := os.Stat(c.base.Append(
name, dirIdentifier,
ent.Name(), ent.Name(),
).String()); err != nil { ).String()); err != nil {
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
addErr(dir.Append(ent.Name()), err) addErr(dir.Append(ent.Name()), err)
} }
continue
}
ok = true
}
if !ok {
seMu.Lock() seMu.Lock()
se.DanglingStatus = append(se.DanglingStatus, *want) se.DanglingStatus = append(se.DanglingStatus, *want)
seMu.Unlock() seMu.Unlock()
return false
} }
return ok return true
}} }}
} }
wg.Wait() wg.Wait()
@@ -1838,40 +1710,6 @@ func (r *RContext) NewMeasuredReader(
return r.cache.newMeasuredReader(rc, checksum) return r.cache.newMeasuredReader(rc, checksum)
} }
// tryExtern attempts to obtain an [Artifact] outcome from extern.
func (c *Cache) tryExtern(ctx context.Context, id unique.Handle[ID]) (
unique.Handle[Checksum],
io.ReadCloser,
error,
) {
c.externMu.RLock()
defer c.externMu.RUnlock()
if c.extern == nil {
return zeroChecksum, nil, nil
}
v, err := c.extern.Artifact(ctx, id)
if err != nil {
return zeroChecksum, nil, err
}
if v == nil {
return zeroChecksum, nil, nil
}
checksum := unique.Make(*v)
var got unique.Handle[Checksum]
if _, got, err = c.Cure(c.extern.Checksum(checksum)); err != nil {
return checksum, nil, err
} else if got != checksum {
return zeroChecksum, nil, &ChecksumMismatchError{got.Value(), checksum.Value()}
}
var status io.ReadCloser
status, err = c.extern.Status(&RContext{common{ctx, c}}, id)
return checksum, status, err
}
// cure implements Cure without acquiring a read lock on abortMu. cure must not // cure implements Cure without acquiring a read lock on abortMu. cure must not
// be entered during Abort. // be entered during Abort.
func (c *Cache) cure(a Artifact, curesExempt bool) ( func (c *Cache) cure(a Artifact, curesExempt bool) (
@@ -1938,7 +1776,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
err = zeroTimes(pathname.String()) err = zeroTimes(pathname.String())
} }
if err == nil && alternative != nil && substitute != id { if err == nil && alternative != nil {
c.substituteMu.Lock() c.substituteMu.Lock()
err = os.Symlink( err = os.Symlink(
linkname, linkname,
@@ -2085,9 +1923,6 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
} }
c.exitCure(a, curesExempt) c.exitCure(a, curesExempt)
if err != nil { if err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("cure file %s: %v", reportName(f, id), err)
}
return return
} }
@@ -2119,7 +1954,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
t := TContext{ t := TContext{
c.base.Append(dirWork, ids), c.base.Append(dirWork, ids),
c.base.Append(dirTemp, ids), c.base.Append(dirTemp, ids),
ids, nil, nil, nil, nil, ids, nil, nil, nil,
common{ctx, c}, common{ctx, c},
} }
switch ca := a.(type) { switch ca := a.(type) {
@@ -2131,9 +1966,6 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
err = ca.Cure(&t) err = ca.Cure(&t)
c.exitCure(a, curesExempt) c.exitCure(a, curesExempt)
if err != nil { if err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("cure trivial %s: %v", reportName(ca, id), err)
}
return return
} }
break break
@@ -2206,65 +2038,21 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
} }
defer f.destroy(&err) defer f.destroy(&err)
var (
externChecksum unique.Handle[Checksum]
externStatus io.ReadCloser
)
if externChecksum, externStatus, err = c.tryExtern(ctx, id); err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("extern %s: %v", reportName(ca, id), err)
}
return
}
if externChecksum != zeroChecksum {
if checksum != zeroChecksum && externChecksum != checksum {
if externStatus != nil {
_ = externStatus.Close()
}
err = &ChecksumMismatchError{externChecksum.Value(), checksum.Value()}
if c.msg.IsVerbose() {
c.msg.Verbosef("extern %s: %v", reportName(ca, id), err)
}
return
}
checksum = externChecksum
checksums = Encode(checksum.Value())
checksumPathname = c.base.Append(
dirChecksum,
checksums,
)
if externStatus != nil {
if err = f.prepareStatus(false); err != nil {
_ = externStatus.Close()
return
} else if _, err = io.Copy(f.status, externStatus); err != nil {
_ = externStatus.Close()
return
} else if err = externStatus.Close(); err != nil {
return
} else if err = f.linkSubstitute(ids, substitutes); err != nil {
return
}
}
return
}
if err = c.enterCure(a, curesExempt); err != nil { if err = c.enterCure(a, curesExempt); err != nil {
return return
} }
err = ca.Cure(&f) err = ca.Cure(&f)
if err == nil && f.status != nil {
err = os.Link(c.base.Append(
dirStatus,
ids,
).String(), c.base.Append(
dirStatus,
substitutes,
).String())
}
c.exitCure(a, curesExempt) c.exitCure(a, curesExempt)
if err == nil {
err = f.linkSubstitute(ids, substitutes)
}
if err != nil { if err != nil {
if c.msg.IsVerbose() {
c.msg.Verbosef("cure %s: %v", reportName(ca, id), err)
}
return return
} }
break break
@@ -2293,7 +2081,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
} }
var gotChecksum Checksum var gotChecksum Checksum
if err = SumFS( if err = HashFS(
&gotChecksum, &gotChecksum,
dotOverrideFS{os.DirFS(t.work.String()).(dirFS)}, dotOverrideFS{os.DirFS(t.work.String()).(dirFS)},
".", ".",
@@ -2313,9 +2101,6 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
Got: gotChecksum, Got: gotChecksum,
Want: checksum.Value(), Want: checksum.Value(),
} }
if c.msg.IsVerbose() {
c.msg.Verbosef("validate %s: %v", reportName(a, id), err)
}
return return
} }
@@ -2568,37 +2353,6 @@ func open(
c.unlock = func() {} c.unlock = func() {}
} }
for _, name := range []string{
dirWork,
dirTemp,
} {
dents, err := os.ReadDir(base.Append(name).String())
if err != nil {
if errors.Is(err, os.ErrNotExist) {
continue
}
c.unlock()
return nil, err
}
if len(dents) != 0 {
c.unlock()
return nil, fmt.Errorf(
"%s is not empty, scrub likely required",
name,
)
}
}
if _, err := os.ReadDir(base.Append(
dirExecScratch,
).String()); !errors.Is(err, os.ErrNotExist) {
c.unlock()
if err != nil {
return nil, err
}
return nil, errors.New(dirExecScratch + " is present, scrub likely required")
}
variantPath := base.Append(fileVariant).String() variantPath := base.Append(fileVariant).String()
if p, err := os.ReadFile(variantPath); err != nil { if p, err := os.ReadFile(variantPath); err != nil {
if !errors.Is(err, os.ErrNotExist) { if !errors.Is(err, os.ErrNotExist) {
+34 -299
View File
@@ -61,11 +61,6 @@ var (
// //
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact //go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
irArtifact map[pkg.Kind]pkg.IRReadFunc irArtifact map[pkg.Kind]pkg.IRReadFunc
// statusHeader is the header written to all status files in dirStatus.
//
//go:linkname statusHeader hakurei.app/internal/pkg.statusHeader
statusHeader string
) )
// newRContext returns the address of a new [pkg.RContext] unsafely created for // newRContext returns the address of a new [pkg.RContext] unsafely created for
@@ -197,35 +192,6 @@ func newStubFile(
} }
} }
// stubExtern implements [External] with hardcoded prepared outcomes.
type stubExtern struct {
artifact map[unique.Handle[pkg.ID]]pkg.Checksum
checksum map[unique.Handle[pkg.Checksum]]fstest.MapFS
status map[unique.Handle[pkg.ID]]string
}
func (e stubExtern) Artifact(_ context.Context, id unique.Handle[pkg.ID]) (*pkg.Checksum, error) {
if checksum, ok := e.artifact[id]; ok {
return &checksum, nil
}
return nil, nil
}
func (e stubExtern) Checksum(checksum unique.Handle[pkg.Checksum]) pkg.Artifact {
var buf bytes.Buffer
if err := pkg.Write(e.checksum[checksum], ".", &buf); err != nil {
panic(err)
}
return pkg.NewArchive(pkg.NewFile("", buf.Bytes()))
}
func (e stubExtern) Status(_ *pkg.RContext, id unique.Handle[pkg.ID]) (io.ReadCloser, error) {
if status, ok := e.status[id]; ok {
return io.NopCloser(strings.NewReader(status)), nil
}
return nil, nil
}
// destroyArtifact removes all traces of an [Artifact] from the on-disk cache. // destroyArtifact removes all traces of an [Artifact] from the on-disk cache.
// Do not use this in a test case without a very good reason to do so. // Do not use this in a test case without a very good reason to do so.
func destroyArtifact( func destroyArtifact(
@@ -322,15 +288,15 @@ func TestIdent(t *testing.T) {
a pkg.Artifact a pkg.Artifact
want unique.Handle[pkg.ID] want unique.Handle[pkg.ID]
}{ }{
{"decompress", &stubArtifact{ {"tar", &stubArtifact{
pkg.KindDecompress, pkg.KindTar,
[]byte{pkg.Gzip, 0, 0, 0, 0, 0, 0, 0}, []byte{pkg.TarGzip, 0, 0, 0, 0, 0, 0, 0},
[]pkg.Artifact{ []pkg.Artifact{
overrideIdent{pkg.ID{}, new(stubArtifact)}, overrideIdent{pkg.ID{}, new(stubArtifact)},
}, },
nil, nil,
}, unique.Make[pkg.ID](pkg.MustDecode( }, unique.Make[pkg.ID](pkg.MustDecode(
"97Y85QewssfPbNIN9cyNhzD4e6dLHcDTU8rb2c34k-aCrZfBNXFUc0duPiLFFcw_", "WKErnjTOVbuH2P9a0gM4OcAAO4p-CoX2HQu7CbZrg8ZOzApvWoO3-ISzPw6av_rN",
))}, ))},
} }
@@ -386,7 +352,7 @@ type expectsFS fstest.MapFS
// hash computes the checksum of e. // hash computes the checksum of e.
func (e expectsFS) hash() (checksum pkg.Checksum) { func (e expectsFS) hash() (checksum pkg.Checksum) {
if err := pkg.SumFS(&checksum, fstest.MapFS(e), "."); err != nil { if err := pkg.HashFS(&checksum, fstest.MapFS(e), "."); err != nil {
panic(err) panic(err)
} }
return return
@@ -468,31 +434,6 @@ const (
checkDestroySubstitutes = 1 << (iota + 32) checkDestroySubstitutes = 1 << (iota + 32)
) )
// makeBase returns a [pkg.Cache] base directory created for tb.
func makeBase(tb testing.TB) (base *check.Absolute) {
tb.Helper()
base = check.MustAbs(tb.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
tb.Fatal(err)
}
tb.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
tb.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
tb.Fatal(err)
}
})
return
}
// checkWithCache runs a slice of cacheTestCase. // checkWithCache runs a slice of cacheTestCase.
func checkWithCache(t *testing.T, testCases []cacheTestCase) { func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper() t.Helper()
@@ -502,7 +443,25 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
t.Helper() t.Helper()
t.Parallel() t.Parallel()
base := makeBase(t) base := check.MustAbs(t.TempDir())
if err := os.Chmod(base.String(), 0700); err != nil {
t.Fatal(err)
}
t.Cleanup(func() {
if err := filepath.WalkDir(base.String(), func(path string, d fs.DirEntry, err error) error {
if err != nil {
t.Error(err)
return nil
}
if !d.IsDir() {
return nil
}
return os.Chmod(path, 0700)
}); err != nil {
t.Fatal(err)
}
})
msg := message.New(log.New(os.Stderr, "cache: ", 0)) msg := message.New(log.New(os.Stderr, "cache: ", 0))
msg.SwapVerbose(testing.Verbose()) msg.SwapVerbose(testing.Verbose())
@@ -527,17 +486,7 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
tc.early(t, base) tc.early(t, base)
} }
tc.f(t, base, c) tc.f(t, base, c)
scrubFunc = func() error { scrubFunc = func() error { return c.Scrub(1 << 7) }
err = c.Scrub(1 << 7)
idents, checksums, cleanErr := c.Clean(false, false)
if len(idents) > 0 {
t.Errorf("destroyed %d idents", len(idents))
}
if len(checksums) > 0 {
t.Errorf("destroyed %d checksums", len(checksums))
}
return errors.Join(err, cleanErr)
}
} }
var restoreTemp bool var restoreTemp bool
@@ -577,10 +526,9 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
// destroy empty status directory // destroy empty status directory
if err := syscall.Rmdir(base.Append("status").String()); err != nil { if err := syscall.Rmdir(base.Append("status").String()); err != nil {
if !errors.Is(err, syscall.ENOTEMPTY) { t.Error(expectsFrom(base.Append("status").String()))
t.Fatal(err) t.Fatal(err)
} }
}
// destroy empty fault directory // destroy empty fault directory
if err := os.Remove(base.Append("fault").String()); err != nil { if err := os.Remove(base.Append("fault").String()); err != nil {
@@ -590,8 +538,8 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
want := tc.want.hash() want := tc.want.hash()
var checksum pkg.Checksum var checksum pkg.Checksum
if err := pkg.SumDir(&checksum, base); err != nil { if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err) t.Fatalf("HashDir: error = %v", err)
} else if checksum != want { } else if checksum != want {
t.Fatal(expectsFrom(base.String())) t.Fatal(expectsFrom(base.String()))
} }
@@ -610,10 +558,13 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
} }
// validate again to make sure scrub did not condemn anything // validate again to make sure scrub did not condemn anything
if err := pkg.SumDir(&checksum, base); err != nil { if err := pkg.HashDir(&checksum, base); err != nil {
t.Fatalf("SumDir: error = %v", err) t.Fatalf("HashDir: error = %v", err)
} else if checksum != want { } else if checksum != want {
t.Fatalf("(scrubbed) %s", expectsFrom(base.String())) t.Fatalf("(scrubbed) HashDir: %v", &pkg.ChecksumMismatchError{
Got: checksum,
Want: want,
})
} }
}) })
} }
@@ -1448,179 +1399,6 @@ func TestCache(t *testing.T) {
"substitute": {Mode: fs.ModeDir | 0700}, "substitute": {Mode: fs.ModeDir | 0700},
"work": {Mode: fs.ModeDir | 0700}, "work": {Mode: fs.ModeDir | 0700},
}}, }},
{"status substitute clean", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
destroyed := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("destroyed"),
deps: []pkg.Artifact{
pkg.NewFile("destroyed-input", []byte("destroyed")),
},
cure: func(f *pkg.FContext) error {
if w, err := f.GetStatusWriter(); err != nil {
return err
} else if _, err = w.Write([]byte("destroyed")); err != nil {
return err
}
p := f.GetWorkDir()
if err := os.MkdirAll(p.String(), 0755); err != nil {
return err
}
return os.WriteFile(p.Append("result").String(), nil, 0444)
},
}
substituted := new(*destroyed)
substituted.deps = []pkg.Artifact{
pkg.NewFile("destroyed-input-0", []byte("destroyed")),
}
substituted.cure = func(*pkg.FContext) error {
panic("substitutable cure reached")
}
cureMany(t, c, []cureStep{
{"destroyed", destroyed, base.Append(
"identifier",
pkg.Encode(c.Ident(destroyed).Value()),
), expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}, nil},
{"substituted", substituted, base.Append(
"identifier",
pkg.Encode(c.Ident(substituted).Value()),
), expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: 0444},
}, nil},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE": {Mode: fs.ModeDir | 0500},
"checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE/result": {Mode: 0444},
"checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV": {Mode: 0400, Data: []byte("destroyed")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/L-A8SK4ZX631eyealbJVH08u5pAVEf2NMk8RmLlxl7BVADkU4hNjWD6pi5H7uL1F": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"identifier/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"identifier/aSMwvPAwdsIF9e1spuLyRNEc8aTFA4HRVasoNqGjxdm1laSMs2h2teGsoSzLp_pR": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/wILUy2izpj2sgKJVhGUGIAde1XVuqvp5BpFMIQHanT5Q8R6jK4QPVSrJsjZh-njV")},
"status": {Mode: fs.ModeDir | 0700},
"status/0Jmc-vGnNDlXTD3jxy-4DGxHW-2-2LtLZ9SXaJtDIqu4uyHfDwDbghNBQ2aYRpab": {Mode: fs.ModeSymlink | 0777, Data: []byte("dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM")},
"status/VbVV2dFVosCriHnG9t5vwfW4lbHvzOkV2eYwpGpPJZOgNBc0g1wZAhbdKEweLqmW": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"status/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: 0400, Data: []byte(statusHeader + "destroyed")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/dzO8FEY9lu4hwRT6BfRZOX-uYGsC_5XH4jEJ7sJyThcmG9J_w1ArOAaUCGfL8wAM": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/UjZSrgz7_B7XMd9fHU7jM33UZhWlFgX0rz7JZbCBYR28bCS7jr_CAJdcDhi52ruE")},
"work": {Mode: fs.ModeDir | 0700},
}},
{"extern", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
a := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("extern"),
}
wantIdent := c.Ident(a)
wantOutput := expectsFS{
".": {Mode: fs.ModeDir | 0500},
"result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")},
}
var wantChecksum pkg.Checksum
if err := pkg.SumFS(
&wantChecksum,
fstest.MapFS(wantOutput),
".",
); err != nil {
t.Fatal(err)
}
wantChecksumH := unique.Make(wantChecksum)
_a := &stubArtifactF{
kind: pkg.KindExec,
params: []byte("extern substitute"),
deps: []pkg.Artifact{pkg.NewFile("", nil)},
}
_wantIdent := c.Ident(_a)
_wantOutput := expectsFS{
".": {Mode: fs.ModeDir | 0500},
}
var _wantChecksum pkg.Checksum
if err := pkg.SumFS(
&_wantChecksum,
fstest.MapFS(_wantOutput),
".",
); err != nil {
t.Fatal(err)
}
_wantChecksumH := unique.Make(_wantChecksum)
kca := pkg.NewExec(
"", "",
new(pkg.Checksum), 0, false, false,
fhs.AbsRoot, nil, fhs.AbsRoot, nil,
)
kcIdent := c.Ident(kca)
c.SetExternal(stubExtern{
artifact: map[unique.Handle[pkg.ID]]pkg.Checksum{
wantIdent: wantChecksum,
_wantIdent: _wantChecksum,
kcIdent: wantChecksum,
},
checksum: map[unique.Handle[pkg.Checksum]]fstest.MapFS{
wantChecksumH: fstest.MapFS(wantOutput),
_wantChecksumH: fstest.MapFS(_wantOutput),
},
status: map[unique.Handle[pkg.ID]]string{
wantIdent: "\x00",
kcIdent: "unreachable",
},
})
cureMany(t, c, []cureStep{
{"extern", a, base.Append(
"identifier",
pkg.Encode(wantIdent.Value()),
), wantOutput, nil},
{"substitute", _a, base.Append(
"identifier",
pkg.Encode(_wantIdent.Value()),
), _wantOutput, nil},
{"mismatch", kca, nil, nil, &pkg.ChecksumMismatchError{
Got: wantChecksum,
}},
})
}, expectsFS{
".": {Mode: fs.ModeDir | 0700},
"checksum": {Mode: fs.ModeDir | 0700},
"checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU": {Mode: fs.ModeDir | 0500},
"checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb": {Mode: 0400},
"checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl": {Mode: fs.ModeDir | 0500},
"checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl/result": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent")},
"identifier": {Mode: fs.ModeDir | 0700},
"identifier/4HqRo4uTwRQjfy3d2cujMoDC_pC4iv20h4a7NYlx0UdbVuky18o5iK78TFEfPX2U": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/7AZcJm58ghFyTVf_v2baSntgpsxkP5el7ti9dC77C29n8YTEqQW9jRW92KGNdYnz": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")},
"identifier/c4aCI00C-ZVyo_FQDQLl1OYK4U_kjzxwrLdFDiXMHnbMcZXCkXo_nxUWauScZ_4V": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"identifier/cNoG77frXGRCJa7fUi1INKUEQg7L4qrX5acsSv-wqZdGZT7dQwM93rD3at6kSFFF": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb")},
"identifier/gvCqzexZVqXjF8B5lKMcP5onmq3jJ6AKqzOW_WN0Fl2yTr9NKhPt9l_ClD2EOSlS": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/fHkl_RuHOoc4rso__nV-qreikovd6Yhrq5mpBlkf5hmPGaxDlik2bYOQ4dhUQjtl")},
"status": {Mode: fs.ModeDir | 0700},
"status/gvCqzexZVqXjF8B5lKMcP5onmq3jJ6AKqzOW_WN0Fl2yTr9NKhPt9l_ClD2EOSlS": {Mode: 0400, Data: []byte("\x00")},
"substitute": {Mode: fs.ModeDir | 0700},
"substitute/qOYrxy9ztKeOA96Os811_0Ox5sd8FBOxis6psJAnRJL5MLazFMaqmd4g7t7k1OHk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU")},
"work": {Mode: fs.ModeDir | 0700},
}},
} }
checkWithCache(t, testCases) checkWithCache(t, testCases)
} }
@@ -1983,49 +1761,6 @@ func TestOpen(t *testing.T) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr) t.Errorf("Open: error = %#v, want %#v", err, wantErr)
} }
}) })
t.Run("dirty", func(t *testing.T) {
t.Parallel()
tempDir := check.MustAbs(t.TempDir())
if err := os.MkdirAll(tempDir.Append(
"cache",
"work",
"dirty",
).String(), 0755); err != nil {
t.Fatal(err)
}
wantErr := errors.New("work is not empty, scrub likely required")
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
})
t.Run("scratch", func(t *testing.T) {
t.Parallel()
tempDir := check.MustAbs(t.TempDir())
if err := os.MkdirAll(tempDir.Append(
"cache",
"scratch",
).String(), 0755); err != nil {
t.Fatal(err)
}
wantErr := errors.New("scratch is present, scrub likely required")
if _, err := pkg.Open(
t.Context(),
message.New(nil),
0, 0, 0, tempDir.Append("cache"),
); !reflect.DeepEqual(err, wantErr) {
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
}
})
} }
func TestExtensionRegister(t *testing.T) { func TestExtensionRegister(t *testing.T) {
+66 -17
View File
@@ -2,18 +2,32 @@ package pkg
import ( import (
"archive/tar" "archive/tar"
"compress/bzip2"
"compress/gzip"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/fs" "io/fs"
"net/http"
"os" "os"
"path/filepath" "path/filepath"
) )
const (
// TarUncompressed denotes an uncompressed tarball.
TarUncompressed = iota
// TarGzip denotes a tarball compressed via [gzip].
TarGzip
// TarBzip2 denotes a tarball compressed via [bzip2].
TarBzip2
)
// A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact]. // A tarArtifact is an [Artifact] unpacking a tarball backed by a [FileArtifact].
type tarArtifact struct { type tarArtifact struct {
// Caller-supplied backing tarball. // Caller-supplied backing tarball.
f Artifact f Artifact
// Compression on top of the tarball.
compression uint32
} }
// tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball. // tarArtifactNamed embeds tarArtifact for a [fmt.Stringer] tarball.
@@ -25,13 +39,13 @@ type tarArtifactNamed struct {
var _ fmt.Stringer = new(tarArtifactNamed) var _ fmt.Stringer = new(tarArtifactNamed)
// String returns the name of the underlying [Artifact] prefixed with unpack. // String returns the name of the underlying [Artifact] suffixed with unpack.
func (a *tarArtifactNamed) String() string { return "unpack-" + a.name } func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }
// NewTar returns a new [Artifact] unpacking the tar stream produced by the // NewTar returns a new [Artifact] backed by the supplied [Artifact] and
// backing [Artifact]. The source [Artifact] must be a [FileArtifact]. // compression method. The source [Artifact] must be a [FileArtifact].
func NewTar(a Artifact) Artifact { func NewTar(a Artifact, compression uint32) Artifact {
ta := tarArtifact{a} ta := tarArtifact{a, compression}
if s, ok := a.(fmt.Stringer); ok { if s, ok := a.(fmt.Stringer); ok {
if name := s.String(); name != "" { if name := s.String(); name != "" {
return &tarArtifactNamed{ta, name} return &tarArtifactNamed{ta, name}
@@ -40,15 +54,25 @@ func NewTar(a Artifact) Artifact {
return &ta return &ta
} }
// NewHTTPGetTar is abbreviation for NewHTTPGet passed to NewTar.
func NewHTTPGetTar(
hc *http.Client,
url string,
checksum Checksum,
compression uint32,
) Artifact {
return NewTar(NewHTTPGet(hc, url, checksum), compression)
}
// Kind returns the hardcoded [Kind] constant. // Kind returns the hardcoded [Kind] constant.
func (a *tarArtifact) Kind() Kind { return KindTar } func (a *tarArtifact) Kind() Kind { return KindTar }
// Params is a noop. // Params writes compression encoded in little endian.
func (a *tarArtifact) Params(*IContext) {} func (a *tarArtifact) Params(ctx *IContext) { ctx.WriteUint32(a.compression) }
func init() { func init() {
register(KindTar, func(r *IRReader) Artifact { register(KindTar, func(r *IRReader) Artifact {
a := NewTar(r.Next()) a := NewTar(r.Next(), r.ReadUint32())
if _, ok := r.Finalise(); ok { if _, ok := r.Finalise(); ok {
panic(ErrUnexpectedChecksum) panic(ErrUnexpectedChecksum)
} }
@@ -74,17 +98,42 @@ func (e DisallowedTypeflagError) Error() string {
// Cure cures the [Artifact], producing a directory located at work. // Cure cures the [Artifact], producing a directory located at work.
func (a *tarArtifact) Cure(t *TContext) (err error) { func (a *tarArtifact) Cure(t *TContext) (err error) {
var r io.ReadCloser var tr io.ReadCloser
if r, err = t.Open(a.f); err != nil { if tr, err = t.Open(a.f); err != nil {
return return
} }
defer func() { defer func(f io.ReadCloser) {
closeErr := r.Close() if err == nil {
err = tr.Close()
}
closeErr := f.Close()
if err == nil { if err == nil {
err = closeErr err = closeErr
} }
}() }(tr)
br := t.cache.getReader(tr)
defer t.cache.putReader(br)
tr = io.NopCloser(br)
switch a.compression {
case TarUncompressed:
break
case TarGzip:
if tr, err = gzip.NewReader(tr); err != nil {
return
}
break
case TarBzip2:
tr = io.NopCloser(bzip2.NewReader(tr))
break
default:
return os.ErrInvalid
}
type dirTargetPerm struct { type dirTargetPerm struct {
path string path string
@@ -107,8 +156,8 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
}() }()
var header *tar.Header var header *tar.Header
tr := tar.NewReader(r) r := tar.NewReader(tr)
for header, err = tr.Next(); err == nil; header, err = tr.Next() { for header, err = r.Next(); err == nil; header, err = r.Next() {
typeflag := header.Typeflag typeflag := header.Typeflag
if typeflag == 0 { if typeflag == 0 {
if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' { if len(header.Name) > 0 && header.Name[len(header.Name)-1] == '/' {
@@ -134,7 +183,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
); err != nil { ); err != nil {
return return
} }
if _, err = io.Copy(f, tr); err != nil { if _, err = io.Copy(f, r); err != nil {
_ = f.Close() _ = f.Close()
return return
} else if err = f.Close(); err != nil { } else if err = f.Close(); err != nil {
+61 -19
View File
@@ -80,8 +80,8 @@ func TestTar(t *testing.T) {
"checksum/" + wantEncode + "/work": {Mode: fs.ModeDir | 0500}, "checksum/" + wantEncode + "/work": {Mode: fs.ModeDir | 0500},
"identifier": {Mode: fs.ModeDir | 0700}, "identifier": {Mode: fs.ModeDir | 0700},
"identifier/T68YqeEW5moiwVe4J0wPvjERgyMUd4k_cKKxTcQx6AXqZaQAuuryu-Iv-qxDV6-T": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, "identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)}, "identifier/rg7F1D5hwv6o4xctjD5zDq4i5MD0mArTsUIWfhUbik8xC6Bsyt3mjXXOm3goojTz": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantEncode)},
"substitute": {Mode: fs.ModeDir | 0700}, "substitute": {Mode: fs.ModeDir | 0700},
@@ -104,8 +104,8 @@ func TestTar(t *testing.T) {
"checksum/" + wantExpandEncode + "/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")}, "checksum/" + wantExpandEncode + "/libedac.so": {Mode: fs.ModeSymlink | 0777, Data: []byte("/proc/nonexistent/libedac.so")},
"identifier": {Mode: fs.ModeDir | 0700}, "identifier": {Mode: fs.ModeDir | 0700},
"identifier/WbWIIUGl6bqDBsjU-UB69JqOWLjG5waiUJBUBGeeRp3V7FBbwaBe3sG1qm7DlPSk": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, "identifier/W5S65DEhawz_WKaok5NjUKLmnD9dNl5RPauNJjcOVcB3VM4eGhSaLGmXbL8vZpiw": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"identifier/e3BECw_x_vDyTvj2P48MJVgwpgALeXQ90Ye2Y8gr6SwIPtMup6SR2LVcCTDRGpVi": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)}, "identifier/_v1blm2h-_KA-dVaawdpLas6MjHc6rbhhFS8JWwx8iJxZGUu8EBbRrhr5AaZ9PJL": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/" + wantExpandEncode)},
"substitute": {Mode: fs.ModeDir | 0700}, "substitute": {Mode: fs.ModeDir | 0700},
@@ -156,6 +156,49 @@ func checkTarHTTP(
"testdata": {Data: []byte(testdata), Mode: 0400}, "testdata": {Data: []byte(testdata), Mode: 0400},
})) }))
wantIdent := func() pkg.ID {
h := sha512.New384()
// kind uint64
h.Write([]byte{byte(pkg.KindTar), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h.Write([]byte{1, 0, 0, 0, 0, 0, 0, 0})
// kind uint64
h.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// ident ID
h0 := sha512.New384()
// kind uint64
h0.Write([]byte{byte(pkg.KindHTTPGet), 0, 0, 0, 0, 0, 0, 0})
// deps_sz uint64
h0.Write([]byte{0, 0, 0, 0, 0, 0, 0, 0})
// url string
h0.Write([]byte{byte(pkg.IRKindString), 0, 0, 0})
h0.Write([]byte{0x10, 0, 0, 0})
h0.Write([]byte("file:///testdata"))
// end(KnownChecksum)
h0.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h0.Write([]byte{byte(pkg.IREndKnownChecksum), 0, 0, 0})
// checksum Checksum
h0.Write(testdataChecksum[:])
h.Write(h0.Sum(nil))
// compression uint32
h.Write([]byte{byte(pkg.IRKindUint32), 0, 0, 0})
h.Write([]byte{pkg.TarGzip, 0, 0, 0})
// end
h.Write([]byte{byte(pkg.IRKindEnd), 0, 0, 0})
h.Write([]byte{0, 0, 0, 0})
return pkg.ID(h.Sum(nil))
}()
a := pkg.NewHTTPGetTar(
&client,
"file:///testdata",
testdataChecksum,
pkg.TarGzip,
)
tarDir := stubArtifact{ tarDir := stubArtifact{
kind: pkg.KindExec, kind: pkg.KindExec,
params: []byte("directory containing a single regular file"), params: []byte("directory containing a single regular file"),
@@ -208,28 +251,27 @@ func checkTarHTTP(
defer newDestroyArtifactFunc(&tarDirType)(t, base, c) defer newDestroyArtifactFunc(&tarDirType)(t, base, c)
cureMany(t, c, []cureStep{ cureMany(t, c, []cureStep{
{"file", pkg.NewTar(pkg.NewDecompress(pkg.NewHTTPGet( {"file", a, base.Append(
&client, "identifier",
"file:///testdata", pkg.Encode(wantIdent),
testdataChecksum, ), want, nil},
), pkg.Gzip)), ignorePathname, want, nil},
{"directory", pkg.NewTar(pkg.NewDecompress( {"directory", pkg.NewTar(
&tarDir, &tarDir,
pkg.Gzip, pkg.TarGzip,
)), ignorePathname, want, nil}, ), ignorePathname, want, nil},
{"multiple entries", pkg.NewTar(pkg.NewDecompress( {"multiple entries", pkg.NewTar(
&tarDirMulti, &tarDirMulti,
pkg.Gzip, pkg.TarGzip,
)), nil, nil, errors.New( ), nil, nil, errors.New(
"input directory does not contain a single regular file", "input directory does not contain a single regular file",
)}, )},
{"bad type", pkg.NewTar(pkg.NewDecompress( {"bad type", pkg.NewTar(
&tarDirType, &tarDirType,
pkg.Gzip, pkg.TarGzip,
)), nil, nil, errors.New( ), nil, nil, errors.New(
"input directory does not contain a single regular file", "input directory does not contain a single regular file",
)}, )},
@@ -239,6 +281,6 @@ func checkTarHTTP(
cure: func(t *pkg.TContext) error { cure: func(t *pkg.TContext) error {
return stub.UniqueError(0xcafe) return stub.UniqueError(0xcafe)
}, },
}), nil, nil, stub.UniqueError(0xcafe)}, }, pkg.TarGzip), nil, nil, stub.UniqueError(0xcafe)},
}) })
} }
-189
View File
@@ -1,189 +0,0 @@
package report
import (
"encoding/json"
"errors"
"fmt"
"log"
"os"
"strconv"
"sync"
"sync/atomic"
"time"
"hakurei.app/check"
)
const (
// Trivial denotes an error that may happen frequently under normal operation.
Trivial = iota
// Inconsistent denotes an error diagnosed due to inconsistent state.
Inconsistent
// Degraded denotes an error condition causing a degraded state.
Degraded
// Fatal denotes an unrecoverable error. This is generally followed by
// notifying the user and suggesting immediate restart, or an automatic
// restart of a nonessential daemon process.
Fatal
)
// A Reporter represents an error reporting object backed by user-facing display
// and optional persistent storage. A Reporter can be used simultaneously from
// multiple goroutines.
type Reporter struct {
// Backing logger, the zero value implies the return value of [log.Default].
log atomic.Pointer[log.Logger]
// Backing persistent storage.
pathname atomic.Pointer[check.Absolute]
// Reporting behaviour.
flag atomic.Uint64
// Caller-supplied reporting functions.
notify []func(e Error)
// Synchronises access to notify.
notifyMu sync.RWMutex
// Synchronises access to persistent storage.
mu sync.Mutex
}
// SetOutput sets the backing [log.Logger].
func (r *Reporter) SetOutput(l *log.Logger) { r.log.Store(l) }
// SetPathname sets the pathname to persistent storage.
func (r *Reporter) SetPathname(a *check.Absolute) { r.pathname.Store(a) }
const (
// DStrict causes all dispatches to end in a call to panic.
DStrict = 1 << iota
// DNoRecover disallows recovering from error write to persistent storage.
DNoRecover
// DBypassEarly allows persistent storage to be skipped before it is ready.
DBypassEarly
)
// SetFlags sets flags for r.
func (r *Reporter) SetFlags(flag uint64) { r.flag.Store(flag) }
// An Error represents an error reported via [Reporter.Dispatch].
type Error struct {
// Nature of error.
Severity int
// User-facing reporting message.
Message string
// Underlying error.
Err error
}
// RepresentableError is implemented by errors friendly to JSON serialisation.
type RepresentableError interface {
error
Representable()
}
// MarshalJSON returns an incomplete JSON representation of e.
func (e Error) MarshalJSON() (data []byte, err error) {
v := struct {
Severity any `json:"kind"`
Message string `json:"message"`
Err json.RawMessage `json:"error"`
}{Message: e.Message}
switch e.Severity {
case Trivial:
v.Severity = "trivial"
case Inconsistent:
v.Severity = "inconsistent"
case Degraded:
v.Severity = "degradation"
case Fatal:
v.Severity = "fatal"
default:
v.Severity = e.Severity
}
if re, ok := errors.AsType[RepresentableError](e.Err); ok {
v.Err, err = json.Marshal(re)
} else {
v.Err, err = json.Marshal(e.Err.Error())
}
if err != nil {
return
}
return json.Marshal(&v)
}
// Error is generally only called during a [Reporter.Dispatch] call ending in
// a panic or an otherwise unrecoverable condition.
func (e Error) Error() string {
return fmt.Sprintf("%s: %v", e.Message, e.Err)
}
// Notify arranges for f to be called for all future dispatched errors.
func (r *Reporter) Notify(f func(Error)) {
r.notifyMu.Lock()
r.notify = append(r.notify, f)
r.notifyMu.Unlock()
}
// getLog returns the address of the underlying [log.Logger], or the return
// value of [log.Default].
func (r *Reporter) getLog() *log.Logger {
l := r.log.Load()
if l == nil {
l = log.Default()
}
return l
}
// dispatch implements Dispatch but returns the reporting error.
func (r *Reporter) dispatch(severity int, message string, e error) (err error) {
p := Error{severity, message, e}
r.getLog().Println(message+":", e)
flag := r.flag.Load()
defer func() {
if flag&DNoRecover != 0 && err != nil {
panic(err)
}
}()
if a := r.pathname.Load(); a != nil {
var w *os.File
r.mu.Lock()
w, err = os.OpenFile(
a.Append(strconv.FormatUint(uint64(time.Now().UnixNano()), 10)).String(),
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
0400,
)
if err != nil {
r.mu.Unlock()
return
}
err = json.NewEncoder(w).Encode(p)
if _err := w.Close(); err == nil {
err = _err
}
r.mu.Unlock()
} else if flag&DBypassEarly == 0 && severity != Trivial {
panic(p)
}
if flag&DStrict != 0 && severity != Trivial {
panic(p)
}
r.notifyMu.RLock()
defer r.notifyMu.RUnlock()
for _, f := range r.notify {
f(p)
}
return
}
// Dispatch reports an error and saves it to backing storage if available.
func (r *Reporter) Dispatch(severity int, message string, e error) {
if err := r.dispatch(severity, message, e); err != nil {
r.getLog().Println(err)
}
}
-214
View File
@@ -1,214 +0,0 @@
package report_test
import (
"bytes"
"errors"
"log"
"os"
"reflect"
"slices"
"testing"
"unsafe"
"hakurei.app/check"
"hakurei.app/internal/kobject"
"hakurei.app/internal/report"
"hakurei.app/internal/stub"
"hakurei.app/internal/uevent"
)
type representableUE struct{ stub.UniqueError }
func (representableUE) Representable() {}
func TestDispatch(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
flag uint64
errs []report.Error
persist bool
wantFiles []string
wantLog string
wantPanic error
}{
{"default", 0, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
{
Severity: report.Inconsistent,
Message: "processed inconsistent uevent",
Err: (&kobject.Event{
Action: uevent.KOBJ_ADD,
DevPath: "\x00",
Env: map[string]string{"V": "\xfd"},
Sequence: 2,
}).NewError(kobject.EDuplicateAdd, &kobject.Object{
State: kobject.StateNew,
DevPath: "\x00",
Env: map[string]string{"V": "\xff"},
}),
},
}, true, []string{`{"kind":"fatal","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`, `{"kind":"inconsistent","message":"processed inconsistent uevent","error":{"fault":1,"event":{"action":"add","devpath":"\u0000","env":{"V":"\ufffd"},"seqnum":2,"subsystem":""},"object":{"state":1,"devpath":"\u0000","subsystem":"","env":{"V":"\ufffd"}}}}
`}, `dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite
dispatch: processed inconsistent uevent: duplicate add event on devpath "\x00"
`, nil},
{"strict", report.DStrict, []report.Error{
{
Severity: report.Degraded,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, true, []string{
`{"kind":"degradation","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Degraded,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"strict no recover", report.DStrict | report.DNoRecover, []report.Error{
{
Severity: report.Inconsistent,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, true, []string{
`{"kind":"inconsistent","message":"rejecting coldboot loop","error":"unique error 51966 injected by the test suite"}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Inconsistent,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"no recover", report.DNoRecover, []report.Error{
{
Severity: 0xbeef,
Message: "rejecting coldboot loop",
Err: representableUE{stub.UniqueError(0xcafe)},
},
}, true, []string{
`{"kind":48879,"message":"rejecting coldboot loop","error":{"UniqueError":51966}}
`,
}, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
{"early", 0, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", report.Error{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
}},
{"bypass early", report.DBypassEarly, []report.Error{
{
Severity: report.Fatal,
Message: "rejecting coldboot loop",
Err: stub.UniqueError(0xcafe),
},
}, false, nil, "dispatch: rejecting coldboot loop: unique error 51966 injected by the test suite\n", nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var r report.Reporter
r.SetFlags(tc.flag)
var buf bytes.Buffer
l := log.New(&buf, "dispatch: ", 0)
r.SetOutput(l)
a := check.MustAbs(t.TempDir())
if tc.persist {
r.SetPathname(a)
}
var got []report.Error
r.Notify(func(p report.Error) { got = append(got, p) })
var gotPanic any
func() {
defer func() { gotPanic = recover() }()
for _, p := range tc.errs {
r.Dispatch(p.Severity, p.Message, p.Err)
}
}()
if gotPanic == nil && !reflect.DeepEqual(got, tc.errs) {
t.Errorf("Dispatch: %#v, want %#v", got, tc.errs)
}
if !reflect.DeepEqual(gotPanic, tc.wantPanic) {
t.Errorf("Dispatch: panic = %v, want %v", gotPanic, tc.wantPanic)
}
if gotLog := buf.String(); gotLog != tc.wantLog {
t.Errorf("Dispatch: log =\n%s\nwant\n%s", gotLog, tc.wantLog)
}
if tc.persist {
var names []string
if dents, err := os.ReadDir(a.String()); err != nil {
t.Fatal(err)
} else {
names = make([]string, len(dents))
for i, dent := range dents {
names[i] = dent.Name()
}
slices.Sort(names)
}
gotFiles := make([]string, len(names))
for i, name := range names {
if p, err := os.ReadFile(a.Append(name).String()); err != nil {
t.Fatal(err)
} else {
gotFiles[i] = unsafe.String(unsafe.SliceData(p), len(p))
}
}
if !slices.Equal(gotFiles, tc.wantFiles) {
t.Errorf("Dispatch: persist = %s, want %s", gotFiles, tc.wantFiles)
}
}
})
}
}
func TestBadPersist(t *testing.T) {
t.Parallel()
var r report.Reporter
r.SetFlags(report.DNoRecover)
r.SetPathname(check.MustAbs("/proc/nonexistent"))
var pathError *os.PathError
func() {
defer func() {
if !errors.As(recover().(error), &pathError) {
t.Fatal("invalid panic kind")
}
}()
r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
}()
if !errors.Is(pathError.Err, os.ErrNotExist) {
t.Fatalf("Dispatch: panic = %v", pathError)
}
var buf bytes.Buffer
l := log.New(&buf, "persist: ", 0)
r.SetOutput(l)
r.SetFlags(0)
r.Dispatch(report.Fatal, "\x00", stub.UniqueError(0xbad))
}
+110
View File
@@ -0,0 +1,110 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newAttr() (pkg.Artifact, string) {
const (
version = "2.5.2"
checksum = "YWEphrz6vg1sUMmHHVr1CRo53pFXRhq_pjN-AlG8UgwZK1y6m7zuDhxqJhD0SV0l"
)
return t.NewPackage("attr", version, newTar(
"https://download.savannah.nongnu.org/releases/attr/"+
"attr-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
Patches: []KV{
{"libgen-basename", `From 8a80d895dfd779373363c3a4b62ecce5a549efb2 Mon Sep 17 00:00:00 2001
From: "Haelwenn (lanodan) Monnier" <contact@hacktivis.me>
Date: Sat, 30 Mar 2024 10:17:10 +0100
Subject: tools/attr.c: Add missing libgen.h include for basename(3)
Fixes compilation issue with musl and modern C99 compilers.
See: https://bugs.gentoo.org/926294
---
tools/attr.c | 1 +
1 file changed, 1 insertion(+)
diff --git a/tools/attr.c b/tools/attr.c
index f12e4af..6a3c1e9 100644
--- a/tools/attr.c
+++ b/tools/attr.c
@@ -28,6 +28,7 @@
#include <errno.h>
#include <string.h>
#include <locale.h>
+#include <libgen.h>
#include <attr/attributes.h>
--
cgit v1.1`},
{"musl-errno", `diff --git a/test/attr.test b/test/attr.test
index 6ce2f9b..e9bde92 100644
--- a/test/attr.test
+++ b/test/attr.test
@@ -11,7 +11,7 @@ Try various valid and invalid names
$ touch f
$ setfattr -n user -v value f
- > setfattr: f: Operation not supported
+ > setfattr: f: Not supported
$ setfattr -n user. -v value f
> setfattr: f: Invalid argument
`},
},
ScriptEarly: `
ln -s ../../system/bin/perl /usr/bin
`,
}, (*MakeHelper)(nil),
Perl,
), version
}
func init() {
artifactsM[Attr] = Metadata{
f: Toolchain.newAttr,
Name: "attr",
Description: "Commands for Manipulating Filesystem Extended Attributes",
Website: "https://savannah.nongnu.org/projects/attr/",
ID: 137,
}
}
func (t Toolchain) newACL() (pkg.Artifact, string) {
const (
version = "2.3.2"
checksum = "-fY5nwH4K8ZHBCRXrzLdguPkqjKI6WIiGu4dBtrZ1o0t6AIU73w8wwJz_UyjIS0P"
)
return t.NewPackage("acl", version, newTar(
"https://download.savannah.nongnu.org/releases/acl/"+
"acl-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), nil, &MakeHelper{
// makes assumptions about uid_map/gid_map
SkipCheck: true,
},
Attr,
), version
}
func init() {
artifactsM[ACL] = Metadata{
f: Toolchain.newACL,
Name: "acl",
Description: "Commands for Manipulating POSIX Access Control Lists",
Website: "https://savannah.nongnu.org/projects/acl/",
Dependencies: P{
Attr,
},
ID: 16,
}
}
+463
View File
@@ -0,0 +1,463 @@
package rosa
import (
"context"
"encoding/json"
"errors"
"fmt"
"net/http"
"runtime"
"strconv"
"sync"
"hakurei.app/internal/pkg"
)
// PArtifact is a lazily-initialised [pkg.Artifact] preset.
type PArtifact int
const (
LLVM PArtifact = iota
// EarlyInit is the Rosa OS init program.
EarlyInit
// ImageSystem is the Rosa OS /system image.
ImageSystem
// ImageInitramfs is the Rosa OS initramfs archive.
ImageInitramfs
// Kernel is the generic Rosa OS Linux kernel.
Kernel
// KernelHeaders is an installation of kernel headers for [Kernel].
KernelHeaders
// KernelSource is a writable kernel source tree installed to [AbsUsrSrc].
KernelSource
// Firmware is firmware blobs for use with the Linux kernel.
Firmware
ACL
ArgpStandalone
Attr
Autoconf
Automake
BC
Bash
Binutils
Bison
Bzip2
Cairo
CMake
Connman
Coreutils
Curl
DBus
DTC
Diffutils
Elfutils
Fakeroot
Findutils
Flex
FontConfig
FontUtil
Freetype
Fribidi
Fuse
Gconf
GMP
GLib
Gawk
GenInitCPIO
Gettext
Git
Glslang
GnuTLS
Go
GObjectIntrospection
Gperf
Grep
Gzip
Hakurei
HakureiDist
HarfBuzz
Hwdata
IPTables
JDK
Kmod
LIT
LibX11
LibXau
LibXdmcp
LibXext
LibXfixes
LibXfont2
LibXft
LibXi
LibXrandr
LibXrender
LibXtst
LibXxf86vm
Libarchive
Libbsd
Libcap
Libconfig
Libdatrie
LibdisplayInfo
Libdrm
Libepoxy
Libev
Libexpat
Libffi
Libfontenc
Libgd
Libglvnd
Libiconv
Libjpeg
Libmd
Libmnl
Libnftnl
Libpciaccess
Libpng
Libpsl
Libseccomp
Libtasn1
Libthai
Libtirpc
Libtool
Libucontext
Libunistring
Libva
LibxcbRenderUtil
LibxcbUtil
LibxcbUtilImage
LibxcbUtilKeysyms
LibxcbUtilWM
Libxcvt
Libxkbfile
Libxml2
Libxshmfence
Libxslt
Libxtrans
LMSensors
M4
MPC
MPFR
Make
Mesa
Meson
Mksh
MuslFts
MuslObstack
NSS
NSSCACert
Ncurses
Nettle
Ninja
OpenSSL
P11Kit
PCRE2
Pango
Parallel
Patch
Perl
PerlLocaleGettext
PerlMIMECharset
PerlModuleBuild
PerlPodParser
PerlSGMLS
PerlTermReadKey
PerlTestCmd
PerlTextCharWidth
PerlTextWrapI18N
PerlUnicodeLineBreak
PerlYAMLTiny
Pixman
PkgConfig
Procps
Python
PythonFlitCore
PythonHatchling
PythonIniConfig
PythonMako
PythonMarkupSafe
PythonPackaging
PythonPathspec
PythonPluggy
PythonPyTest
PythonPyYAML
PythonPycparser
PythonPygments
PythonSetuptools
PythonSetuptoolsSCM
PythonTroveClassifiers
PythonVCSVersioning
PythonWheel
QEMU
Rdfind
Readline
Rsync
Sed
SharedMimeInfo
SPIRVHeaders
SPIRVLLVMTranslator
SPIRVTools
SquashfsTools
Strace
TamaGo
Tar
Texinfo
Toybox
toyboxEarly
Unzip
UtilLinux
VIM
Wayland
WaylandProtocols
XCB
XCBProto
XDGDBusProxy
XZ
Xkbcomp
XkeyboardConfig
XorgProto
Xserver
Zip
Zlib
Zstd
// PresetUnexportedStart is the first unexported preset.
PresetUnexportedStart
stage0Dist = iota - 1
llvmSource
// earlyCompilerRT is an early, standalone compiler-rt installation for the
// standalone runtimes build.
//
// earlyCompilerRT must only be loaded by [LLVM].
earlyCompilerRT
// earlyRuntimes is an early, standalone installation of LLVM runtimes to
// work around the cmake build system leaking the system LLVM installation
// when invoking the newly built toolchain.
//
// earlyRuntimes must only be loaded by [LLVM].
earlyRuntimes
buildcatrust
utilMacros
// Musl is a standalone libc that does not depend on the toolchain.
Musl
// muslHeaders is a system installation of [Musl] headers.
muslHeaders
// gcc is a hacked-to-pieces GCC toolchain meant for use in intermediate
// stages only. This preset and its direct output must never be exposed.
gcc
// nettle3 is an older version of [Nettle].
nettle3
// Stage0 is a tarball containing all compile-time dependencies of artifacts
// part of the [Std] toolchain.
Stage0
// PresetEnd is the total number of presets and does not denote a preset.
PresetEnd
)
// P represents multiple [PArtifact] and is stable through JSON.
type P []PArtifact
// MarshalJSON represents [PArtifact] by their [Metadata.Name].
func (s P) MarshalJSON() ([]byte, error) {
names := make([]string, len(s))
for i, p := range s {
names[i] = GetMetadata(p).Name
}
return json.Marshal(names)
}
// UnmarshalJSON resolves the value created by MarshalJSON back to [P].
func (s *P) UnmarshalJSON(data []byte) error {
var names []string
if err := json.Unmarshal(data, &names); err != nil {
return err
}
*s = make(P, len(names))
for i, name := range names {
if p, ok := ResolveName(name); !ok {
return fmt.Errorf("unknown artifact %q", name)
} else {
(*s)[i] = p
}
}
return nil
}
// Metadata is stage-agnostic information of a [PArtifact] not directly
// representable in the resulting [pkg.Artifact].
type Metadata struct {
f func(t Toolchain) (a pkg.Artifact, version string)
// Unique package name.
Name string `json:"name"`
// Short user-facing description.
Description string `json:"description"`
// Project home page.
Website string `json:"website,omitempty"`
// Runtime dependencies.
Dependencies P `json:"dependencies"`
// Project identifier on [Anitya].
//
// [Anitya]: https://release-monitoring.org/
ID int `json:"-"`
// Optional custom version checking behaviour.
latest func(v *Versions) string
}
// GetLatest returns the latest version described by v.
func (meta *Metadata) GetLatest(v *Versions) string {
if meta.latest != nil {
return meta.latest(v)
}
return v.Latest
}
// Unversioned denotes an unversioned [PArtifact].
const Unversioned = "\x00"
// UnpopulatedIDError is returned by [Metadata.GetLatest] for an instance of
// [Metadata] where ID is not populated.
type UnpopulatedIDError struct{}
func (UnpopulatedIDError) Unwrap() error { return errors.ErrUnsupported }
func (UnpopulatedIDError) Error() string { return "Anitya ID is not populated" }
// Versions are package versions returned by Anitya.
type Versions struct {
// The latest version for the project, as determined by the version sorting algorithm.
Latest string `json:"latest_version"`
// List of all versions that arent flagged as pre-release.
Stable []string `json:"stable_versions"`
// List of all versions stored, sorted from newest to oldest.
All []string `json:"versions"`
}
// getStable returns the first Stable version, or Latest if that is unavailable.
func (v *Versions) getStable() string {
if len(v.Stable) == 0 {
return v.Latest
}
return v.Stable[0]
}
// GetVersions returns versions fetched from Anitya.
func (meta *Metadata) GetVersions(ctx context.Context) (*Versions, error) {
if meta.ID == 0 {
return nil, UnpopulatedIDError{}
}
var resp *http.Response
if req, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
"https://release-monitoring.org/api/v2/versions/?project_id="+
strconv.Itoa(meta.ID),
nil,
); err != nil {
return nil, err
} else {
req.Header.Set("User-Agent", "Rosa/1.1")
if resp, err = http.DefaultClient.Do(req); err != nil {
return nil, err
}
}
var v Versions
err := json.NewDecoder(resp.Body).Decode(&v)
return &v, errors.Join(err, resp.Body.Close())
}
var (
// artifactsM is an array of [PArtifact] metadata.
artifactsM [PresetEnd]Metadata
// artifacts stores the result of Metadata.f.
artifacts [_toolchainEnd][len(artifactsM)]struct {
a pkg.Artifact
v string
}
// artifactsOnce is for lazy initialisation of artifacts.
artifactsOnce [_toolchainEnd][len(artifactsM)]sync.Once
// arch is the target architecture.
arch = runtime.GOARCH
// presetOpts globally modifies behaviour of presets.
presetOpts int
)
const (
// OptSkipCheck skips running all test suites.
OptSkipCheck = 1 << iota
// OptLLVMNoLTO disables LTO in all [LLVM] stages.
OptLLVMNoLTO
)
// Arch returns the target architecture.
func Arch() string { return arch }
// Flags returns the current preset flags
func Flags() int { return presetOpts }
// zero zeros the value pointed to by p.
func zero[T any](p *T) { var v T; *p = v }
// DropCaches arranges for all cached [pkg.Artifact] to be freed some time after
// it returns. Must not be used concurrently with any other function from this
// package.
func DropCaches(targetArch string, flags int) {
if targetArch == "" {
targetArch = runtime.GOARCH
}
arch = targetArch
presetOpts = flags
zero(&artifacts)
zero(&artifactsOnce)
}
// GetMetadata returns [Metadata] of a [PArtifact].
func GetMetadata(p PArtifact) *Metadata { return &artifactsM[p] }
// construct constructs a [pkg.Artifact] corresponding to a [PArtifact] once.
func (t Toolchain) construct(p PArtifact) {
artifactsOnce[t][p].Do(func() {
artifacts[t][p].a, artifacts[t][p].v = artifactsM[p].f(t)
})
}
// Load returns the resulting [pkg.Artifact] of [PArtifact].
func (t Toolchain) Load(p PArtifact) pkg.Artifact {
t.construct(p)
return artifacts[t][p].a
}
// Version returns the version string of [PArtifact].
func (t Toolchain) Version(p PArtifact) string {
t.construct(p)
return artifacts[t][p].v
}
// ResolveName returns a [PArtifact] by name.
func ResolveName(name string) (p PArtifact, ok bool) {
for i := range PresetUnexportedStart {
if name == artifactsM[i].Name {
return i, true
}
}
return 0, false
}
+81
View File
@@ -0,0 +1,81 @@
package rosa_test
import (
"testing"
"hakurei.app/internal/rosa"
)
func TestLoad(t *testing.T) {
t.Parallel()
for i := range rosa.PresetEnd {
p := rosa.PArtifact(i)
t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
t.Parallel()
rosa.Std.Load(p)
})
}
}
func BenchmarkAll(b *testing.B) {
arch, flags := rosa.Arch(), rosa.Flags()
b.Cleanup(func() { rosa.DropCaches(arch, flags) })
for b.Loop() {
for i := range rosa.PresetEnd {
rosa.Std.Load(rosa.PArtifact(i))
}
b.StopTimer()
rosa.DropCaches("", 0)
b.StartTimer()
}
}
func TestResolveName(t *testing.T) {
t.Parallel()
for i := range rosa.PresetUnexportedStart {
p := i
name := rosa.GetMetadata(p).Name
t.Run(name, func(t *testing.T) {
t.Parallel()
if got, ok := rosa.ResolveName(name); !ok {
t.Fatal("ResolveName: ok = false")
} else if got != p {
t.Fatalf("ResolveName: %d, want %d", got, p)
}
})
}
}
func TestResolveNameUnexported(t *testing.T) {
t.Parallel()
for i := rosa.PresetUnexportedStart; i < rosa.PresetEnd; i++ {
p := i
name := rosa.GetMetadata(p).Name
t.Run(name, func(t *testing.T) {
t.Parallel()
if got, ok := rosa.ResolveName(name); ok {
t.Fatalf("ResolveName: resolved unexported preset %d", got)
}
})
}
}
func TestUnique(t *testing.T) {
t.Parallel()
names := make(map[string]struct{})
for i := range rosa.PresetEnd {
name := rosa.GetMetadata(rosa.PArtifact(i)).Name
if _, ok := names[name]; ok {
t.Fatalf("name %s is not unique", name)
}
names[name] = struct{}{}
}
}
+36
View File
@@ -0,0 +1,36 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newArgpStandalone() (pkg.Artifact, string) {
const (
version = "1.3"
checksum = "vtW0VyO2pJ-hPyYmDI2zwSLS8QL0sPAUKC1t3zNYbwN2TmsaE-fADhaVtNd3eNFl"
)
return t.NewPackage("argp-standalone", version, newTar(
"http://www.lysator.liu.se/~nisse/misc/"+
"argp-standalone-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
Env: []string{
"CC=cc -std=gnu89 -fPIC",
},
}, &MakeHelper{
Install: `
install -D -m644 /usr/src/argp-standalone/argp.h /work/system/include/argp.h
install -D -m755 libargp.a /work/system/lib/libargp.a
`,
},
Diffutils,
), version
}
func init() {
artifactsM[ArgpStandalone] = Metadata{
f: Toolchain.newArgpStandalone,
Name: "argp-standalone",
Description: "hierarchical argument parsing library broken out from glibc",
Website: "http://www.lysator.liu.se/~nisse/misc/",
}
}
-348
View File
@@ -1,348 +0,0 @@
// Package azalea implements a proof-of-concept, domain-specific language for
// Rosa OS software packaging.
package azalea
import (
"errors"
"io"
"strconv"
"text/scanner"
)
// idents are runes accepted in an identifier.
var idents = [...]bool{
'0': true, '1': true, '2': true, '3': true, '4': true, '5': true, '6': true,
'7': true, '8': true, '9': true,
'A': true, 'B': true, 'C': true, 'D': true, 'E': true, 'F': true, 'G': true,
'H': true, 'I': true, 'J': true, 'K': true, 'L': true, 'M': true, 'N': true,
'O': true, 'P': true, 'Q': true, 'R': true, 'S': true, 'T': true, 'U': true,
'V': true, 'W': true, 'X': true, 'Y': true, 'Z': true,
'a': true, 'b': true, 'c': true, 'd': true, 'e': true, 'f': true, 'g': true,
'h': true, 'i': true, 'j': true, 'k': true, 'l': true, 'm': true, 'n': true,
'o': true, 'p': true, 'q': true, 'r': true, 's': true, 't': true, 'u': true,
'v': true, 'w': true, 'x': true, 'y': true, 'z': true,
'-': true, '_': true,
}
// TokenError describes an unexpected token.
type TokenError [2]rune
func (e TokenError) Error() string {
return "expected " + scanner.TokenString(e[0]) +
", found " + scanner.TokenString(e[1])
}
// ExprError is an unexpected token encountered while parsing an expression.
type ExprError rune
func (e ExprError) Error() string {
return "unexpected token " + scanner.TokenString(rune(e))
}
// must1 returns v, or panics if err is not nil.
func must1[T any](v T, err error) T {
if err != nil {
panic(err)
}
return v
}
// parser retains the current token.
type parser struct {
s scanner.Scanner
tok rune
}
// scan advances the underlying scanner to the next token, storing its result.
func (p *parser) scan() rune { p.tok = p.s.Scan(); return p.tok }
// expects panics with [TokenError] for an unexpected tok.
func (p *parser) expects(expects rune) {
if p.tok != expects {
panic(TokenError{expects, p.tok})
}
}
// scanAs advances the scanner for an expected token.
func (p *parser) scanAs(expects rune) { p.scan(); p.expects(expects) }
// An Int is the value represented by an integer literal.
type Int int64
func (v Int) GoString() string {
return "azalea.Int(" + strconv.FormatInt(int64(v), 10) + ")"
}
// parseInt parses the current token as a base 10 representation of a 64-bit
// signed integer.
func (p *parser) parseInt() Int {
v, err := strconv.ParseInt(p.s.TokenText(), 10, 64)
return must1(Int(v), err)
}
// A String holds the unquoted content of a string literal.
type String string
func (v String) GoString() string {
return "azalea.String(" + strconv.Quote(string(v)) + ")"
}
// parseString parses the current token as a string.
func (p *parser) parseString() String {
s, err := strconv.Unquote(p.s.TokenText())
return must1(String(s), err)
}
// An Ident holds the name of an identifier.
type Ident string
func (v Ident) GoString() string {
return "azalea.Ident(" + strconv.Quote(string(v)) + ")"
}
// A Val are statements joined by the '+' operator. Only the [String] type
// supports concatenation.
type Val []any
// parseVal parses until the end of the [Val].
func (p *parser) parseVal() (v Val) {
v = append(v, p.parseExpr())
for p.tok == '+' {
p.scan()
v = append(v, p.parseExpr())
}
return
}
// An Array holds statements in an array.
type Array []Val
// A KV holds a key/value pair.
type KV struct {
K String
V Val
}
// An Arg represents an argument of [Func].
type Arg struct {
K []Ident
V Val
R bool
}
// Func is a function call or package declaration.
type Func struct {
// Function or package identifier.
Ident Ident
// Whether this is a package declaration.
Package bool
// Key-value arguments.
Args []Arg
}
// parseExpr parses the current expression.
func (p *parser) parseExpr() any {
switch p.tok {
case scanner.Int:
v := p.parseInt()
p.scan()
return v
case scanner.String, scanner.RawString:
v := p.parseString()
p.scan()
return v
case scanner.Ident:
var v Func
v.Ident = Ident(p.s.TokenText())
if v.Package = v.Ident == "package"; v.Package {
p.scanAs(scanner.Ident)
v.Ident = Ident(p.s.TokenText())
}
p.scan()
switch p.tok {
case '{':
for {
p.scan()
switch p.tok {
case '}':
p.scan()
return v
case scanner.Ident:
break
default:
panic(TokenError{scanner.Ident, p.tok})
}
arg := Arg{K: []Ident{Ident(p.s.TokenText())}}
delim := true
arg:
for {
p.scan()
switch p.tok {
case ',':
if delim {
delim = false
continue
}
panic(ExprError(p.tok))
case scanner.Ident:
if delim {
panic(TokenError{',', p.tok})
}
delim = true
arg.K = append(arg.K, Ident(p.s.TokenText()))
default:
break arg
}
}
switch p.tok {
case '=':
break
case '#':
arg.R = true
p.scanAs('=')
default:
panic(TokenError{'=', p.tok})
}
p.scan()
arg.V = p.parseVal()
v.Args = append(v.Args, arg)
p.expects(';')
}
default:
return v.Ident
}
case '{':
var v []KV
for {
p.scan()
switch p.tok {
case '}':
p.scan()
return v
case scanner.String:
pair := KV{K: p.parseString()}
p.scan()
switch p.tok {
case ';':
break
case ':':
p.scan()
pair.V = p.parseVal()
p.expects(';')
break
default:
panic(ExprError(p.tok))
}
v = append(v, pair)
default:
panic(ExprError(p.tok))
}
}
case '[':
var (
v Array
delim bool
)
p.scan()
for {
switch p.tok {
case ',':
if delim {
p.scan()
delim = false
continue
}
panic(ExprError(','))
case ']':
p.scan()
return v
case scanner.EOF:
panic(ExprError(scanner.EOF))
default:
if delim {
panic(TokenError{',', p.tok})
}
delim = true
break
}
v = append(v, p.parseVal())
}
default:
panic(ExprError(p.tok))
}
}
// ScanError is the error count parsing all expressions.
type ScanError int
func (ScanError) Error() string {
return "aborting due to scanning errors"
}
// Parse parses expressions from r.
func Parse(r io.Reader) (e []any, err error) {
var p parser
p.s.Init(r)
p.s.Mode = scanner.ScanIdents |
scanner.ScanInts |
scanner.ScanStrings |
scanner.ScanRawStrings |
scanner.ScanComments |
scanner.SkipComments
p.s.IsIdentRune = func(ch rune, i int) bool {
if i == 0 && ch >= '0' && ch <= '9' {
return false
}
return ch > 0 && ch < rune(len(idents)) && idents[ch]
}
defer func() {
v := recover()
if v == nil {
return
}
_err, ok := v.(error)
if !ok {
panic(v)
}
if err == nil {
err = _err
return
}
err = errors.Join(err, _err)
}()
p.scan()
for p.tok != scanner.EOF {
e = append(e, p.parseExpr())
}
if p.s.ErrorCount != 0 {
err = ScanError(p.s.ErrorCount)
}
return
}
-169
View File
@@ -1,169 +0,0 @@
package azalea_test
import (
_ "embed"
"reflect"
"strings"
"testing"
"text/scanner"
. "hakurei.app/internal/rosa/azalea"
)
//go:embed testdata/gcc.az
var sample string
func TestParse(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data string
want []any
err error
}{
{"invalid", "}", nil, ExprError('}')},
{"bad sep", "f{v?}", nil, TokenError{'=', '?'}},
{"bad ident", "f{9}", nil, TokenError{scanner.Ident, scanner.Int}},
{"share bad sep", "f { v,,v = v; }", nil, ExprError(',')},
{"share missing sep", "f { v v }", nil, TokenError{',', scanner.Ident}},
{"ident", `v`, []any{Ident("v")}, nil},
{"concat", `f { v = v+"\xfd"+p{}+9; }`, []any{Func{
Ident: "f",
Args: []Arg{{K: []Ident{"v"}, V: Val{
Ident("v"),
String("\xfd"),
Func{Ident: "p"},
Int(9),
}}},
}}, nil},
{"truncated string concat", `f { v = v+; }`, nil,
ExprError(';')},
{"empty pairs", `{}`, []any{[]KV(nil)}, nil},
{"short kv", `{"\x00":v;}`, []any{[]KV{
{K: "\x00", V: Val{Ident("v")}},
}}, nil},
{"truncated kv", `{"\x00"`, nil, ExprError(scanner.EOF)},
{"ident kv", `{v="";}`, nil, ExprError(scanner.Ident)},
{"empty array", `[]`, []any{Array(nil)}, nil},
{"integer array", `[9]`, []any{Array{{Int(9)}}}, nil},
{"short array", `[ "\x00" ]`, []any{
Array{{String("\x00")}},
}, nil},
{"short array delim", `[ "\x00", ]`, []any{
Array{{String("\x00")}},
}, nil},
{"missing array value", `[ "\x00", , v ]`, nil, ExprError(',')},
{"missing array delimiter", `[ v0 v1 ]`, nil, TokenError{',', scanner.Ident}},
{"truncated array", `[ "\x00"`, nil,
ExprError(scanner.EOF)},
{"gcc", sample, []any{Func{
Ident: Ident("gcc"),
Package: true,
Args: []Arg{
{K: []Ident{Ident("description")}, V: Val{String("The GNU Compiler Collection")}},
{K: []Ident{Ident("website")}, V: Val{String("https://www.gnu.org/software/gcc")}},
{K: []Ident{Ident("anitya")}, V: Val{Int(6502)}},
{K: []Ident{Ident("version")}, V: Val{String("16.1.0")}, R: true},
{K: []Ident{Ident("source")}, V: Val{Func{
Ident: Ident("remoteTar"),
Args: []Arg{
{K: []Ident{Ident("url")}, V: Val{
String("https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/"),
String("gcc-"),
Ident("version"),
String("/gcc-"),
Ident("version"),
String(".tar.gz"),
}},
{K: []Ident{Ident("checksum")}, V: Val{String("4ASoWbxaA2FW7PAB0zzHDPC5XnNhyaAyjtDPpGzceSLeYnEIXsNYZR3PA_Zu5P0K")}},
{K: []Ident{Ident("compress")}, V: Val{Ident("gzip")}},
},
}}},
{K: []Ident{Ident("patches")}, V: Val{Array{
{String("musl-off64_t-loff_t.patch")},
{String("musl-legacy-lfs.patch")},
}}},
{K: []Ident{Ident("exclusive")}, V: Val{Ident("true")}},
{K: []Ident{Ident("exec")}, V: Val{Func{
Ident: Ident("make"),
Args: []Arg{
{K: []Ident{Ident("configure")}, V: Val{[]KV{
{K: String("disable-multilib")},
{K: String("enable-default-pie")},
{K: String("disable-nls")},
{K: String("with-gnu-as")},
{K: String("with-gnu-ld")},
{K: String("with-system-zlib")},
{K: String("enable-languages"), V: Val{String("c,c++,go")}},
{K: String("with-native-system-header-dir"), V: Val{String("/system/include")}},
{K: String("with-multilib-list"), V: Val{Func{
Ident: Ident("arch"),
Args: []Arg{
{K: []Ident{Ident("amd64"), Ident("arm64")}, V: Val{String("''")}},
{K: []Ident{Ident("default")}, V: Val{Ident("unset")}},
},
}}},
}}},
{K: []Ident{Ident("make")}, V: Val{Array{
{String("BOOT_CFLAGS='-O2 -g'")},
{
Func{Ident: Ident("noop"), Args: []Arg{{K: []Ident{Ident("key")}, V: Val{Ident("value")}}}},
String("\x00"),
},
{String("bootstrap")},
}}},
{K: []Ident{Ident("skip-check")}, V: Val{Ident("true")}},
},
}}},
{K: []Ident{Ident("inputs")}, V: Val{Array{
{Ident("binutils")},
{Ident("mpc")},
{Ident("zlib")},
{Ident("libucontext")},
{Ident("kernel-headers")},
}}},
},
}}, nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
p, err := Parse(strings.NewReader(tc.data))
if !reflect.DeepEqual(p, tc.want) {
t.Errorf("Parse: %#v, want %#v", p, tc.want)
}
if !reflect.DeepEqual(err, tc.err) {
t.Errorf("Parse: error = %v, want %v", err, tc.err)
}
})
}
}
func BenchmarkParse(b *testing.B) {
r := strings.NewReader(sample)
for b.Loop() {
if _, err := Parse(r); err != nil {
b.Fatal(err)
}
b.StopTimer()
r.Reset(sample)
b.StartTimer()
}
}
-393
View File
@@ -1,393 +0,0 @@
package azalea
import (
"errors"
"fmt"
"maps"
"reflect"
"slices"
"strings"
"unique"
)
// Value are types supported by the language.
type Value interface {
bool | int64 | string | []string | []int64 | [][2]string
}
type (
// FArg is an argument passed to [F].
FArg struct {
K unique.Handle[Ident]
V any
R bool
}
// FArgs are arguments passed to [F].
FArgs []FArg
// PF implements the package declaration function.
PF func(name Ident, args FArgs) (v any, set bool, err error)
// F is the implementation of a [Func].
F struct {
F func(args FArgs) (v any, set bool, err error)
V map[unique.Handle[Ident]]any
}
)
// Apply applies named arguments and rejects unused arguments.
func (args FArgs) Apply(v map[unique.Handle[Ident]]any) error {
for _, arg := range args {
if arg.V == nil {
// unset
continue
}
r, ok := v[arg.K]
if !ok {
if arg.R {
continue
}
return UndefinedError(arg.K.Value())
}
err := storeE(r, arg.V)
if err != nil {
return err
}
}
return nil
}
// A Frame refers to local variables and debugging information.
type Frame struct {
// Local constants.
Val map[unique.Handle[Ident]]any
// Functions.
Func map[unique.Handle[Ident]]F
}
// UnsupportedExprError is an expression with invalid concrete type.
type UnsupportedExprError struct{ E any }
func (e UnsupportedExprError) Error() string {
return fmt.Sprintf("unsupported expression %#v", e.E)
}
// UndefinedError is an identifier not defined in any stack frame visible to the
// expression containing it.
type UndefinedError Ident
func (e UndefinedError) Error() string {
return "undefined: " + string(e)
}
// evaluate is evaluateAny with a type parameter.
func evaluate[T Value](d PF, s []Frame, expr any, rp *T) bool {
return evaluateAny(d, s, expr, rp)
}
// evaluateArray evaluates expr and returns its values as a slice.
func evaluateArray[T Value](d PF, s []Frame, expr Array) []T {
r := make([]T, 0, len(expr))
for i := range expr {
var _r T
if evaluate(d, s, expr[i], &_r) {
r = append(r, _r)
}
}
return r
}
// TypeError is an unexpected type during evaluation.
type TypeError struct {
Concrete, Asserted reflect.Type
}
func (e TypeError) Error() string {
return fmt.Sprintf("expected %s, got %s", e.Asserted, e.Concrete)
}
func (e TypeError) Is(err error) bool {
v, ok := errors.AsType[TypeError](err)
return ok &&
e.Asserted == v.Asserted &&
e.Concrete == v.Concrete
}
// storeE is a convenience function to set the value of a result pointer.
func storeE(rp any, r any) error {
pv := reflect.ValueOf(rp).Elem()
v := reflect.ValueOf(r)
pt, vt := pv.Type(), v.Type()
if !vt.AssignableTo(pt) {
return TypeError{vt, pt}
}
pv.Set(v)
return nil
}
// store is like storeE, but panics if error is non-nil.
func store[T Value](rp any, r T) {
err := storeE(rp, r)
if err != nil {
panic(err)
}
}
// EvaluationError is an error and the expression it occurred in.
type EvaluationError struct {
Expr any
Err error
}
// Unwrap returns the underlying error.
func (e EvaluationError) Unwrap() error { return e.Err }
// Error returns a very long error description that should not be presented
// to the user directly.
func (e EvaluationError) Error() string {
return fmt.Sprintf("expression %#v: %v", e.Expr, e.Err)
}
var (
// IdentInputs is a special array argument in a package declaration whose
// values of [Ident] are kept as is when passed to a [PF].
IdentInputs = unique.Make(Ident("inputs"))
// IdentRuntime has the same semantics as [IdentInputs].
IdentRuntime = unique.Make(Ident("runtime"))
// IdentExtra has the same semantics as [IdentInputs].
IdentExtra = unique.Make(Ident("extra"))
// IdentSource is a special argument in a package declaration where an
// assignment of a [Val] with a single [Ident] passes through the evaluator.
IdentSource = unique.Make(Ident("source"))
// ErrInvalidSpecial is panicked for a special [PF] argument sharing its
// value or set for R.
ErrInvalidSpecial = errors.New("special must not be common or bound to scope")
)
// evaluateAny implements [Evaluate].
func evaluateAny(d PF, s []Frame, expr, rp any) bool {
defer func() {
r := recover()
if r == nil {
return
}
err, ok := r.(error)
if !ok {
panic(r)
}
if _, ok = err.(EvaluationError); ok {
panic(err)
}
panic(EvaluationError{expr, err})
}()
switch e := expr.(type) {
case Int:
store(rp, int64(e))
return true
case String:
store(rp, string(e))
return true
case Ident:
var (
v any
ok bool
)
for i := range s {
v, ok = s[len(s)-1-i].Val[unique.Make(e)]
if ok {
break
}
}
if !ok {
panic(UndefinedError(e))
}
if err := storeE(rp, v); err != nil {
panic(err)
}
return true
case Val:
if len(e) == 1 {
switch v := e[0].(type) {
case Ident:
switch v {
case "unset":
return false
case "true":
store(rp, true)
return true
case "false":
store(rp, false)
return true
default:
return evaluateAny(d, s, v, rp)
}
default:
return evaluateAny(d, s, e[0], rp)
}
}
var v strings.Builder
for i := range e {
var _r string
if evaluate(d, s, e[i], &_r) {
v.WriteString(_r)
}
}
store(rp, v.String())
return true
case Array:
if len(e) > 0 && len(e[0]) == 1 {
if _, ok := e[0][0].(Int); ok {
store(rp, evaluateArray[int64](d, s, e))
return true
}
}
store(rp, evaluateArray[string](d, s, e))
return true
case []KV:
r := make([][2]string, 0, len(e))
for i := range e {
var _r string
if e[i].V == nil || evaluate(d, s, e[i].V, &_r) {
r = append(r, [2]string{string(e[i].K), _r})
}
}
store(rp, r)
return true
case Func:
var (
f F
ok bool
)
if !e.Package {
for i := range s {
f, ok = s[len(s)-1-i].Func[unique.Make(e.Ident)]
if ok {
break
}
}
if !ok {
panic(UndefinedError(e.Ident))
}
}
argc := len(e.Args)
for _, arg := range e.Args {
argc += len(arg.K) - 1
}
fargs := make([]FArg, 0, len(e.Args))
s = append(s, Frame{})
fp := &s[len(s)-1]
if !e.Package {
fp.Val = maps.Clone(f.V)
}
args:
for _, arg := range e.Args {
names := make([]unique.Handle[Ident], len(arg.K))
for i, name := range arg.K {
names[i] = unique.Make(name)
}
farg := FArg{R: arg.R}
if e.Package {
for _, special := range [...]unique.Handle[Ident]{
IdentInputs,
IdentRuntime,
IdentExtra,
} {
if slices.Contains(names, special) {
if len(names) != 1 || len(arg.V) != 1 || arg.R {
panic(ErrInvalidSpecial)
}
farg.K = names[0]
if err := storeE(&farg.V, arg.V[0]); err != nil {
panic(err)
}
fargs = append(fargs, farg)
continue args
}
}
if slices.Contains(names, IdentSource) {
if len(names) != 1 || len(arg.V) != 1 || arg.R {
panic(ErrInvalidSpecial)
}
if _, ok = arg.V[0].(Ident); ok {
farg.K = names[0]
if err := storeE(&farg.V, arg.V[0]); err != nil {
panic(err)
}
fargs = append(fargs, farg)
continue args
}
}
}
if !evaluateAny(d, s, arg.V, &farg.V) {
farg.V = nil
}
for _, name := range names {
farg.K = name
fargs = append(fargs, farg)
if arg.R && farg.V != nil {
if fp.Val == nil {
fp.Val = make(map[unique.Handle[Ident]]any)
}
(*fp).Val[name] = farg.V
}
}
}
var (
v any
err error
)
if !e.Package {
v, ok, err = f.F(fargs)
} else {
v, ok, err = d(e.Ident, fargs)
}
if err != nil {
panic(err)
} else if v != nil {
if err = storeE(rp, v); err != nil {
panic(err)
}
}
return ok
default:
panic(UnsupportedExprError{expr})
}
}
// Evaluate evaluates a statement and returns its value.
func Evaluate[T any](d PF, s []Frame, expr any) (v T, set bool, err error) {
defer func() {
r := recover()
if r == nil {
return
}
_err, ok := r.(error)
if !ok {
panic(r)
}
err = _err
}()
set = evaluateAny(d, s, expr, &v)
return
}
-407
View File
@@ -1,407 +0,0 @@
package azalea_test
import (
"errors"
"fmt"
"reflect"
"strings"
"testing"
"unique"
. "hakurei.app/internal/rosa/azalea"
)
// makeStackCheck creates a stack with a single frame with a single function "f"
// which calls the check function internally.
func makeStackCheck(check func(args FArgs) (any, error)) []Frame {
return []Frame{{Func: map[unique.Handle[Ident]]F{
unique.Make(Ident("f")): {F: func(
args FArgs,
) (v any, set bool, err error) {
set = true
v, err = check(args)
return
}},
}}}
}
// checkArgs is like makeStackCheck, but the resulting function asserts that its
// args match the expected value.
func checkArgs(want FArgs) []Frame {
return makeStackCheck(func(args FArgs) (any, error) {
if !reflect.DeepEqual(args, want) {
return nil, fmt.Errorf("%#v, want %#v", args, want)
}
return "\xfd", nil
})
}
func TestEvaluate(t *testing.T) {
t.Parallel()
testCases := []struct {
name string
data string
s []Frame
want any
err error
}{
{"apply unset", `f { v = unset; }`, makeStackCheck(func(
args FArgs,
) (v any, err error) {
v = "\xfd"
err = args.Apply(map[unique.Handle[Ident]]any{
unique.Make(Ident("v")): &v,
})
return
}), "\xfd", nil},
{"apply bad type", `f { v = 9; }`, makeStackCheck(func(
args FArgs,
) (v any, err error) {
v = "\xfd"
err = args.Apply(map[unique.Handle[Ident]]any{
unique.Make(Ident("v")): &v,
})
return
}), "", TypeError{
Concrete: reflect.TypeFor[int64](),
Asserted: reflect.TypeFor[string](),
}},
{"apply undefined", `f { v = 9; }`, makeStackCheck(func(
args FArgs,
) (v any, err error) {
v = "\xfd"
err = args.Apply(map[unique.Handle[Ident]]any{})
return
}), "", EvaluationError{
Expr: Func{
Ident: Ident("f"),
Args: []Arg{
{K: []Ident{"v"}, V: Val{Int(9)}},
},
},
Err: UndefinedError("v"),
}},
{"apply bound undefined", `f { _v# = "\x00"; v = _v; }`, makeStackCheck(func(
args FArgs,
) (v any, err error) {
v = "\xfd"
err = args.Apply(map[unique.Handle[Ident]]any{
unique.Make(Ident("v")): &v,
})
return
}), "\x00", nil},
{"undefined function", `f {}`, nil, "", EvaluationError{
Expr: Func{Ident: "f"},
Err: UndefinedError("f"),
}},
{"error wrap deep", `f { v = nil; }`, makeStackCheck(func(
FArgs,
) (any, error) {
panic("unreachable")
}), "", EvaluationError{
Expr: Ident("nil"),
Err: UndefinedError("nil"),
}},
{"common inputs", `package name { inputs, v = []; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{
"inputs",
"v",
}, V: Val{Array(nil)}},
},
},
Err: ErrInvalidSpecial,
}},
{"bound inputs", `package name { inputs# = []; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{"inputs"}, V: Val{Array(nil)}, R: true},
},
},
Err: ErrInvalidSpecial,
}},
{"bound runtime", `package name { runtime# = []; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{"runtime"}, V: Val{Array(nil)}, R: true},
},
},
Err: ErrInvalidSpecial,
}},
{"concat inputs", `package name { inputs = ""+""; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{"inputs"}, V: Val{String(""), String("")}},
},
},
Err: ErrInvalidSpecial,
}},
{"concat source", `package name { source = ""+""; }`, nil, "", EvaluationError{
Expr: Func{
Ident: Ident("name"),
Package: true,
Args: []Arg{
{K: []Ident{"source"}, V: Val{String(""), String("")}},
},
},
Err: ErrInvalidSpecial,
}},
{"source handle", `package name { source = name; }`, nil, FArgs{
{K: unique.Make(Ident("source")), V: Ident("name")},
}, nil},
{"integer array", `f { v = [ 0 ]; _v = [ 0, 9 ]; }`, checkArgs(FArgs{
{K: unique.Make(Ident("v")), V: []int64{0}},
{K: unique.Make(Ident("_v")), V: []int64{0, 9}},
}), "\xfd", nil},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
t.Parallel()
var expr Func
if e, err := Parse(strings.NewReader(tc.data)); err != nil {
t.Fatal(err)
} else if len(e) != 1 {
t.Fatalf("got expression %#v", e)
} else {
expr = e[0].(Func)
}
const rPackage = "\xff\xff\xff\xff"
r, set, err := Evaluate[string](func(
name Ident,
args FArgs,
) (v any, set bool, err error) {
v = rPackage
if !reflect.DeepEqual(args, tc.want) {
err = fmt.Errorf("%#v, want %#v", args, tc.want)
}
set = true
return
}, tc.s, expr)
if set != (err == nil) {
t.Error("Evaluate: unexpected unset")
}
if r != rPackage && r != tc.want {
t.Errorf("Evaluate: %q, want %q", r, tc.want)
}
var errEquals bool
if _, ok := errors.AsType[TypeError](err); ok {
errEquals = errors.Is(err, tc.err)
} else {
errEquals = reflect.DeepEqual(err, tc.err)
}
if !errEquals {
t.Errorf("Evaluate: error = %v, want %v", err, tc.err)
}
})
}
}
func TestEvaluateGCC(t *testing.T) {
t.Parallel()
var gcc Func
if e, err := Parse(strings.NewReader(sample)); err != nil {
t.Fatal(err)
} else {
gcc = e[0].(Func)
}
var got [3]FArgs
if r, set, err := Evaluate[string](func(
name Ident,
args FArgs,
) (v any, set bool, err error) {
v = "\x00"
set = true
got[0] = args
return
}, []Frame{{
Func: map[unique.Handle[Ident]]F{
unique.Make(Ident("remoteTar")): {F: func(
args FArgs,
) (v any, set bool, err error) {
var url, checksum string
var compress int
if err = args.Apply(map[unique.Handle[Ident]]any{
unique.Make(Ident("url")): &url,
unique.Make(Ident("checksum")): &checksum,
unique.Make(Ident("compress")): &compress,
}); err != nil {
return
}
if compress != 0xcafe {
err = fmt.Errorf("unexpected compress %#v", compress)
}
set = true
v = url + "?checksum=" + checksum
return
}, V: map[unique.Handle[Ident]]any{
unique.Make(Ident("gzip")): 0xcafe,
}},
unique.Make(Ident("make")): {F: func(
args FArgs,
) (v any, set bool, err error) {
v = args
set = true
return
}},
unique.Make(Ident("arch")): {F: func(
args FArgs,
) (v any, set bool, err error) {
set = false
got[1] = args
return
}},
unique.Make(Ident("noop")): {F: func(
args FArgs,
) (v any, set bool, err error) {
set = false
set = true
got[2] = args
return
}, V: map[unique.Handle[Ident]]any{
unique.Make(Ident("value")): "\xfd",
}},
},
}}, gcc); err != nil {
t.Fatal(err)
} else if r != "\x00" {
t.Fatalf("package: %q", r)
} else if !set {
t.Fatal("package: unset")
}
want := [...]FArgs{
{
{K: unique.Make(Ident("description")), V: "The GNU Compiler Collection"},
{K: unique.Make(Ident("website")), V: "https://www.gnu.org/software/gcc"},
{K: unique.Make(Ident("anitya")), V: int64(6502)},
{K: unique.Make(Ident("version")), V: "16.1.0", R: true},
{K: unique.Make(Ident("source")), V: "https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/gcc-16.1.0/gcc-16.1.0.tar.gz?checksum=4ASoWbxaA2FW7PAB0zzHDPC5XnNhyaAyjtDPpGzceSLeYnEIXsNYZR3PA_Zu5P0K"},
{K: unique.Make(Ident("patches")), V: []string{"musl-off64_t-loff_t.patch", "musl-legacy-lfs.patch"}},
{K: unique.Make(Ident("exclusive")), V: true},
{K: unique.Make(Ident("exec")), V: FArgs{
{K: unique.Make(Ident("configure")), V: [][2]string{
{"disable-multilib", ""},
{"enable-default-pie", ""},
{"disable-nls", ""},
{"with-gnu-as", ""},
{"with-gnu-ld", ""},
{"with-system-zlib", ""},
{"enable-languages", "c,c++,go"},
{"with-native-system-header-dir", "/system/include"},
}},
{K: unique.Make(Ident("make")), V: []string{
"BOOT_CFLAGS='-O2 -g'",
"\x00",
"bootstrap",
}},
{K: unique.Make(Ident("skip-check")), V: true},
}},
{K: unique.Make(Ident("inputs")), V: Array{
{Ident("binutils")},
{Ident("mpc")},
{Ident("zlib")},
{Ident("libucontext")},
{Ident("kernel-headers")},
}},
},
{
{K: unique.Make(Ident("amd64")), V: "''"},
{K: unique.Make(Ident("arm64")), V: "''"},
{K: unique.Make(Ident("default"))},
},
{{K: unique.Make(Ident("key")), V: "\xfd"}},
}
if !reflect.DeepEqual(got, want) {
t.Errorf("package: args = %#v, want %#v", got, want)
}
}
func BenchmarkEvaluate(b *testing.B) {
var gcc Func
if e, err := Parse(strings.NewReader(sample)); err != nil {
b.Fatal(err)
} else {
gcc = e[0].(Func)
}
s := []Frame{{
Func: map[unique.Handle[Ident]]F{
unique.Make(Ident("remoteTar")): {F: func(
FArgs,
) (v any, set bool, err error) {
return
}, V: map[unique.Handle[Ident]]any{
unique.Make(Ident("gzip")): 0xcafe,
}},
unique.Make(Ident("make")): {F: func(
FArgs,
) (v any, set bool, err error) {
return
}},
unique.Make(Ident("arch")): {F: func(
FArgs,
) (v any, set bool, err error) {
return
}},
unique.Make(Ident("noop")): {F: func(
FArgs,
) (v any, set bool, err error) {
return
}, V: map[unique.Handle[Ident]]any{
unique.Make(Ident("value")): "\xfd",
}},
},
}}
for b.Loop() {
if _, _, err := Evaluate[string](func(
Ident,
FArgs,
) (v any, set bool, err error) {
return
}, s, gcc); err != nil {
b.Fatal(err)
}
}
}
-57
View File
@@ -1,57 +0,0 @@
package gcc {
description = "The GNU Compiler Collection";
website = "https://www.gnu.org/software/gcc";
anitya = 6502;
version# = "16.1.0";
source = remoteTar {
url = "https://ftp.tsukuba.wide.ad.jp/software/gcc/releases/"+
"gcc-"+version+"/gcc-"+version+".tar.gz";
checksum = "4ASoWbxaA2FW7PAB0zzHDPC5XnNhyaAyjtDPpGzceSLeYnEIXsNYZR3PA_Zu5P0K";
compress = gzip;
};
patches = [
"musl-off64_t-loff_t.patch",
"musl-legacy-lfs.patch",
];
// GCC spends most of its time in its many configure scripts, however
// it also saturates the CPU for a consequential amount of time.
exclusive = true;
exec = make {
configure = {
"disable-multilib";
"enable-default-pie";
"disable-nls";
"with-gnu-as";
"with-gnu-ld";
"with-system-zlib";
"enable-languages": "c,c++,go";
"with-native-system-header-dir": "/system/include";
"with-multilib-list": arch {
amd64, arm64 = "''";
default = unset;
};
};
make = [
"BOOT_CFLAGS='-O2 -g'",
noop { key = value; } + "\x00",
"bootstrap",
];
// This toolchain is hacked to pieces, it is not expected to ever work
// well in its current state. That does not matter as long as the
// toolchain it produces passes its own test suite.
skip-check = true;
};
inputs = [
binutils,
mpc,
zlib,
libucontext,
kernel-headers,
];
}
-111
View File
@@ -1,111 +0,0 @@
package rosa
import (
"path"
"slices"
"strconv"
"strings"
"hakurei.app/internal/pkg"
)
const (
// jobsE is expression for preferred job count set by [pkg].
jobsE = `"$` + pkg.EnvJobs + `"`
// jobsFlagE is expression for flag with preferred job count.
jobsFlagE = `"-j$` + pkg.EnvJobs + `"`
// jobsLE is expression for twice of preferred job count set by [pkg].
jobsLE = `"$(expr ` + jobsE + ` '*' 2)"`
// jobsLFlagE is expression for flag with double of preferred job count.
jobsLFlagE = `"-j$(expr ` + jobsE + ` '*' 2)"`
)
// newTar wraps [pkg.NewHTTPGetTar] with a simpler function signature.
func newTar(url, checksum string, compress uint32) pkg.Artifact {
return pkg.NewTar(
pkg.NewDecompress(
pkg.NewHTTPGet(nil, url, mustDecode(checksum)),
compress,
),
)
}
// newFromCPAN is a helper for downloading release from CPAN.
func newFromCPAN(author, name, version, checksum string) pkg.Artifact {
return newTar(
"https://cpan.metacpan.org/authors/id/"+
author[:1]+"/"+author[:2]+"/"+author+"/"+
name+"-"+version+".tar.gz",
checksum,
pkg.Gzip,
)
}
// newFromGitLab is a helper for downloading source from GitLab.
func newFromGitLab(domain, suffix, ref, checksum string) pkg.Artifact {
return newTar(
"https://"+domain+"/"+suffix+"/-/archive/"+
ref+"/"+path.Base(suffix)+"-"+
strings.ReplaceAll(ref, "/", "-")+".tar.bz2",
checksum,
pkg.Bzip2,
)
}
// newFromGitHub is a helper for downloading source from Microsoft Github.
func newFromGitHub(suffix, tag, checksum string) pkg.Artifact {
return newTar(
"https://github.com/"+suffix+
"/archive/refs/tags/"+tag+".tar.gz",
checksum,
pkg.Gzip,
)
}
// newFromGitHubRelease is a helper for downloading release tarball from
// Microsoft Github.
func newFromGitHubRelease(
suffix, tag, name, checksum string,
compression uint32,
) pkg.Artifact {
return newTar(
"https://github.com/"+suffix+
"/releases/download/"+tag+"/"+name,
checksum,
compression,
)
}
// skipGNUTests generates a string for skipping specific tests by number in a
// GNU test suite. This is nontrivial because the test suite does not support
// excluding tests in any way, so ranges for all but the skipped tests have to
// be specified instead.
//
// For example, to skip test 764, ranges around the skipped test must be
// specified:
//
// 1-763 765-
//
// Tests are numbered starting from 1. The resulting string is unquoted.
func skipGNUTests(tests ...int64) string {
tests = slices.Clone(tests)
slices.Sort(tests)
var buf strings.Builder
if tests[0] != 1 {
buf.WriteString("1-")
}
for i, n := range tests {
if n != 1 && (i == 0 || tests[i-1] != n-1) {
buf.WriteString(strconv.Itoa(int(n - 1)))
buf.WriteString(" ")
}
if i == len(tests)-1 || tests[i+1] != n+1 {
buf.WriteString(strconv.Itoa(int(n + 1)))
buf.WriteString("-")
}
}
return buf.String()
}
+6 -6
View File
@@ -26,7 +26,7 @@ func (a busyboxBin) Params(*pkg.IContext) {}
// IsExclusive returns false: Cure performs a trivial filesystem write. // IsExclusive returns false: Cure performs a trivial filesystem write.
func (busyboxBin) IsExclusive() bool { return false } func (busyboxBin) IsExclusive() bool { return false }
// Dependencies returns the underlying busybox [pkg.FileArtifact]. // Dependencies returns the underlying busybox [pkg.File].
func (a busyboxBin) Dependencies() []pkg.Artifact { func (a busyboxBin) Dependencies() []pkg.Artifact {
return []pkg.Artifact{a.bin} return []pkg.Artifact{a.bin}
} }
@@ -85,13 +85,13 @@ func (a busyboxBin) Cure(t *pkg.TContext) (err error) {
// newBusyboxBin returns a [pkg.Artifact] containing a busybox installation from // newBusyboxBin returns a [pkg.Artifact] containing a busybox installation from
// the https://busybox.net/downloads/binaries/ binary release. // the https://busybox.net/downloads/binaries/ binary release.
func (s *S) newBusyboxBin() pkg.Artifact { func newBusyboxBin() pkg.Artifact {
var version, url, checksum string var version, url, checksum string
switch s.arch { switch arch {
case "amd64": case "amd64":
version = "1.35.0" version = "1.35.0"
url = "https://busybox.net/downloads/binaries/" + url = "https://busybox.net/downloads/binaries/" +
version + "-" + s.linuxArch() + "-linux-musl/busybox" version + "-" + linuxArch() + "-linux-musl/busybox"
checksum = "L7OBIsPu9enNHn7FqpBT1kOg_mCLNmetSeNMA3i4Y60Z5jTgnlX3qX3zcQtLx5AB" checksum = "L7OBIsPu9enNHn7FqpBT1kOg_mCLNmetSeNMA3i4Y60Z5jTgnlX3qX3zcQtLx5AB"
case "arm64": case "arm64":
version = "1.31.0" version = "1.31.0"
@@ -100,11 +100,11 @@ func (s *S) newBusyboxBin() pkg.Artifact {
checksum = "npJjBO7iwhjW6Kx2aXeSxf8kXhVgTCDChOZTTsI8ZfFfa3tbsklxRiidZQdrVERg" checksum = "npJjBO7iwhjW6Kx2aXeSxf8kXhVgTCDChOZTTsI8ZfFfa3tbsklxRiidZQdrVERg"
default: default:
panic("unsupported target " + s.arch) panic("unsupported target " + arch)
} }
return pkg.NewExec( return pkg.NewExec(
"busybox-bin-"+version, s.arch, nil, pkg.ExecTimeoutMax, false, false, "busybox-bin-"+version, arch, nil, pkg.ExecTimeoutMax, false, false,
fhs.AbsRoot, []string{ fhs.AbsRoot, []string{
"PATH=/system/bin", "PATH=/system/bin",
}, },
+38
View File
@@ -0,0 +1,38 @@
package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newBzip2() (pkg.Artifact, string) {
const (
version = "1.0.8"
checksum = "cTLykcco7boom-s05H1JVsQi1AtChYL84nXkg_92Dm1Xt94Ob_qlMg_-NSguIK-c"
)
return t.NewPackage("bzip2", version, newTar(
"https://sourceware.org/pub/bzip2/bzip2-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
Writable: true,
EnterSource: true,
}, &MakeHelper{
// uses source tree as scratch space
SkipConfigure: true,
SkipCheck: true,
InPlace: true,
Make: []string{
"CC=cc",
},
Install: "make PREFIX=/work/system install",
}), version
}
func init() {
artifactsM[Bzip2] = Metadata{
f: Toolchain.newBzip2,
Name: "bzip2",
Description: "a freely available, patent free, high-quality data compressor",
Website: "https://sourceware.org/bzip2/",
ID: 237,
}
}
+120 -11
View File
@@ -4,14 +4,120 @@ import (
"path/filepath" "path/filepath"
"slices" "slices"
"strings" "strings"
"hakurei.app/internal/pkg"
) )
var ( func (t Toolchain) newCMake() (pkg.Artifact, string) {
_cmake = H("cmake") const (
_ninja = H("ninja") version = "4.3.2"
checksum = "6QylwRVKletndTSkZTV2YBRwgd_9rUVgav_QW23HpjUgV21AVYZOUOal8tdBDmO7"
) )
return t.NewPackage("cmake", version, newFromGitHubRelease(
"Kitware/CMake",
"v"+version,
"cmake-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
// test suite expects writable source tree
Writable: true,
// CMakeHelper builds and tests a CMake project with specified CACHE entries. // expected to be writable in the copy made during bootstrap
Chmod: true,
Patches: []KV{
{"bootstrap-test-no-openssl", `diff --git a/Tests/BootstrapTest.cmake b/Tests/BootstrapTest.cmake
index 137de78bc1..b4da52e664 100644
--- a/Tests/BootstrapTest.cmake
+++ b/Tests/BootstrapTest.cmake
@@ -9,7 +9,7 @@ if(NOT nproc EQUAL 0)
endif()
message(STATUS "running bootstrap: ${bootstrap} ${ninja_arg} ${parallel_arg}")
execute_process(
- COMMAND ${bootstrap} ${ninja_arg} ${parallel_arg}
+ COMMAND ${bootstrap} ${ninja_arg} ${parallel_arg} -- -DCMAKE_USE_OPENSSL=OFF
WORKING_DIRECTORY "${bin_dir}"
RESULT_VARIABLE result
)
`},
{"disable-broken-tests-musl", `diff --git a/Tests/CMakeLists.txt b/Tests/CMakeLists.txt
index 2ead810437..f85cbb8b1c 100644
--- a/Tests/CMakeLists.txt
+++ b/Tests/CMakeLists.txt
@@ -384,7 +384,6 @@ if(BUILD_TESTING)
add_subdirectory(CMakeLib)
endif()
add_subdirectory(CMakeOnly)
- add_subdirectory(RunCMake)
add_subdirectory(FindPackageModeMakefileTest)
@@ -528,9 +527,6 @@ if(BUILD_TESTING)
-DCMake_TEST_CUDA:BOOL=${CMake_TEST_CUDA}
-DCMake_INSTALL_NAME_TOOL_BUG:BOOL=${CMake_INSTALL_NAME_TOOL_BUG}
)
- ADD_TEST_MACRO(ExportImport ExportImport)
- set_property(TEST ExportImport APPEND
- PROPERTY LABELS "CUDA")
ADD_TEST_MACRO(Unset Unset)
ADD_TEST_MACRO(PolicyScope PolicyScope)
ADD_TEST_MACRO(EmptyLibrary EmptyLibrary)
@@ -624,7 +620,6 @@ if(BUILD_TESTING)
# run test for BundleUtilities on supported platforms/compilers
if((MSVC OR
MINGW OR
- CMAKE_SYSTEM_NAME MATCHES "Linux" OR
CMAKE_SYSTEM_NAME MATCHES "Darwin")
AND NOT CMAKE_GENERATOR STREQUAL "Watcom WMake")
@@ -3095,10 +3090,6 @@ if(BUILD_TESTING)
"${CMake_SOURCE_DIR}/Tests/CTestTestFdSetSize/test.cmake.in"
"${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/test.cmake"
@ONLY ESCAPE_QUOTES)
- add_test(CTestTestFdSetSize ${CMAKE_CTEST_COMMAND}
- -S "${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/test.cmake" -j20 -V --timeout 120
- --output-log "${CMake_BINARY_DIR}/Tests/CTestTestFdSetSize/testOutput.log"
- )
if(CMAKE_TESTS_CDASH_SERVER)
set(regex "^([^:]+)://([^/]+)(.*)$")
`},
},
}, &MakeHelper{
OmitDefaults: true,
ConfigureName: "/usr/src/cmake/bootstrap",
Configure: []KV{
{"prefix", "/system"},
{"parallel", jobsE},
{"--"},
{"-DCMAKE_USE_OPENSSL", "OFF"},
{"-DCMake_TEST_NO_NETWORK", "ON"},
},
Check: []string{
"CTEST_OUTPUT_ON_FAILURE=1",
"CTEST_PARALLEL_LEVEL=128",
"test",
},
},
KernelHeaders,
), version
}
func init() {
artifactsM[CMake] = Metadata{
f: Toolchain.newCMake,
Name: "cmake",
Description: "cross-platform, open-source build system",
Website: "https://cmake.org/",
ID: 306,
}
}
// CMakeHelper is the [CMake] build system helper.
type CMakeHelper struct { type CMakeHelper struct {
// Path elements joined with source. // Path elements joined with source.
Append []string Append []string
@@ -34,12 +140,12 @@ type CMakeHelper struct {
var _ Helper = new(CMakeHelper) var _ Helper = new(CMakeHelper)
// extra returns the cmake handle alongside either ninja or make. // extra returns a hardcoded slice of [CMake] and [Ninja].
func (attr *CMakeHelper) extra(int) P { func (attr *CMakeHelper) extra(int) P {
if attr != nil && attr.Make { if attr != nil && attr.Make {
return P{_cmake, _make} return P{CMake, Make}
} }
return P{_cmake, _ninja} return P{CMake, Ninja}
} }
// wantsChmod returns false. // wantsChmod returns false.
@@ -51,11 +157,14 @@ func (*CMakeHelper) wantsWrite() bool { return false }
// scriptEarly returns the zero value. // scriptEarly returns the zero value.
func (*CMakeHelper) scriptEarly() string { return "" } func (*CMakeHelper) scriptEarly() string { return "" }
// createDir returns true.
func (*CMakeHelper) createDir() bool { return true }
// wantsDir returns a hardcoded, deterministic pathname. // wantsDir returns a hardcoded, deterministic pathname.
func (*CMakeHelper) wantsDir() (string, bool) { return "/cure/", true } func (*CMakeHelper) wantsDir() string { return "/cure/" }
// script generates the cure script. // script generates the cure script.
func (attr *CMakeHelper) script(t Toolchain, name string) string { func (attr *CMakeHelper) script(name string) string {
if attr == nil { if attr == nil {
attr = new(CMakeHelper) attr = new(CMakeHelper)
} }
@@ -71,7 +180,7 @@ func (attr *CMakeHelper) script(t Toolchain, name string) string {
} }
script := attr.Script script := attr.Script
if !attr.SkipTest && t.opts&OptSkipCheck == 0 { if !attr.SkipTest && presetOpts&OptSkipCheck == 0 {
script += "\n" + test script += "\n" + test
} }
@@ -98,6 +207,6 @@ cmake -G ` + generate + ` \
-DCMAKE_INSTALL_PREFIX=/system \ -DCMAKE_INSTALL_PREFIX=/system \
'/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `' '/usr/src/` + name + `/` + filepath.Join(attr.Append...) + `'
cmake --build . --parallel=` + jobsE + ` cmake --build . --parallel=` + jobsE + `
DESTDIR=/work cmake --install . cmake --install . --prefix=/work/system
` + script ` + script
} }
@@ -1,4 +1,20 @@
musl does not implement res_ninit package rosa
import "hakurei.app/internal/pkg"
func (t Toolchain) newConnman() (pkg.Artifact, string) {
const (
version = "2.0"
checksum = "MhVTdJOhndnZn2SWd8URKo_Pj7Zvc14tntEbrVOf9L3yVWJvpb3v3Q6104tWJgtW"
)
return t.NewPackage("connman", version, newTar(
"https://git.kernel.org/pub/scm/network/connman/connman.git/"+
"snapshot/connman-"+version+".tar.gz",
checksum,
pkg.TarGzip,
), &PackageAttr{
Patches: []KV{
{"alpine-musl-res", `musl does not implement res_ninit
--- a/gweb/gresolv.c --- a/gweb/gresolv.c
+++ b/gweb/gresolv.c +++ b/gweb/gresolv.c
@@ -57,3 +73,37 @@ musl does not implement res_ninit
} }
if (!resolv->nameserver_list) if (!resolv->nameserver_list)
`},
},
}, &MakeHelper{
Generate: "./bootstrap",
},
Automake,
Libtool,
PkgConfig,
DBus,
IPTables,
GnuTLS,
Readline,
KernelHeaders,
), version
}
func init() {
artifactsM[Connman] = Metadata{
f: Toolchain.newConnman,
Name: "connman",
Description: "a daemon for managing Internet connections",
Website: "https://git.kernel.org/pub/scm/network/connman/connman.git/",
Dependencies: P{
DBus,
IPTables,
GnuTLS,
Readline,
},
ID: 337,
}
}

Some files were not shown because too many files have changed in this diff Show More