Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
2de690e9d6
|
|||
|
058962ca5b
|
|||
|
086cf02d69
|
|||
|
aeb90feaa9
|
|||
|
ba632368f3
|
|||
|
69a1fe4474
|
|||
|
f7ef64a2f5
|
|||
|
ca62d95559
|
|||
|
06ac81cc07
|
|||
|
1b54841d62
|
|||
|
f24ebceb9c
|
|||
|
d90d0aaef8
|
@@ -13,6 +13,3 @@
|
|||||||
|
|
||||||
# cmd/dist default destination
|
# cmd/dist default destination
|
||||||
/dist
|
/dist
|
||||||
|
|
||||||
# local packages
|
|
||||||
/internal/rosa/package/local
|
|
||||||
|
|||||||
+2
-8
@@ -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
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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()
|
|
||||||
}
|
|
||||||
Vendored
-1
@@ -1 +0,0 @@
|
|||||||
v0.4.4
|
|
||||||
Vendored
+1
-6
@@ -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
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -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(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
@@ -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())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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
|
||||||
|
|||||||
+7
-7
@@ -39,13 +39,14 @@ func commandInfo(
|
|||||||
t := rosa.Native().Std()
|
t := rosa.Native().Std()
|
||||||
for i, name := range args {
|
for i, name := range args {
|
||||||
handle := rosa.ArtifactH(unique.Make(name))
|
handle := rosa.ArtifactH(unique.Make(name))
|
||||||
if meta, a := t.Load(handle); meta == nil {
|
if meta := rosa.Native().Get(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 meta.Version != rosa.Unversioned {
|
a, version := t.MustLoad(handle)
|
||||||
suffix += "-" + meta.Version
|
if version != rosa.Unversioned {
|
||||||
|
suffix += "-" + version
|
||||||
}
|
}
|
||||||
mustPrintln("name : " + name + suffix)
|
mustPrintln("name : " + name + suffix)
|
||||||
|
|
||||||
@@ -57,10 +58,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.Native().MustGet(d).Name
|
||||||
s := _meta.Name
|
if _, _version := t.Load(d); _version != rosa.Unversioned {
|
||||||
if _meta.Version != rosa.Unversioned {
|
s += "-" + _version
|
||||||
s += "-" + _meta.Version
|
|
||||||
}
|
}
|
||||||
mustPrint(" " + s)
|
mustPrint(" " + s)
|
||||||
}
|
}
|
||||||
|
|||||||
+16
-16
@@ -22,12 +22,12 @@ func TestInfo(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
_t := rosa.Native().Std()
|
_t := rosa.Native().Std()
|
||||||
qemuMeta, _ := _t.Load(rosa.H("qemu"))
|
_, qemuVersion := _t.Load(rosa.QEMU)
|
||||||
glibMeta, _ := _t.Load(rosa.H("glib"))
|
_, glibVersion := _t.Load(rosa.GLib)
|
||||||
zlibMeta, zlib := _t.Load(rosa.H("zlib"))
|
zlib, zlibVersion := _t.Load(rosa.Zlib)
|
||||||
zstdMeta, _ := _t.Load(rosa.H("zstd"))
|
_, zstdVersion := _t.Load(rosa.Zstd)
|
||||||
hakureiMeta, _ := _t.Load(rosa.H("hakurei"))
|
_, hakureiVersion := _t.Load(rosa.Hakurei)
|
||||||
hakureiDistMeta, _ := _t.Load(rosa.H("hakurei-dist"))
|
_, hakureiDistVersion := _t.Load(rosa.HakureiDist)
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -38,24 +38,24 @@ func TestInfo(t *testing.T) {
|
|||||||
wantErr any
|
wantErr any
|
||||||
}{
|
}{
|
||||||
{"qemu", []string{"qemu"}, nil, "", `
|
{"qemu", []string{"qemu"}, nil, "", `
|
||||||
name : qemu-` + qemuMeta.Version + `
|
name : qemu-` + qemuVersion + `
|
||||||
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-` + glibVersion + ` zstd-` + zstdVersion + `
|
||||||
`, nil},
|
`, nil},
|
||||||
|
|
||||||
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
||||||
name : hakurei-` + hakureiMeta.Version + `
|
name : hakurei-` + hakureiVersion + `
|
||||||
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-` + hakureiDistVersion + `
|
||||||
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-` + zlibVersion + `
|
||||||
description : lossless data-compression library
|
description : lossless data-compression library
|
||||||
website : https://zlib.net
|
website : https://zlib.net
|
||||||
|
|
||||||
@@ -65,12 +65,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-` + zlibVersion + `
|
||||||
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-` + zstdVersion + `
|
||||||
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,7 +79,7 @@ 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-` + zlibVersion + `
|
||||||
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 {
|
||||||
@@ -91,7 +91,7 @@ website : https://zlib.net
|
|||||||
}},
|
}},
|
||||||
|
|
||||||
{"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-` + zlibVersion + `
|
||||||
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,7 +140,7 @@ 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)))
|
a, _ := _t.Load(rosa.ArtifactH(unique.Make(name)))
|
||||||
if a == nil {
|
if a == nil {
|
||||||
t.Fatalf("invalid name %q", name)
|
t.Fatalf("invalid name %q", name)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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.Native().Count())
|
||||||
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
|
||||||
|
|||||||
@@ -31,7 +31,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()})
|
}{rosa.Native().Count(), info.Version()})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAPIGet(t *testing.T) {
|
func TestAPIGet(t *testing.T) {
|
||||||
@@ -92,12 +92,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, rosa.Native().Count()-1,
|
||||||
"index must be an integer between 0 and "+strconv.Itoa(count-1),
|
"index must be an integer between 0 and "+strconv.Itoa(rosa.Native().Count()-1),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -33,10 +33,10 @@ 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
|
handle rosa.ArtifactH
|
||||||
*rosa.Metadata
|
*rosa.Artifact
|
||||||
|
|
||||||
// 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"`
|
||||||
@@ -61,12 +61,12 @@ func (index *packageIndex) populate(report *rosa.Report) (err error) {
|
|||||||
index.names = make(map[string]*metadata)
|
index.names = make(map[string]*metadata)
|
||||||
ir := pkg.NewIR()
|
ir := pkg.NewIR()
|
||||||
for i, handle := range handles {
|
for i, handle := range handles {
|
||||||
meta, a := rosa.Native().Std().MustLoad(handle)
|
a, version := rosa.Native().Std().MustLoad(handle)
|
||||||
m := metadata{
|
m := metadata{
|
||||||
handle: handle,
|
handle: handle,
|
||||||
|
|
||||||
Metadata: meta,
|
Artifact: rosa.Native().MustGet(handle),
|
||||||
Version: meta.Version,
|
Version: version,
|
||||||
}
|
}
|
||||||
if m.Version == "" {
|
if m.Version == "" {
|
||||||
return errors.New("invalid version from " + m.Name)
|
return errors.New("invalid version from " + m.Name)
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
// Import all test files to register their test suites.
|
||||||
|
import "./index_test.js";
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
import { suite, test } from "./jstest/jstest.js";
|
||||||
|
import "./index.js";
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
#!/usr/bin/env node
|
||||||
|
|
||||||
|
// Many editors have terminal emulators built in, so running tests with NodeJS
|
||||||
|
// provides faster iteration, especially for those acclimated to test-driven
|
||||||
|
// development.
|
||||||
|
|
||||||
|
import "../all_tests.js";
|
||||||
|
import { StreamReporter, GLOBAL_REGISTRAR } from "./jstest.js";
|
||||||
|
|
||||||
|
// TypeScript doesn't like process and Deno as their type definitions aren't
|
||||||
|
// installed, but doesn't seem to complain if they're accessed through
|
||||||
|
// globalThis.
|
||||||
|
const process: any = (globalThis as any).process;
|
||||||
|
const Deno: any = (globalThis as any).Deno;
|
||||||
|
|
||||||
|
function getArgs(): string[] {
|
||||||
|
if (process) {
|
||||||
|
const [runtime, program, ...args] = process.argv;
|
||||||
|
return args;
|
||||||
|
}
|
||||||
|
if (Deno) return Deno.args;
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
|
||||||
|
function exit(code?: number): never {
|
||||||
|
if (Deno) Deno.exit(code);
|
||||||
|
if (process) process.exit(code);
|
||||||
|
throw `exited with code ${code ?? 0}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const args = getArgs();
|
||||||
|
let verbose = false;
|
||||||
|
if (args.length > 1) {
|
||||||
|
console.error("Too many arguments");
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
if (args.length === 1) {
|
||||||
|
if (args[0] === "-v" || args[0] === "--verbose" || args[0] === "-verbose") {
|
||||||
|
verbose = true;
|
||||||
|
} else if (args[0] !== "--") {
|
||||||
|
console.error(`Unknown argument '${args[0]}'`);
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let reporter = new StreamReporter({ writeln: console.log }, verbose);
|
||||||
|
GLOBAL_REGISTRAR.run(reporter);
|
||||||
|
exit(reporter.succeeded() ? 0 : 1);
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<!-- This triangle should match success-closed.svg, fill and stroke color notwithstanding. -->
|
||||||
|
<polygon points="0,0 100,50 0,100" fill="red" stroke="red" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
<!--
|
||||||
|
! y-coordinates go before x-coordinates here to highlight the difference
|
||||||
|
! (or, lack thereof) between these numbers and the ones in failure-open.svg;
|
||||||
|
! try a textual diff. Make sure to keep the numbers in sync!
|
||||||
|
-->
|
||||||
|
<line y1="30" x1="10" y2="70" x2="50" stroke="white" stroke-width="16"/>
|
||||||
|
<line y1="30" x1="50" y2="70" x2="10" stroke="white" stroke-width="16"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 788 B |
@@ -0,0 +1,35 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!--
|
||||||
|
! This view box is a bit weird: the strokes assume they're working in a view
|
||||||
|
! box that spans from the (0,0) to (100,100), and indeed that is convenient
|
||||||
|
! conceptualizing the strokes, but the stroke itself has a considerable width
|
||||||
|
! that gets clipped by restrictive view box dimensions. Hence, the view is
|
||||||
|
! shifted from (0,0)–(100,100) to (-20,-20)–(120,120), to make room for the
|
||||||
|
! clipped stroke, while leaving behind an illusion of working in a view box
|
||||||
|
! spanning from (0,0) to (100,100).
|
||||||
|
!
|
||||||
|
! However, the resulting SVG is too close to the summary text, and CSS
|
||||||
|
! properties to add padding do not seem to work with `content:` (likely because
|
||||||
|
! they're anonymous replaced elements); thus, the width of the view is
|
||||||
|
! increased considerably to provide padding in the SVG itself, while leaving
|
||||||
|
! the strokes oblivious.
|
||||||
|
!
|
||||||
|
! It gets worse: the summary text isn't vertically aligned with the icon! As
|
||||||
|
! a flexbox cannot be used in a summary to align the marker with the text, the
|
||||||
|
! simplest and most effective solution is to reduce the height of the view box
|
||||||
|
! from 140 to 130, thereby removing some of the bottom padding present.
|
||||||
|
!
|
||||||
|
! All six SVGs use the same view box (and indeed, they refer to this comment)
|
||||||
|
! so that they all appear to be the same size and position relative to each
|
||||||
|
! other on the DOM—indeed, the view box dimensions, alongside the width,
|
||||||
|
! directly control their placement on the DOM.
|
||||||
|
!
|
||||||
|
! TL;DR: CSS is janky, overflow is weird, and SVG is awesome!
|
||||||
|
-->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<!-- This triangle should match success-open.svg, fill and stroke color notwithstanding. -->
|
||||||
|
<polygon points="0,0 100,0 50,100" fill="red" stroke="red" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
<!-- See the comment in failure-closed.svg before modifying this. -->
|
||||||
|
<line x1="30" y1="10" x2="70" y2="50" stroke="white" stroke-width="16"/>
|
||||||
|
<line x1="30" y1="50" x2="70" y2="10" stroke="white" stroke-width="16"/>
|
||||||
|
</svg>
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
import "../all_tests.js";
|
||||||
|
import { GoTestReporter, GLOBAL_REGISTRAR } from "./jstest.js";
|
||||||
|
GLOBAL_REGISTRAR.run(new GoTestReporter());
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||||
|
<link rel="stylesheet" href="./style.css">
|
||||||
|
<title>PkgServer Tests</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<noscript>
|
||||||
|
I hate JavaScript as much as you, but this page runs tests written in
|
||||||
|
JavaScript to test the functionality of code written in JavaScript, so it
|
||||||
|
wouldn't make sense for it to work without JavaScript. <strong>Please turn
|
||||||
|
JavaScript on!</strong>
|
||||||
|
</noscript>
|
||||||
|
|
||||||
|
<h1>PkgServer Tests</h1>
|
||||||
|
|
||||||
|
<main>
|
||||||
|
<p id="counters">
|
||||||
|
<span id="success-counter">0</span> succeeded, <span id="failure-counter">0</span>
|
||||||
|
failed<span id="skip-counter-text" hidden>, <span id="skip-counter">0</span> skipped</span>.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<p hidden id="success-description">Successful test</p>
|
||||||
|
<p hidden id="failure-description">Failed test</p>
|
||||||
|
<p hidden id="skip-description">Partially or fully skipped test</p>
|
||||||
|
|
||||||
|
<div id="root">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script type="module">
|
||||||
|
import "../all_tests.js";
|
||||||
|
import { DOMReporter, GLOBAL_REGISTRAR } from "./jstest.js";
|
||||||
|
GLOBAL_REGISTRAR.run(new DOMReporter());
|
||||||
|
</script>
|
||||||
|
</main>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
@@ -0,0 +1,478 @@
|
|||||||
|
// =============================================================================
|
||||||
|
// DSL
|
||||||
|
|
||||||
|
type TestTree = TestGroup | Test;
|
||||||
|
type TestGroup = { name: string; children: TestTree[] };
|
||||||
|
type Test = { name: string; test: (t: TestController) => void };
|
||||||
|
|
||||||
|
// A registrar provides a central location to register test suites.
|
||||||
|
export class TestRegistrar {
|
||||||
|
// Note that, while this is equivalent to a new tree node sans a name, the
|
||||||
|
// lack of a name provides the illusion of multiple “top-level” suites,
|
||||||
|
// while still allowing reporters to pick their favorite name—say, “JS
|
||||||
|
// tests”—were they to need to label all suites together.
|
||||||
|
#suites: TestGroup[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#suites = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
suite(name: string, children: TestTree[]) {
|
||||||
|
checkDuplicates(name, children);
|
||||||
|
this.#suites.push({ name, children });
|
||||||
|
}
|
||||||
|
|
||||||
|
run(reporter: Reporter) {
|
||||||
|
reporter.register(this.#suites);
|
||||||
|
for (const suite of this.#suites) {
|
||||||
|
for (const c of suite.children) runTests(reporter, [suite.name], c);
|
||||||
|
}
|
||||||
|
reporter.finalize();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export let GLOBAL_REGISTRAR = new TestRegistrar();
|
||||||
|
|
||||||
|
// Register a suite in the global registrar.
|
||||||
|
export function suite(name: string, children: TestTree[]) {
|
||||||
|
GLOBAL_REGISTRAR.suite(name, children);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function group(name: string, children: TestTree[]): TestTree {
|
||||||
|
checkDuplicates(name, children);
|
||||||
|
return { name, children };
|
||||||
|
}
|
||||||
|
export const context = group;
|
||||||
|
export const describe = group;
|
||||||
|
|
||||||
|
export function test(name: string, test: (t: TestController) => void): TestTree {
|
||||||
|
return { name, test };
|
||||||
|
}
|
||||||
|
|
||||||
|
// While this function could certainly refine the type to a map instead of
|
||||||
|
// simply checking for duplicates and discarding that knowledge, these test
|
||||||
|
// trees are primarily for flooding—that is, iteration—for which an array is
|
||||||
|
// better suited.
|
||||||
|
function checkDuplicates(parent: string, names: { name: string }[]) {
|
||||||
|
let seen = new Set<string>();
|
||||||
|
for (const { name } of names) {
|
||||||
|
if (seen.has(name)) {
|
||||||
|
throw new RangeError(`duplicate name '${name}' in '${parent}'`);
|
||||||
|
}
|
||||||
|
seen.add(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export type TestState = "success" | "failure" | "skip";
|
||||||
|
|
||||||
|
class AbortSentinel {}
|
||||||
|
|
||||||
|
export class TestController {
|
||||||
|
#state: TestState;
|
||||||
|
logs: string[];
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.#state = "success";
|
||||||
|
this.logs = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
getState(): TestState {
|
||||||
|
return this.#state;
|
||||||
|
}
|
||||||
|
|
||||||
|
fail() {
|
||||||
|
this.#state = "failure";
|
||||||
|
}
|
||||||
|
|
||||||
|
failed(): boolean {
|
||||||
|
return this.#state === "failure";
|
||||||
|
}
|
||||||
|
|
||||||
|
failNow(): never {
|
||||||
|
this.fail();
|
||||||
|
throw new AbortSentinel();
|
||||||
|
}
|
||||||
|
|
||||||
|
log(message: string) {
|
||||||
|
this.logs.push(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
error(message: string) {
|
||||||
|
this.log(message);
|
||||||
|
this.fail();
|
||||||
|
}
|
||||||
|
|
||||||
|
fatal(message: string): never {
|
||||||
|
this.log(message);
|
||||||
|
this.failNow();
|
||||||
|
}
|
||||||
|
|
||||||
|
skip(message?: string): never {
|
||||||
|
if (message != null) this.log(message);
|
||||||
|
if (this.#state !== "failure") this.#state = "skip";
|
||||||
|
throw new AbortSentinel();
|
||||||
|
}
|
||||||
|
|
||||||
|
skipped(): boolean {
|
||||||
|
return this.#state === "skip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Execution
|
||||||
|
|
||||||
|
export interface TestResult {
|
||||||
|
state: TestState;
|
||||||
|
logs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
function runTests(reporter: Reporter, parents: string[], node: TestTree) {
|
||||||
|
const path = [...parents, node.name];
|
||||||
|
if ("children" in node) {
|
||||||
|
for (const c of node.children) runTests(reporter, path, c);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let controller = new TestController();
|
||||||
|
try {
|
||||||
|
node.test(controller);
|
||||||
|
} catch (e) {
|
||||||
|
if (!(e instanceof AbortSentinel)) {
|
||||||
|
controller.error(extractExceptionString(e));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
reporter.update(path, { state: controller.getState(), logs: controller.logs });
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractExceptionString(e: any): string {
|
||||||
|
// String() instead of .toString() as null and undefined don't have
|
||||||
|
// properties.
|
||||||
|
const s = String(e);
|
||||||
|
if (!(e instanceof Error && e.stack)) return s;
|
||||||
|
// v8 (Chromium, NodeJS) includes the error message, while Firefox and
|
||||||
|
// WebKit do not.
|
||||||
|
if (e.stack.startsWith(s)) return e.stack;
|
||||||
|
return `${s}\n${e.stack}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// Reporting
|
||||||
|
|
||||||
|
export interface Reporter {
|
||||||
|
// A notable feature—or flaw, to some—of the DSL is that the tree of tests
|
||||||
|
// is statically known, which might greatly aid in implementing a reporter.
|
||||||
|
register(suites: TestGroup[]): void;
|
||||||
|
// While we could simply call a function with a tree representing all
|
||||||
|
// results, which would indeed greatly simplify implementation of reporters,
|
||||||
|
// simply registering a path and allowing the reporter to—either implicitly
|
||||||
|
// or explicitly—construct a tree themselves allows for results to be
|
||||||
|
// *incrementally reported*, instead of a great deal of silence until all
|
||||||
|
// tests finish.
|
||||||
|
update(path: string[], result: TestResult): void;
|
||||||
|
// With just update(), the reporter never knows when all tests have
|
||||||
|
// completed. The simplest possible use for this is to notify the user, but
|
||||||
|
// its intent is actually more tailored to the StreamReporter scenario:
|
||||||
|
// while destructively updated report rendering (as with a tree of DOM nodes
|
||||||
|
// which are mutated) always displays the results in a structured manner
|
||||||
|
// matching that of the tests, “rerendering” or otherwise destructively
|
||||||
|
// updating the rendered output might be infeasible in some paradigms, such
|
||||||
|
// as command-line applications—all existing implementations of such
|
||||||
|
// rendering both mess up the scrollback position and necessarily crop out
|
||||||
|
// some of the data at the bottom (since the top of the tree is forced to be
|
||||||
|
// at the top of the screen, as one cannot unscroll portably). Explicitly
|
||||||
|
// signaling to the reporter that no more results will be received permits
|
||||||
|
// it to simply display live test progress linearly, while building up
|
||||||
|
// a tree and displaying it once it's known the tree is complete.
|
||||||
|
finalize(): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A reporter that diligently reports absolutely nothing. This is essentially
|
||||||
|
// a way to “undo” the incremental reporting update() provides, getting back the
|
||||||
|
// underlying result tree; this makes it extremely convenient in some cases like
|
||||||
|
// testing ourself.
|
||||||
|
export class NoOpReporter implements Reporter {
|
||||||
|
suites: TestGroup[];
|
||||||
|
results: ({ path: string[] } & TestResult)[];
|
||||||
|
finalized: boolean;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.suites = [];
|
||||||
|
this.results = [];
|
||||||
|
this.finalized = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
register(suites: TestGroup[]) {
|
||||||
|
this.suites = suites;
|
||||||
|
}
|
||||||
|
|
||||||
|
update(path: string[], result: TestResult) {
|
||||||
|
this.results.push({ path, ...result });
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() {
|
||||||
|
this.finalized = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface Stream {
|
||||||
|
writeln(s: string): void;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SEP = " ❯ ";
|
||||||
|
|
||||||
|
// A simple reporter that outputs to some stream; suitable for CLIs.
|
||||||
|
export class StreamReporter implements Reporter {
|
||||||
|
stream: Stream;
|
||||||
|
verbose: boolean;
|
||||||
|
#successes: ({ path: string[] } & TestResult)[];
|
||||||
|
#failures: ({ path: string[] } & TestResult)[];
|
||||||
|
#skips: ({ path: string[] } & TestResult)[];
|
||||||
|
|
||||||
|
constructor(stream: Stream, verbose: boolean = false) {
|
||||||
|
this.stream = stream;
|
||||||
|
this.verbose = verbose;
|
||||||
|
this.#successes = [];
|
||||||
|
this.#failures = [];
|
||||||
|
this.#skips = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
succeeded(): boolean {
|
||||||
|
return this.#successes.length > 0 && this.#failures.length === 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// We don't need the structure for reporting.
|
||||||
|
register(suites: TestGroup[]) {}
|
||||||
|
|
||||||
|
update(path: string[], result: TestResult) {
|
||||||
|
if (path.length === 0) throw new RangeError("path is empty");
|
||||||
|
const pathStr = path.join(SEP);
|
||||||
|
switch (result.state) {
|
||||||
|
case "success":
|
||||||
|
this.#successes.push({ path, ...result });
|
||||||
|
// NOTE: emojis are used instead of colored Unicode symbols as
|
||||||
|
// coloring isn't possible through all streams and detecting if
|
||||||
|
// colors should be used is very difficult¹. Furthermore, ensuring
|
||||||
|
// reasonable contrast is retained on every possible theme is
|
||||||
|
// difficult, with reverse video often being the only way (which
|
||||||
|
// also has questionable support across terminal emulators), and the
|
||||||
|
// Unicode characters might be too small to be immediately
|
||||||
|
// noticeable—consider ✓ and ⚠ and ✗. Emojis have an upper hand in
|
||||||
|
// that they're more common than obscure Unicode characters—which
|
||||||
|
// also means you're likely to have an emoji font but not a font for
|
||||||
|
// some weird symbols—and that they're double-width. Finally,
|
||||||
|
// Unicode characters are often very different across fonts; some
|
||||||
|
// fonts make ⚠ filled in, while others have just an outline for the
|
||||||
|
// triangle (which is much harder to comprehend), and the various
|
||||||
|
// crosses like all of x ⨯ × ✕ ✖ ✗ 🗙 🞨 🞩 🞪 🞫 🞬 🞭 🞮 look different
|
||||||
|
// across different fonts, which makes using them for some specific
|
||||||
|
// purpose difficult. Emojis don't have this problem because emoji
|
||||||
|
// vendors try to make them look similar to each other.
|
||||||
|
//
|
||||||
|
// ¹This necessitates checking if the stream is a TTY, checking if
|
||||||
|
// $TERM is `dumb` when connected to a TTY, checking
|
||||||
|
// https://no-color.org, https://bixense.com/clicolors, and
|
||||||
|
// https://force-color.org, checking if setting the
|
||||||
|
// ENABLE_VIRTUAL_TERMINAL_PROCESSING bit on the TTY works when on
|
||||||
|
// on Windows, and doing something similar for Cygwin.
|
||||||
|
if (this.verbose) this.stream.writeln(`✅️ ${pathStr}`);
|
||||||
|
break;
|
||||||
|
case "failure":
|
||||||
|
this.#failures.push({ path, ...result });
|
||||||
|
this.stream.writeln(`⚠️ ${pathStr}`);
|
||||||
|
break;
|
||||||
|
case "skip":
|
||||||
|
this.#skips.push({ path, ...result });
|
||||||
|
this.stream.writeln(`⏭️ ${pathStr}`);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() {
|
||||||
|
if (this.verbose) this.#displaySection("successes", this.#successes, true);
|
||||||
|
this.#displaySection("failures", this.#failures);
|
||||||
|
this.#displaySection("skips", this.#skips);
|
||||||
|
this.stream.writeln("");
|
||||||
|
this.stream.writeln(
|
||||||
|
`${this.#successes.length} succeeded, ${this.#failures.length} failed` +
|
||||||
|
(this.#skips.length ? `, ${this.#skips.length} skipped` : ""),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#displaySection(name: string, data: ({ path: string[] } & TestResult)[], ignoreEmpty: boolean = false) {
|
||||||
|
if (!data.length) return;
|
||||||
|
|
||||||
|
// Transform [{ path: ["a", "b", "c"] }, { path: ["a", "b", "d"] }] into
|
||||||
|
// { "a ❯ b": ["c", "d"] }. NOTE: intermediate nodes are collapsed as
|
||||||
|
// excessive nesting is difficult to convey clearly in a text-only
|
||||||
|
// environment.
|
||||||
|
let pathMap = new Map<string, ({ name: string } & TestResult)[]>();
|
||||||
|
for (const t of data) {
|
||||||
|
if (t.path.length === 0) throw new RangeError("path is empty");
|
||||||
|
const key = t.path.slice(0, -1).join(SEP);
|
||||||
|
if (!pathMap.has(key)) pathMap.set(key, []);
|
||||||
|
pathMap.get(key)!.push({ name: t.path.at(-1)!, ...t });
|
||||||
|
}
|
||||||
|
|
||||||
|
this.stream.writeln("");
|
||||||
|
this.stream.writeln(name.toUpperCase());
|
||||||
|
this.stream.writeln("=".repeat(name.length));
|
||||||
|
|
||||||
|
for (let [path, tests] of pathMap) {
|
||||||
|
if (ignoreEmpty) tests = tests.filter((t) => t.logs.length);
|
||||||
|
if (tests.length === 0) continue;
|
||||||
|
if (tests.length === 1) {
|
||||||
|
this.#writeOutput(tests[0], path ? `${path}${SEP}` : "", false);
|
||||||
|
} else {
|
||||||
|
this.stream.writeln(path);
|
||||||
|
for (const t of tests) this.#writeOutput(t, " - ", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#writeOutput(test: { name: string } & TestResult, prefix: string, nested: boolean) {
|
||||||
|
let output = "";
|
||||||
|
if (test.logs.length) {
|
||||||
|
// Individual logs might span multiple lines, so join them together
|
||||||
|
// then split it again.
|
||||||
|
const logStr = test.logs.join("\n");
|
||||||
|
const lines = logStr.split("\n");
|
||||||
|
if (lines.length <= 1) {
|
||||||
|
output = `: ${logStr}`;
|
||||||
|
} else {
|
||||||
|
const padding = nested ? " " : " ";
|
||||||
|
output = ":\n" + lines.map((line) => padding + line).join("\n");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.stream.writeln(`${prefix}${test.name}${output}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function assertGetElementById(id: string): HTMLElement {
|
||||||
|
let elem = document.getElementById(id);
|
||||||
|
if (elem == null) throw new ReferenceError(`element with ID '${id}' missing from DOM`);
|
||||||
|
return elem;
|
||||||
|
}
|
||||||
|
|
||||||
|
// A reporter that directly translates a tree of results into a tree of
|
||||||
|
// collapsible elements in the DOM.
|
||||||
|
export class DOMReporter implements Reporter {
|
||||||
|
// It is very difficult to implement this using the statically known tree,
|
||||||
|
// because Map doesn't handle array keys properly (to store the path), and
|
||||||
|
// it's unknown of there's any way to implement it without writing one's own
|
||||||
|
// data types. Oh well; using the DOM as a data structure might seem hacky
|
||||||
|
// but it does have its benefits, apart from encouraging a tagless final.
|
||||||
|
register(suites: TestGroup[]) {}
|
||||||
|
|
||||||
|
update(path: string[], result: TestResult) {
|
||||||
|
if (path.length === 0) throw new RangeError("path is empty");
|
||||||
|
if (result.state === "skip") {
|
||||||
|
assertGetElementById("skip-counter-text").hidden = false;
|
||||||
|
}
|
||||||
|
const counter = assertGetElementById(`${result.state}-counter`);
|
||||||
|
counter.innerText = (Number(counter.innerText) + 1).toString();
|
||||||
|
|
||||||
|
let parent = assertGetElementById("root");
|
||||||
|
for (const node of path) {
|
||||||
|
let child: HTMLDetailsElement | null = null;
|
||||||
|
let summary: HTMLElement | null = null;
|
||||||
|
let d: Element;
|
||||||
|
outer: for (d of parent.children) {
|
||||||
|
if (!(d instanceof HTMLDetailsElement)) continue;
|
||||||
|
for (const s of d.children) {
|
||||||
|
if (!(s instanceof HTMLElement)) continue;
|
||||||
|
if (!(s.tagName === "SUMMARY" && s.innerText === node)) continue;
|
||||||
|
child = d;
|
||||||
|
summary = s;
|
||||||
|
break outer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!child) {
|
||||||
|
child = document.createElement("details");
|
||||||
|
child.className = "test-node";
|
||||||
|
child.ariaRoleDescription = "test";
|
||||||
|
summary = document.createElement("summary");
|
||||||
|
summary.appendChild(document.createTextNode(node));
|
||||||
|
summary.ariaRoleDescription = "test name";
|
||||||
|
child.appendChild(summary);
|
||||||
|
parent.appendChild(child);
|
||||||
|
}
|
||||||
|
if (!summary) throw new Error("unreachable as assigned above");
|
||||||
|
|
||||||
|
switch (result.state) {
|
||||||
|
case "failure":
|
||||||
|
// Only expand failures, to minimize successes and skips.
|
||||||
|
child.open = true;
|
||||||
|
child.classList.add("failure");
|
||||||
|
child.classList.remove("skip");
|
||||||
|
child.classList.remove("success");
|
||||||
|
// The summary marker does not appear in the AOM, so setting its
|
||||||
|
// alt text is fruitless; label the summary itself instead.
|
||||||
|
summary.setAttribute("aria-labelledby", "failure-description");
|
||||||
|
break;
|
||||||
|
case "skip":
|
||||||
|
if (child.classList.contains("failure")) break;
|
||||||
|
child.classList.add("skip");
|
||||||
|
child.classList.remove("success");
|
||||||
|
summary.setAttribute("aria-labelledby", "skip-description");
|
||||||
|
break;
|
||||||
|
case "success":
|
||||||
|
if (child.classList.contains("failure") || child.classList.contains("skip")) break;
|
||||||
|
child.classList.add("success");
|
||||||
|
summary.setAttribute("aria-labelledby", "success-description");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent = child;
|
||||||
|
}
|
||||||
|
|
||||||
|
const p = document.createElement("p");
|
||||||
|
p.classList.add("test-desc");
|
||||||
|
if (result.logs.length) {
|
||||||
|
const pre = document.createElement("pre");
|
||||||
|
pre.appendChild(document.createTextNode(result.logs.join("\n")));
|
||||||
|
p.appendChild(pre);
|
||||||
|
} else {
|
||||||
|
p.classList.add("italic");
|
||||||
|
p.appendChild(document.createTextNode("No output."));
|
||||||
|
}
|
||||||
|
parent.appendChild(p);
|
||||||
|
}
|
||||||
|
|
||||||
|
finalize() {}
|
||||||
|
}
|
||||||
|
|
||||||
|
interface GoNode {
|
||||||
|
name: string;
|
||||||
|
subtests?: GoNode[];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to display results via `go test`, via some glue code from the Go side.
|
||||||
|
// TODO(ophestra): said glue code has to be written.
|
||||||
|
export class GoTestReporter implements Reporter {
|
||||||
|
register(suites: TestGroup[]) {
|
||||||
|
console.log(JSON.stringify(suites.map(GoTestReporter.serialize)));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert a test tree into the one expected by the Go code.
|
||||||
|
static serialize(node: TestTree): GoNode {
|
||||||
|
return {
|
||||||
|
name: node.name,
|
||||||
|
subtests: "children" in node ? node.children.map(GoTestReporter.serialize) : undefined,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
update(path: string[], result: TestResult) {
|
||||||
|
let state: number;
|
||||||
|
switch (result.state) {
|
||||||
|
case "success": state = 0; break;
|
||||||
|
case "failure": state = 1; break;
|
||||||
|
case "skip": state = 2; break;
|
||||||
|
}
|
||||||
|
console.log(JSON.stringify({ path, state, logs: result.logs }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unnecessary but convenient on the Go side, so that it doesn't have to
|
||||||
|
// infer this via process exit.
|
||||||
|
finalize() {
|
||||||
|
console.log("null");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<!-- This triangle should match success-closed.svg, fill and stroke color notwithstanding. -->
|
||||||
|
<polygon points="0,0 100,50 0,100" fill="blue" stroke="blue" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
<!--
|
||||||
|
! This path is extremely similar to the one in skip-open.svg; before
|
||||||
|
! making minor modifications, diff the two to understand how they should
|
||||||
|
! remain in sync.
|
||||||
|
-->
|
||||||
|
<path
|
||||||
|
d="M 50,50
|
||||||
|
A 23,23 270,1,1 30,30
|
||||||
|
l -10,20
|
||||||
|
m 10,-20
|
||||||
|
l -20,-10"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 812 B |
@@ -0,0 +1,21 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<!-- This triangle should match success-open.svg, fill and stroke color notwithstanding. -->
|
||||||
|
<polygon points="0,0 100,0 50,100" fill="blue" stroke="blue" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
<!--
|
||||||
|
! This path is extremely similar to the one in skip-closed.svg; before
|
||||||
|
! making minor modifications, diff the two to understand how they should
|
||||||
|
! remain in sync.
|
||||||
|
-->
|
||||||
|
<path
|
||||||
|
d="M 50,50
|
||||||
|
A 23,23 270,1,1 70,30
|
||||||
|
l 10,-20
|
||||||
|
m -10,20
|
||||||
|
l -20,-10"
|
||||||
|
fill="none"
|
||||||
|
stroke="white"
|
||||||
|
stroke-width="12"
|
||||||
|
stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 812 B |
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* When updating the theme colors, also update them in success-closed.svg and
|
||||||
|
* success-open.svg!
|
||||||
|
*/
|
||||||
|
|
||||||
|
:root {
|
||||||
|
--bg: #d3d3d3;
|
||||||
|
--fg: black;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
:root {
|
||||||
|
--bg: #2c2c2c;
|
||||||
|
--fg: ghostwhite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
html {
|
||||||
|
background-color: var(--bg);
|
||||||
|
color: var(--fg);
|
||||||
|
}
|
||||||
|
|
||||||
|
h1, p, summary, noscript {
|
||||||
|
font-family: sans-serif;
|
||||||
|
}
|
||||||
|
|
||||||
|
noscript {
|
||||||
|
font-size: 16pt;
|
||||||
|
}
|
||||||
|
|
||||||
|
.root {
|
||||||
|
margin: 1rem 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
details.test-node {
|
||||||
|
margin-left: 1rem;
|
||||||
|
padding: 0.2rem 0.5rem;
|
||||||
|
border-left: 2px dashed var(--fg);
|
||||||
|
> summary {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
&.success > summary::marker {
|
||||||
|
/*
|
||||||
|
* WebKit only supports color and font-size properties in ::marker [1], and
|
||||||
|
* its ::-webkit-details-marker only supports hiding the marker entirely
|
||||||
|
* [2], contrary to mdn's example [3]; thus, set a color as a fallback:
|
||||||
|
* while it may not be accessible for colorblind individuals, it's better
|
||||||
|
* than no indication of a test's state for anyone, as that there's no other
|
||||||
|
* way to include an indication in the marker on WebKit.
|
||||||
|
*
|
||||||
|
* [1]: https://developer.mozilla.org/en-US/docs/Web/CSS/Reference/Selectors/::marker#browser_compatibility
|
||||||
|
* [2]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#default_style
|
||||||
|
* [3]: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/summary#changing_the_summarys_icon
|
||||||
|
*/
|
||||||
|
color: var(--fg);
|
||||||
|
content: url("./success-closed.svg") / "success";
|
||||||
|
}
|
||||||
|
&.success[open] > summary::marker {
|
||||||
|
content: url("./success-open.svg") / "success";
|
||||||
|
}
|
||||||
|
&.failure > summary::marker {
|
||||||
|
color: red;
|
||||||
|
content: url("./failure-closed.svg") / "failure";
|
||||||
|
}
|
||||||
|
&.failure[open] > summary::marker {
|
||||||
|
content: url("./failure-open.svg") / "failure";
|
||||||
|
}
|
||||||
|
&.skip > summary::marker {
|
||||||
|
color: blue;
|
||||||
|
content: url("./skip-closed.svg") / "skip";
|
||||||
|
}
|
||||||
|
&.skip[open] > summary::marker {
|
||||||
|
content: url("./skip-open.svg") / "skip";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
p.test-desc {
|
||||||
|
margin: 0 0 0 1rem;
|
||||||
|
padding: 2px 0;
|
||||||
|
> pre {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.italic {
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<style>
|
||||||
|
.adaptive-stroke {
|
||||||
|
stroke: black;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.adaptive-stroke {
|
||||||
|
stroke: ghostwhite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- When updating this triangle, also update the other five SVGs. -->
|
||||||
|
<polygon points="0,0 100,50 0,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
@@ -0,0 +1,16 @@
|
|||||||
|
<?xml version="1.0"?>
|
||||||
|
<!-- See failure-open.svg for an explanation of the view box dimensions. -->
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="20" viewBox="-20,-20 160 130">
|
||||||
|
<style>
|
||||||
|
.adaptive-stroke {
|
||||||
|
stroke: black;
|
||||||
|
}
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
.adaptive-stroke {
|
||||||
|
stroke: ghostwhite;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<!-- When updating this triangle, also update the other five SVGs. -->
|
||||||
|
<polygon points="0,0 100,0 50,100" fill="none" class="adaptive-stroke" stroke-width="15" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 572 B |
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build frontend && frontend_test
|
||||||
|
|
||||||
|
package ui
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:generate tsc -p tsconfig.test.json
|
||||||
|
//go:generate cp index.html style.css favicon.ico static
|
||||||
|
//go:generate cp jstest/index.html jstest/style.css static/jstest
|
||||||
|
//go:generate sh -c "cp jstest/*.svg static/jstest"
|
||||||
|
//go:embed static
|
||||||
|
var _static embed.FS
|
||||||
|
var static = staticFS(_static)
|
||||||
@@ -1,8 +1,11 @@
|
|||||||
|
// This file defines the common options for all TypeScript here. This shouldn't
|
||||||
|
// be directly used as the project file in builds; see tsconfig.*.json instead,
|
||||||
|
// which inherit from this file and essentially define specific build targets.
|
||||||
{
|
{
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
"target": "ES2024",
|
"target": "ES2024",
|
||||||
"strict": true,
|
"strict": true,
|
||||||
"alwaysStrict": true,
|
"alwaysStrict": true,
|
||||||
"outDir": "static"
|
"outDir": "static",
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
// Project file for building pkgserver alongside its tests. test_ui.go uses this
|
||||||
|
// as its project file.
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
}
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
// Project file for building just the pkgserver UI, with none of the testing
|
||||||
|
// stuff attached. ui_full.go uses this as its project file.
|
||||||
|
{
|
||||||
|
"extends": "./tsconfig.json",
|
||||||
|
"exclude": ["jstest", "all_tests.ts", "*_test.ts"],
|
||||||
|
}
|
||||||
@@ -1,7 +1,19 @@
|
|||||||
// Package ui holds the static web UI.
|
// Package ui holds the static web UI.
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import "net/http"
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
// staticFS is an internal helper to wrap around go:embed's filesystem.
|
||||||
|
func staticFS(static fs.FS) fs.FS {
|
||||||
|
if f, err := fs.Sub(static, "static"); err != nil {
|
||||||
|
panic(err)
|
||||||
|
} else {
|
||||||
|
return f
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Register arranges for mux to serve the embedded frontend.
|
// Register arranges for mux to serve the embedded frontend.
|
||||||
func Register(mux *http.ServeMux) {
|
func Register(mux *http.ServeMux) {
|
||||||
|
|||||||
@@ -1,21 +1,11 @@
|
|||||||
//go:build frontend
|
//go:build frontend && !frontend_test
|
||||||
|
|
||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import "embed"
|
||||||
"embed"
|
|
||||||
"io/fs"
|
|
||||||
)
|
|
||||||
|
|
||||||
//go:generate tsc
|
//go:generate tsc -p tsconfig.ui.json
|
||||||
//go:generate cp index.html style.css static
|
//go:generate cp index.html style.css static
|
||||||
//go:embed static
|
//go:embed static
|
||||||
var _static embed.FS
|
var _static embed.FS
|
||||||
|
var static = staticFS(_static)
|
||||||
var static = func() fs.FS {
|
|
||||||
if f, err := fs.Sub(_static, "static"); err != nil {
|
|
||||||
panic(err)
|
|
||||||
} else {
|
|
||||||
return f
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|||||||
+149
-285
@@ -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)
|
||||||
@@ -141,7 +100,7 @@ func main() {
|
|||||||
rosa.Native().DropCaches("", flags)
|
rosa.Native().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.Native().Std().Load(rosa.QEMU)
|
||||||
}
|
}
|
||||||
|
|
||||||
if cross {
|
if cross {
|
||||||
@@ -150,28 +109,11 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rosa.Native().DropCaches(flagArch, flags)
|
rosa.Native().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
|
||||||
@@ -417,16 +336,11 @@ func main() {
|
|||||||
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.Native().MustGet(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,13 @@ func main() {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
if latest := meta.GetLatest(v); meta.Version != latest {
|
_, version := rosa.Native().Std().Load(p)
|
||||||
|
if current, latest :=
|
||||||
|
version,
|
||||||
|
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,7 +365,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
done:
|
done:
|
||||||
for _, p := range rosa.Native().CollectAll() {
|
for _, p := range rosa.Native().Collect() {
|
||||||
select {
|
select {
|
||||||
case w <- p:
|
case w <- p:
|
||||||
break
|
break
|
||||||
@@ -467,23 +385,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,74 +401,12 @@ 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",
|
||||||
@@ -588,9 +430,8 @@ 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)
|
llvm, _ := rosa.Native().New(s - 2).Load(rosa.LLVM)
|
||||||
pathname, _, err = cache.Cure(llvm)
|
pathname, _, err = cache.Cure(llvm)
|
||||||
return
|
return
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -599,7 +440,7 @@ 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)
|
llvm, _ := rosa.Native().New(s - 1).Load(rosa.LLVM)
|
||||||
pathname, checksum[0], err = cache.Cure(llvm)
|
pathname, checksum[0], err = cache.Cure(llvm)
|
||||||
return
|
return
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -607,7 +448,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
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)
|
llvm, _ := rosa.Native().New(s).Load(rosa.LLVM)
|
||||||
pathname, checksum[1], err = cache.Cure(llvm)
|
pathname, checksum[1], err = cache.Cure(llvm)
|
||||||
return
|
return
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
@@ -626,6 +467,18 @@ func main() {
|
|||||||
"("+pkg.Encode(checksum[0].Value())+")",
|
"("+pkg.Encode(checksum[0].Value())+")",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if flagStage0 {
|
||||||
|
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||||
|
stage0, _ := rosa.Native().New(s).Load(rosa.Stage0)
|
||||||
|
pathname, _, err = cache.Cure(stage0)
|
||||||
|
return
|
||||||
|
}); err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Println(pathname)
|
||||||
|
}
|
||||||
|
|
||||||
return
|
return
|
||||||
},
|
},
|
||||||
).Flag(
|
).Flag(
|
||||||
@@ -636,6 +489,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",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -667,7 +524,7 @@ func main() {
|
|||||||
t -= 1
|
t -= 1
|
||||||
}
|
}
|
||||||
|
|
||||||
_, a := rosa.Native().New(t).Load(rosa.ArtifactH(unique.Make(args[0])))
|
a, _ := rosa.Native().New(t).Load(rosa.ArtifactH(unique.Make(args[0])))
|
||||||
if a == nil {
|
if a == nil {
|
||||||
return fmt.Errorf("unknown artifact %q", args[0])
|
return fmt.Errorf("unknown artifact %q", args[0])
|
||||||
}
|
}
|
||||||
@@ -694,7 +551,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,
|
||||||
@@ -730,7 +587,7 @@ func main() {
|
|||||||
return cache.EnterExec(
|
return cache.EnterExec(
|
||||||
ctx,
|
ctx,
|
||||||
a,
|
a,
|
||||||
"", 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",
|
||||||
)
|
)
|
||||||
@@ -840,9 +697,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 +719,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 +737,131 @@ 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"
|
handles := make([]rosa.ArtifactH, 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))
|
handles[i] = rosa.ArtifactH(unique.Make(arg))
|
||||||
if meta, _ := rosa.Native().Std().Load(handles[i]); meta == nil {
|
if rosa.Native().Get(handles[i]) == nil {
|
||||||
return fmt.Errorf("unknown artifact %q", arg)
|
return fmt.Errorf("unknown artifact %q", arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
base := rosa.H("llvm")
|
base := rosa.LLVM
|
||||||
if !flagWithToolchain {
|
if !flagWithToolchain {
|
||||||
base = rosa.H("musl")
|
base = rosa.Musl
|
||||||
}
|
}
|
||||||
handles = append(handles,
|
handles = append(handles,
|
||||||
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.Native().Std().Append(root, handles...)
|
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]
|
||||||
"SHELL=/system/bin/mksh",
|
}
|
||||||
"PATH=/system/bin",
|
cured := make(map[pkg.Artifact]cureRes)
|
||||||
"HOME=/",
|
for _, a := range root {
|
||||||
},
|
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||||
fhs.AbsProc.Append("nonexistent"),
|
pathname, checksum, err := cache.Cure(a)
|
||||||
nil,
|
if err == nil {
|
||||||
pkg.Path(fhs.AbsRoot, true, root...),
|
cured[a] = cureRes{pathname, checksum}
|
||||||
pkg.Path(
|
}
|
||||||
fhs.AbsEtc.Append("resolv.conf"), false,
|
return err
|
||||||
pkg.NewFile(
|
}); err != nil {
|
||||||
"resolv.conf",
|
return err
|
||||||
unsafe.Slice(unsafe.StringData(resolvconf), len(resolvconf)),
|
}
|
||||||
),
|
}
|
||||||
),
|
|
||||||
),
|
// explicitly open for direct error-free use from this point
|
||||||
"localhost",
|
if cm.c == nil {
|
||||||
flagSession, os.Stdin, os.Stdout, os.Stderr,
|
if err := cm.open(); err != nil {
|
||||||
rosa.AbsSystem.Append("bin", "mksh"),
|
return err
|
||||||
"sh",
|
}
|
||||||
)
|
}
|
||||||
|
|
||||||
|
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",
|
||||||
|
"PATH=/system/bin",
|
||||||
|
"HOME=/",
|
||||||
|
}
|
||||||
|
z.Path = rosa.AbsSystem.Append("bin", "mksh")
|
||||||
|
z.Args = []string{"mksh"}
|
||||||
|
z.
|
||||||
|
OverlayEphemeral(fhs.AbsRoot, layers...).
|
||||||
|
Place(
|
||||||
|
fhs.AbsEtc.Append("hosts"),
|
||||||
|
[]byte("127.0.0.1 localhost\n"),
|
||||||
|
).
|
||||||
|
Place(
|
||||||
|
fhs.AbsEtc.Append("passwd"),
|
||||||
|
[]byte("media_rw:x:1023:1023::/:/system/bin/sh\n"+
|
||||||
|
"nobody:x:65534:65534::/proc/nonexistent:/system/bin/false\n"),
|
||||||
|
).
|
||||||
|
Place(
|
||||||
|
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 +876,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(
|
||||||
|
|||||||
@@ -36,8 +36,8 @@ func TestCureAll(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
for _, handle := range rosa.Native().Collect() {
|
for _, handle := range rosa.Native().Collect() {
|
||||||
_, a := rosa.Native().Std().MustLoad(handle)
|
a, _ := rosa.Native().Std().MustLoad(handle)
|
||||||
t.Run(handle.String(), func(t *testing.T) {
|
t.Run(rosa.Native().MustGet(handle).Name, func(t *testing.T) {
|
||||||
_, err := cureRemote(t.Context(), &addr, a, 0)
|
_, err := cureRemote(t.Context(), &addr, a, 0)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Error(err)
|
t.Error(err)
|
||||||
|
|||||||
+2
-2
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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,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
@@ -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
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+9
-27
@@ -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.Upper.String()),
|
|
||||||
check.EscapeOverlayDataSegment(o.Work.String())},
|
|
||||||
lower...), check.SpecialOverlayPath)
|
|
||||||
}
|
|
||||||
return "e*" + 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())},
|
||||||
lower...), check.SpecialOverlayPath)
|
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
@@ -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"},
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
@@ -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"]}
|
|
||||||
Vendored
-1
@@ -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"]}
|
|
||||||
Vendored
-8
@@ -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"]}
|
|
||||||
Vendored
-19
@@ -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
@@ -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
@@ -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"]}
|
|
||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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).
|
||||||
|
|
||||||
|
|||||||
@@ -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...)}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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{
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
@@ -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()))
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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},
|
|
||||||
}},
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -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()), ".")
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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},
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package expected
|
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"
|
Promote = "TX3eCloaQFkV-SZIH6Jg6E5WKH--rcXY1P0jnZKmLFKWrNqnOzd4G9eIBh6i5ywN"
|
||||||
Promote = "TX3eCloaQFkV-SZIH6Jg6E5WKH--rcXY1P0jnZKmLFKWrNqnOzd4G9eIBh6i5ywN"
|
Work = "OuNiLSC68pZhAOr1YQ4WbV1tzASA0nxLEBcK7lO7MqxDY_j8dmP_C612RTuF23Lu"
|
||||||
Work = "OuNiLSC68pZhAOr1YQ4WbV1tzASA0nxLEBcK7lO7MqxDY_j8dmP_C612RTuF23Lu"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package expected
|
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"
|
Promote = "3k4V16n96Lq04gjFSKmm4sFjyQ883FFBNXgTy9s_DjeTwxT3pg_iacEh8yMb_S4m"
|
||||||
Promote = "3k4V16n96Lq04gjFSKmm4sFjyQ883FFBNXgTy9s_DjeTwxT3pg_iacEh8yMb_S4m"
|
Work = "6Q49MhFWRE3Ne6MycwAotgl1GtoU5WCHqJNWG2byYZCY-zX-IxPrWiKk7bKkNzhE"
|
||||||
Work = "6Q49MhFWRE3Ne6MycwAotgl1GtoU5WCHqJNWG2byYZCY-zX-IxPrWiKk7bKkNzhE"
|
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,11 +1,10 @@
|
|||||||
package expected
|
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"
|
Promote = "_3LPrLp--4h9k4GsNNApu9hHtAafq-GUhfU6d4hJKBDKT3bz_szOsvkXxc5sK53d"
|
||||||
Promote = "_3LPrLp--4h9k4GsNNApu9hHtAafq-GUhfU6d4hJKBDKT3bz_szOsvkXxc5sK53d"
|
Work = "FEgHeiCD_WT4wsfB-9kDH5n6cRWCEYtJmXdKZgmUUukAOoXumH_hLlosXREC-tqq"
|
||||||
Work = "FEgHeiCD_WT4wsfB-9kDH5n6cRWCEYtJmXdKZgmUUukAOoXumH_hLlosXREC-tqq"
|
|
||||||
)
|
)
|
||||||
|
|||||||
+1
-14
@@ -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
@@ -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 {
|
||||||
|
|||||||
+39
-285
@@ -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
|
if _, err := os.Stat(c.base.Append(
|
||||||
for _, name := range [...]string{
|
|
||||||
dirIdentifier,
|
dirIdentifier,
|
||||||
dirSubstitute,
|
ent.Name(),
|
||||||
} {
|
).String()); err != nil {
|
||||||
if _, err := os.Stat(c.base.Append(
|
if !errors.Is(err, os.ErrNotExist) {
|
||||||
name,
|
addErr(dir.Append(ent.Name()), err)
|
||||||
ent.Name(),
|
|
||||||
).String()); err != nil {
|
|
||||||
if !errors.Is(err, os.ErrNotExist) {
|
|
||||||
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)
|
||||||
c.exitCure(a, curesExempt)
|
if err == nil && f.status != nil {
|
||||||
|
err = os.Link(c.base.Append(
|
||||||
if err == nil {
|
dirStatus,
|
||||||
err = f.linkSubstitute(ids, substitutes)
|
ids,
|
||||||
|
).String(), c.base.Append(
|
||||||
|
dirStatus,
|
||||||
|
substitutes,
|
||||||
|
).String())
|
||||||
}
|
}
|
||||||
|
c.exitCure(a, curesExempt)
|
||||||
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) {
|
||||||
|
|||||||
+35
-300
@@ -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,9 +526,8 @@ 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
|
||||||
@@ -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) {
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user