forked from rosa/hakurei
Compare commits
264 Commits
19c76e0831
...
v0.4.1
| Author | SHA1 | Date | |
|---|---|---|---|
|
337bf20f50
|
|||
|
1cb792cf6e
|
|||
|
b2b40b07e8
|
|||
|
da11b26ec1
|
|||
|
024489e800
|
|||
|
0f795712b0
|
|||
|
7e2210ff71
|
|||
|
a71a008f3c
|
|||
|
162265b47e
|
|||
|
3fa7ac04e4
|
|||
|
bf2867d653
|
|||
|
ec0f0f6507
|
|||
|
a77a802955
|
|||
|
4407e14dfc
|
|||
|
e024d3184a
|
|||
|
8e1bf00c2d
|
|||
|
b111e22050
|
|||
|
1fa458c0be
|
|||
|
2c7ae67a67
|
|||
|
3826621b21
|
|||
|
041b505c2e
|
|||
|
e6debce649
|
|||
|
aa26b86fce
|
|||
|
a57a8fd5d8
|
|||
|
1d5d063d6a
|
|||
|
e61628a34e
|
|||
|
5a18f14929
|
|||
|
f12880688d
|
|||
|
bb5bbfe16a
|
|||
|
427e1ca37c
|
|||
|
96fdd9ecc5
|
|||
|
02771b655b
|
|||
|
d1c8d2c39b
|
|||
|
0efd742e8a
|
|||
|
ae1fe638d5
|
|||
|
445d95023b
|
|||
|
fc66f0bb47
|
|||
|
2cd6b35bee
|
|||
|
09a216c6ec
|
|||
|
44d17325c2
|
|||
|
544ce77cbc
|
|||
|
63c3c30b23
|
|||
|
d23c4ecc7c
|
|||
|
a46656dff8
|
|||
|
77db153ff5
|
|||
|
520d95bc07
|
|||
|
451df3f4e7
|
|||
|
011fac15ed
|
|||
|
347682ad0b
|
|||
|
1a2b979add
|
|||
|
b1c90cc380
|
|||
|
3a66b8143a
|
|||
|
64bbd3aabd
|
|||
|
08799a13d0
|
|||
|
1aef9c3bbb
|
|||
|
1f38303747
|
|||
|
640777b00c
|
|||
|
1d657193cf
|
|||
|
bab5406295
|
|||
|
725ae7d64d
|
|||
|
37a0c3967e
|
|||
|
ea0692548f
|
|||
|
48ea23e648
|
|||
|
40320e4920
|
|||
|
3ca0f61632
|
|||
|
6ffaac96e3
|
|||
|
13c7713d0c
|
|||
|
42389f7ec5
|
|||
|
30f130c691
|
|||
|
ceb4d26087
|
|||
|
852f3a9b3d
|
|||
|
5e02dbdb0d
|
|||
|
6a3248d472
|
|||
|
67404c98d9
|
|||
|
b9bf69cfce
|
|||
|
4648f98272
|
|||
|
11d99439ac
|
|||
|
39e4c5b8ac
|
|||
|
e8f6db38b6
|
|||
|
20d5b71575
|
|||
|
e903e7f542
|
|||
|
1caa051f4d
|
|||
|
dcdc6f7f6d
|
|||
|
5ad6f26b46
|
|||
|
7ba75a79f4
|
|||
|
9ef84d3904
|
|||
|
3b7b6e51fb
|
|||
|
b1b4debb82
|
|||
|
021cbbc2a8
|
|||
|
a4a54a4a4d
|
|||
|
04a344aac6
|
|||
|
6b98156a3d
|
|||
|
753432cf09
|
|||
|
f8902e3679
|
|||
|
8ee53a5164
|
|||
|
3981d44757
|
|||
|
9fd67e47b4
|
|||
|
4dcec40156
|
|||
|
9a274c78a3
|
|||
|
5647c3a91f
|
|||
|
992139c75d
|
|||
|
57c69b533e
|
|||
|
6f0c2a80f2
|
|||
|
08dfefb28d
|
|||
|
b081629662
|
|||
|
fba541f301
|
|||
|
5f0da3d5c2
|
|||
|
4d5841dd62
|
|||
|
9e752b588a
|
|||
|
27b1aaae38
|
|||
|
9e18de1dc2
|
|||
|
b80ea91a42
|
|||
|
30a9dfa4b8
|
|||
|
8d657b6fdf
|
|||
|
ae9b9adfd2
|
|||
|
dd6a480a21
|
|||
|
3942272c30
|
|||
|
9036986156
|
|||
|
a394971dd7
|
|||
|
9daba60809
|
|||
|
bcd79a22ff
|
|||
|
0ff7ab915b
|
|||
|
823575acac
|
|||
|
136bc0917b
|
|||
|
d6b082dd0b
|
|||
|
89d6d9576b
|
|||
|
fafce04a5d
|
|||
|
5d760a1db9
|
|||
|
d197e40b2a
|
|||
|
2008902247
|
|||
|
30ac985fd2
|
|||
|
e9fec368f8
|
|||
|
46add42f58
|
|||
|
377b61e342
|
|||
|
520c36db6d
|
|||
|
3352bb975b
|
|||
|
f7f48d57e9
|
|||
|
5c2345128e
|
|||
|
78f9676b1f
|
|||
|
5b5b676132
|
|||
|
78383fb6e8
|
|||
|
e97f6a393f
|
|||
|
eeffefd22b
|
|||
|
ac825640ab
|
|||
|
a7f7ce1795
|
|||
|
38c639e35c
|
|||
|
b2cb13e94c
|
|||
|
46f98d12d6
|
|||
|
503c7f953c
|
|||
|
15c9f6545d
|
|||
|
83b0e32c55
|
|||
|
eeaf26e7a2
|
|||
|
b587caf2e8
|
|||
|
f1c2ca4928
|
|||
|
0ca301219f
|
|||
|
e2199e1276
|
|||
|
86eacb3208
|
|||
|
8541bdd858
|
|||
|
46be0b0dc8
|
|||
|
cbe37e87e7
|
|||
|
66d741fb07
|
|||
|
0d449011f6
|
|||
|
46428ed85d
|
|||
|
081d6b463c
|
|||
|
11b3171180
|
|||
|
adbb84c3dd
|
|||
|
1084e31d95
|
|||
|
27a1b8fe0a
|
|||
|
b2141a41d7
|
|||
|
c0dff5bc87
|
|||
|
04513c0510
|
|||
|
28ebf973d6
|
|||
|
41aeb404ec
|
|||
|
0b1009786f
|
|||
|
b390640376
|
|||
|
ad2c9f36cd
|
|||
|
67db3fbb8d
|
|||
|
560cb626a1
|
|||
|
c33a6a5b7e
|
|||
|
952082bd9b
|
|||
|
24a9b24823
|
|||
|
c2e61e7987
|
|||
|
86787b3bc5
|
|||
|
cdfcfe6ce0
|
|||
|
68a2f0c240
|
|||
|
7319c7adf9
|
|||
|
e9c890cbb2
|
|||
|
6f924336fc
|
|||
|
bd88f10524
|
|||
|
e34e3b917e
|
|||
|
b0ba165107
|
|||
|
351d6c5a35
|
|||
|
f23f73701c
|
|||
|
876917229a
|
|||
|
0558032c2d
|
|||
|
c61cdc505f
|
|||
|
062edb3487
|
|||
|
e4355279a1
|
|||
|
289fdebead
|
|||
|
9c9e190db9
|
|||
|
d7d42c69a1
|
|||
|
c758e762bd
|
|||
|
10f8b1c221
|
|||
|
6907700d67
|
|||
|
0243f3ffbd
|
|||
|
cd0beeaf8e
|
|||
|
a69273ab2a
|
|||
|
4cd0f57e48
|
|||
|
33a0e6c01b
|
|||
|
d58f5c7590
|
|||
|
1da992e342
|
|||
|
9641805ec2
|
|||
|
0738f4889a
|
|||
|
7de3cfe221
|
|||
|
8b0648dd5d
|
|||
|
4667fac76c
|
|||
|
52e5443b0e
|
|||
|
130e470b60
|
|||
|
ba5ee8e3ee
|
|||
|
d1cef30877
|
|||
|
0188a3f0c7
|
|||
|
04fe3b24ce
|
|||
|
93ad551054
|
|||
|
3d54d1f176
|
|||
|
9feac7738f
|
|||
|
591a60bac9
|
|||
|
5093a06026
|
|||
|
50c1d7f880
|
|||
|
9e63633fbc
|
|||
|
61f981a34a
|
|||
|
d717c41bbe
|
|||
|
b896eec9b7
|
|||
|
8ab99e5e40
|
|||
|
2b6160ef7d
|
|||
|
4dcac7f133
|
|||
|
966fd4df9e
|
|||
|
a2cf59b989
|
|||
|
e87f59c4e4
|
|||
|
3b221c3e77
|
|||
|
ff3b385b12
|
|||
|
c6920e6ab7
|
|||
|
59b25d45fe
|
|||
|
9b99650eb1
|
|||
|
15bff9e1a6
|
|||
|
b948525c07
|
|||
|
9acbd16e9a
|
|||
|
64e5a1068b
|
|||
|
b6cbd49d8c
|
|||
|
6913b9224a
|
|||
|
9584958ecc
|
|||
|
389844b1ea
|
|||
|
5b7ab35633
|
|||
|
52b1a5a725
|
|||
|
6b78df8714
|
|||
|
dadf170a46
|
|||
|
9594832302
|
|||
|
91a2d4d6e1
|
|||
|
a854719b9f
|
|||
|
f03c0fb249
|
|||
|
a6600be34a
|
|||
|
b5592633f5
|
|||
|
584e302168
|
|||
|
141958656f
|
|||
|
648079f42c
|
32
.gitignore
vendored
32
.gitignore
vendored
@@ -1,37 +1,15 @@
|
||||
# Binaries for programs and plugins
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
*.pkg
|
||||
/hakurei
|
||||
|
||||
# Test binary, built with `go test -c`
|
||||
# produced by tools and text editors
|
||||
*.qcow2
|
||||
*.test
|
||||
|
||||
# Output of the go coverage tool, specifically when used with LiteIDE
|
||||
*.out
|
||||
|
||||
# Dependency directories (remove the comment below to include it)
|
||||
# vendor/
|
||||
|
||||
# Go workspace file
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# env file
|
||||
.env
|
||||
.idea
|
||||
.vscode
|
||||
|
||||
# go generate
|
||||
/cmd/hakurei/LICENSE
|
||||
/cmd/mbf/internal/pkgserver/ui/static
|
||||
/internal/pkg/testdata/testtool
|
||||
/internal/rosa/hakurei_current.tar.gz
|
||||
|
||||
# release
|
||||
/dist/hakurei-*
|
||||
|
||||
# interactive nixos vm
|
||||
nixos.qcow2
|
||||
# cmd/dist default destination
|
||||
/dist
|
||||
|
||||
10
all.sh
Executable file
10
all.sh
Executable file
@@ -0,0 +1,10 @@
|
||||
#!/bin/sh -e
|
||||
|
||||
TOOLCHAIN_VERSION="$(go version)"
|
||||
cd "$(dirname -- "$0")/"
|
||||
echo "Building cmd/dist using ${TOOLCHAIN_VERSION}."
|
||||
FLAGS=''
|
||||
if test -n "$VERBOSE"; then
|
||||
FLAGS="$FLAGS -v"
|
||||
fi
|
||||
go run $FLAGS --tags=dist ./cmd/dist
|
||||
@@ -2,10 +2,10 @@
|
||||
package check
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"encoding"
|
||||
"errors"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -30,6 +30,16 @@ func (e AbsoluteError) Is(target error) bool {
|
||||
// Absolute holds a pathname checked to be absolute.
|
||||
type Absolute struct{ pathname unique.Handle[string] }
|
||||
|
||||
var (
|
||||
_ encoding.TextAppender = new(Absolute)
|
||||
_ encoding.TextMarshaler = new(Absolute)
|
||||
_ encoding.TextUnmarshaler = new(Absolute)
|
||||
|
||||
_ encoding.BinaryAppender = new(Absolute)
|
||||
_ encoding.BinaryMarshaler = new(Absolute)
|
||||
_ encoding.BinaryUnmarshaler = new(Absolute)
|
||||
)
|
||||
|
||||
// ok returns whether [Absolute] is not the zero value.
|
||||
func (a *Absolute) ok() bool { return a != nil && *a != (Absolute{}) }
|
||||
|
||||
@@ -61,7 +71,7 @@ func (a *Absolute) Is(v *Absolute) bool {
|
||||
|
||||
// NewAbs checks pathname and returns a new [Absolute] if pathname is absolute.
|
||||
func NewAbs(pathname string) (*Absolute, error) {
|
||||
if !path.IsAbs(pathname) {
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return nil, AbsoluteError(pathname)
|
||||
}
|
||||
return unsafeAbs(pathname), nil
|
||||
@@ -76,46 +86,35 @@ func MustAbs(pathname string) *Absolute {
|
||||
}
|
||||
}
|
||||
|
||||
// Append calls [path.Join] with [Absolute] as the first element.
|
||||
// Append calls [filepath.Join] with [Absolute] as the first element.
|
||||
func (a *Absolute) Append(elem ...string) *Absolute {
|
||||
return unsafeAbs(path.Join(append([]string{a.String()}, elem...)...))
|
||||
return unsafeAbs(filepath.Join(append([]string{a.String()}, elem...)...))
|
||||
}
|
||||
|
||||
// Dir calls [path.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(path.Dir(a.String())) }
|
||||
// Dir calls [filepath.Dir] with [Absolute] as its argument.
|
||||
func (a *Absolute) Dir() *Absolute { return unsafeAbs(filepath.Dir(a.String())) }
|
||||
|
||||
// GobEncode returns the checked pathname.
|
||||
func (a *Absolute) GobEncode() ([]byte, error) {
|
||||
return []byte(a.String()), nil
|
||||
// AppendText appends the checked pathname.
|
||||
func (a *Absolute) AppendText(data []byte) ([]byte, error) {
|
||||
return append(data, a.String()...), nil
|
||||
}
|
||||
|
||||
// GobDecode stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) GobDecode(data []byte) error {
|
||||
// MarshalText returns the checked pathname.
|
||||
func (a *Absolute) MarshalText() ([]byte, error) { return a.AppendText(nil) }
|
||||
|
||||
// UnmarshalText stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalText(data []byte) error {
|
||||
pathname := string(data)
|
||||
if !path.IsAbs(pathname) {
|
||||
if !filepath.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
return nil
|
||||
}
|
||||
|
||||
// MarshalJSON returns a JSON representation of the checked pathname.
|
||||
func (a *Absolute) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(a.String())
|
||||
}
|
||||
|
||||
// UnmarshalJSON stores data if it represents an absolute pathname.
|
||||
func (a *Absolute) UnmarshalJSON(data []byte) error {
|
||||
var pathname string
|
||||
if err := json.Unmarshal(data, &pathname); err != nil {
|
||||
return err
|
||||
}
|
||||
if !path.IsAbs(pathname) {
|
||||
return AbsoluteError(pathname)
|
||||
}
|
||||
a.pathname = unique.Make(pathname)
|
||||
return nil
|
||||
}
|
||||
func (a *Absolute) AppendBinary(data []byte) ([]byte, error) { return a.AppendText(data) }
|
||||
func (a *Absolute) MarshalBinary() ([]byte, error) { return a.MarshalText() }
|
||||
func (a *Absolute) UnmarshalBinary(data []byte) error { return a.UnmarshalText(data) }
|
||||
|
||||
// SortAbs calls [slices.SortFunc] for a slice of [Absolute].
|
||||
func SortAbs(x []*Absolute) {
|
||||
|
||||
@@ -170,20 +170,20 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
|
||||
{"good", MustAbs("/etc"),
|
||||
nil,
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\b\xff\x80\x00\x04/etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x04/etc\x01\xfc\xc0\xed\x00\x00\x00",
|
||||
|
||||
`"/etc"`, `{"val":"/etc","magic":3236757504}`},
|
||||
{"not absolute", nil,
|
||||
AbsoluteError("etc"),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\a\xff\x80\x00\x03etc",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x0f\xff\x84\x01\x03etc\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
|
||||
`"etc"`, `{"val":"etc","magic":3236757504}`},
|
||||
{"zero", nil,
|
||||
new(AbsoluteError),
|
||||
"\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x05\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
"\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\x04\xff\x80\x00\x00",
|
||||
",\xff\x83\x03\x01\x01\x06sCheck\x01\xff\x84\x00\x01\x02\x01\bPathname\x01\xff\x80\x00\x01\x05Magic\x01\x06\x00\x00\x00\t\x7f\x06\x01\x02\xff\x82\x00\x00\x00\f\xff\x84\x01\x00\x01\xfb\x01\x81\xda\x00\x00\x00",
|
||||
`""`, `{"val":"","magic":3236757504}`},
|
||||
}
|
||||
|
||||
@@ -347,15 +347,6 @@ func TestCodecAbsolute(t *testing.T) {
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("json passthrough", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
wantErr := "invalid character ':' looking for beginning of value"
|
||||
if err := new(Absolute).UnmarshalJSON([]byte(":3")); err == nil || err.Error() != wantErr {
|
||||
t.Errorf("UnmarshalJSON: error = %v, want %s", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestAbsoluteWrap(t *testing.T) {
|
||||
|
||||
@@ -4,15 +4,23 @@ import "strings"
|
||||
|
||||
const (
|
||||
// SpecialOverlayEscape is the escape string for overlay mount options.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayEscape = `\`
|
||||
// SpecialOverlayOption is the separator string between overlay mount options.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayOption = ","
|
||||
// SpecialOverlayPath is the separator string between overlay paths.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
SpecialOverlayPath = ":"
|
||||
)
|
||||
|
||||
// EscapeOverlayDataSegment escapes a string for formatting into the data
|
||||
// argument of an overlay mount system call.
|
||||
//
|
||||
// Deprecated: This is no longer used and will be removed in 0.5.
|
||||
func EscapeOverlayDataSegment(s string) string {
|
||||
if s == "" {
|
||||
return ""
|
||||
|
||||
246
cmd/dist/main.go
vendored
Normal file
246
cmd/dist/main.go
vendored
Normal file
@@ -0,0 +1,246 @@
|
||||
//go:build dist
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
_ "embed"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// getenv looks up an environment variable, and returns fallback if it is unset.
|
||||
func getenv(key, fallback string) string {
|
||||
if v, ok := os.LookupEnv(key); ok {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
|
||||
// mustRun runs a command with the current process's environment and panics
|
||||
// on error or non-zero exit code.
|
||||
func mustRun(ctx context.Context, name string, arg ...string) {
|
||||
cmd := exec.CommandContext(ctx, name, arg...)
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
if err := cmd.Run(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
//go:embed comp/_hakurei
|
||||
var comp []byte
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("")
|
||||
|
||||
verbose := os.Getenv("VERBOSE") != ""
|
||||
version := getenv("HAKUREI_VERSION", "untagged")
|
||||
prefix := getenv("PREFIX", "/usr")
|
||||
destdir := getenv("DESTDIR", "dist")
|
||||
|
||||
if verbose {
|
||||
log.Println()
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(destdir, 0755); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
s, err := os.MkdirTemp(destdir, ".dist.*")
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
var code int
|
||||
|
||||
if err = os.RemoveAll(s); err != nil {
|
||||
code = 1
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
code = 1
|
||||
log.Println(r)
|
||||
}
|
||||
|
||||
os.Exit(code)
|
||||
}()
|
||||
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), os.Interrupt)
|
||||
defer cancel()
|
||||
|
||||
verboseFlag := "-v"
|
||||
if !verbose {
|
||||
verboseFlag = "-buildvcs=false"
|
||||
}
|
||||
|
||||
log.Printf("Building hakurei for %s/%s.", runtime.GOOS, runtime.GOARCH)
|
||||
mustRun(ctx, "go", "generate", "./...")
|
||||
mustRun(
|
||||
ctx, "go", "build",
|
||||
"-trimpath",
|
||||
verboseFlag, "-o", s,
|
||||
"-ldflags=-s -w "+
|
||||
"-buildid= -linkmode external -extldflags=-static "+
|
||||
"-X hakurei.app/internal/info.buildVersion="+version+" "+
|
||||
"-X hakurei.app/internal/info.hakureiPath="+prefix+"/bin/hakurei "+
|
||||
"-X hakurei.app/internal/info.hsuPath="+prefix+"/bin/hsu "+
|
||||
"-X main.hakureiPath="+prefix+"/bin/hakurei",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
|
||||
log.Println("##### Testing Hakurei.")
|
||||
mustRun(
|
||||
ctx, "go", "test",
|
||||
"-ldflags=-buildid= -linkmode external -extldflags=-static",
|
||||
"./...",
|
||||
)
|
||||
log.Println()
|
||||
|
||||
log.Println("##### Creating distribution.")
|
||||
const suffix = ".tar.gz"
|
||||
distName := "hakurei-" + version + "-" + runtime.GOARCH
|
||||
var f *os.File
|
||||
if f, err = os.OpenFile(
|
||||
filepath.Join(s, distName+suffix),
|
||||
os.O_CREATE|os.O_EXCL|os.O_WRONLY,
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer func() {
|
||||
if f == nil {
|
||||
return
|
||||
}
|
||||
if err = f.Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}()
|
||||
|
||||
h := sha512.New()
|
||||
gw, _ := gzip.NewWriterLevel(io.MultiWriter(f, h), gzip.BestCompression)
|
||||
tw := tar.NewWriter(gw)
|
||||
|
||||
mustWriteHeader := func(name string, size int64, mode os.FileMode) {
|
||||
header := tar.Header{
|
||||
Name: filepath.Join(distName, name),
|
||||
Size: size,
|
||||
Mode: int64(mode),
|
||||
Uname: "root",
|
||||
Gname: "root",
|
||||
}
|
||||
|
||||
if mode&os.ModeDir != 0 {
|
||||
header.Typeflag = tar.TypeDir
|
||||
fmt.Printf("%s %s\n", mode, name)
|
||||
} else {
|
||||
header.Typeflag = tar.TypeReg
|
||||
fmt.Printf("%s %s (%d bytes)\n", mode, name, size)
|
||||
}
|
||||
|
||||
if err = tw.WriteHeader(&header); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile := func(name string, data []byte, mode os.FileMode) {
|
||||
mustWriteHeader(name, int64(len(data)), mode)
|
||||
if mode&os.ModeDir != 0 {
|
||||
return
|
||||
}
|
||||
if _, err = tw.Write(data); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFromPath := func(dst, src string, mode os.FileMode) {
|
||||
var r *os.File
|
||||
if r, err = os.Open(src); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
var fi os.FileInfo
|
||||
if fi, err = r.Stat(); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
}
|
||||
|
||||
if mode == 0 {
|
||||
mode = fi.Mode()
|
||||
}
|
||||
|
||||
mustWriteHeader(dst, fi.Size(), mode)
|
||||
if _, err = io.Copy(tw, r); err != nil {
|
||||
_ = r.Close()
|
||||
panic(err)
|
||||
} else if err = r.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
|
||||
mustWriteFile(".", nil, fs.ModeDir|0755)
|
||||
mustWriteFile("comp/", nil, os.ModeDir|0755)
|
||||
mustWriteFile("comp/_hakurei", comp, 0644)
|
||||
mustWriteFile("install.sh", []byte(`#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}`+prefix+`/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}`+prefix+`/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}`+prefix+`/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}`+prefix+`/share/zsh/site-functions/_hakurei"
|
||||
`), 0755)
|
||||
|
||||
mustWriteFromPath("README.md", "README.md", 0)
|
||||
mustWriteFile("hsurc.default", []byte("1000 0"), 0400)
|
||||
mustWriteFromPath("bin/hsu", filepath.Join(s, "hsu"), 04511)
|
||||
for _, name := range []string{
|
||||
"hakurei",
|
||||
"sharefs",
|
||||
} {
|
||||
mustWriteFromPath(
|
||||
filepath.Join("bin", name),
|
||||
filepath.Join(s, name),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
if err = tw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = gw.Close(); err != nil {
|
||||
panic(err)
|
||||
} else if err = f.Close(); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
f = nil
|
||||
|
||||
if err = os.WriteFile(
|
||||
filepath.Join(destdir, distName+suffix+".sha512"),
|
||||
append(hex.AppendEncode(nil, h.Sum(nil)), " "+distName+suffix+"\n"...),
|
||||
0644,
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
if err = os.Rename(
|
||||
filepath.Join(s, distName+suffix),
|
||||
filepath.Join(destdir, distName+suffix),
|
||||
); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}
|
||||
@@ -38,8 +38,9 @@ var errSuccess = errors.New("success")
|
||||
|
||||
func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErrs, out io.Writer) command.Command {
|
||||
var (
|
||||
flagVerbose bool
|
||||
flagJSON bool
|
||||
flagVerbose bool
|
||||
flagInsecure bool
|
||||
flagJSON bool
|
||||
)
|
||||
c := command.New(out, log.Printf, "hakurei", func([]string) error {
|
||||
msg.SwapVerbose(flagVerbose)
|
||||
@@ -57,6 +58,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
return nil
|
||||
}).
|
||||
Flag(&flagVerbose, "v", command.BoolFlag(false), "Increase log verbosity").
|
||||
Flag(&flagInsecure, "insecure", command.BoolFlag(false), "Allow use of insecure compatibility options").
|
||||
Flag(&flagJSON, "json", command.BoolFlag(false), "Serialise output in JSON when applicable")
|
||||
|
||||
c.Command("shim", command.UsageInternal, func([]string) error { outcome.Shim(msg); return errSuccess })
|
||||
@@ -75,7 +77,12 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
config.Container.Args = append(config.Container.Args, args[1:]...)
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flagIdentifierFile)
|
||||
var flags int
|
||||
if flagInsecure {
|
||||
flags |= hst.VAllowInsecure
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, config, flags, flagIdentifierFile)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagIdentifierFile, "identifier-fd", command.IntFlag(-1),
|
||||
@@ -145,7 +152,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if flagWayland {
|
||||
et |= hst.EWayland
|
||||
}
|
||||
@@ -163,7 +170,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
ID: flagID,
|
||||
Identity: flagIdentity,
|
||||
Groups: flagGroups,
|
||||
Enablements: hst.NewEnablements(et),
|
||||
Enablements: &et,
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -282,7 +289,7 @@ func buildCommand(ctx context.Context, msg message.Msg, early *earlyHardeningErr
|
||||
}
|
||||
}
|
||||
|
||||
outcome.Main(ctx, msg, &config, -1)
|
||||
outcome.Main(ctx, msg, &config, 0, -1)
|
||||
panic("unreachable")
|
||||
}).
|
||||
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
|
||||
|
||||
@@ -20,7 +20,7 @@ func TestHelp(t *testing.T) {
|
||||
}{
|
||||
{
|
||||
"main", []string{}, `
|
||||
Usage: hakurei [-h | --help] [-v] [--json] COMMAND [OPTIONS]
|
||||
Usage: hakurei [-h | --help] [-v] [--insecure] [--json] COMMAND [OPTIONS]
|
||||
|
||||
Commands:
|
||||
run Load and start container from configuration file
|
||||
|
||||
@@ -56,7 +56,7 @@ func printShowInstance(
|
||||
t := newPrinter(output)
|
||||
defer t.MustFlush()
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(hst.VAllowInsecure); err != nil {
|
||||
valid = false
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
mustPrint(output, "Error: "+m+"!\n\n")
|
||||
|
||||
@@ -32,7 +32,7 @@ var (
|
||||
PID: 0xbeef,
|
||||
ShimPID: 0xcafe,
|
||||
Config: &hst.Config{
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EPipeWire),
|
||||
Enablements: new(hst.EWayland | hst.EPipeWire),
|
||||
Identity: 1,
|
||||
Container: &hst.ContainerConfig{
|
||||
Shell: check.MustAbs("/bin/sh"),
|
||||
|
||||
@@ -58,7 +58,7 @@ import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -102,13 +102,13 @@ func main() {
|
||||
log.Fatal("this program must not be started by root")
|
||||
}
|
||||
|
||||
if !path.IsAbs(hakureiPath) {
|
||||
if !filepath.IsAbs(hakureiPath) {
|
||||
log.Fatal("this program is compiled incorrectly")
|
||||
return
|
||||
}
|
||||
|
||||
var toolPath string
|
||||
pexe := path.Join("/proc", strconv.Itoa(os.Getppid()), "exe")
|
||||
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)") {
|
||||
|
||||
91
cmd/mbf/cache.go
Normal file
91
cmd/mbf/cache.go
Normal file
@@ -0,0 +1,91 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
// cache refers to an instance of [pkg.Cache] that might be open.
|
||||
type cache struct {
|
||||
ctx context.Context
|
||||
msg message.Msg
|
||||
|
||||
// Should generally not be used directly.
|
||||
c *pkg.Cache
|
||||
|
||||
cures, jobs int
|
||||
hostAbstract, idle bool
|
||||
|
||||
base string
|
||||
}
|
||||
|
||||
// open opens the underlying [pkg.Cache].
|
||||
func (cache *cache) open() (err error) {
|
||||
if cache.c != nil {
|
||||
return os.ErrInvalid
|
||||
}
|
||||
|
||||
var base *check.Absolute
|
||||
if cache.base, err = filepath.Abs(cache.base); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(cache.base); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var flags int
|
||||
if cache.idle {
|
||||
flags |= pkg.CSchedIdle
|
||||
}
|
||||
if cache.hostAbstract {
|
||||
flags |= pkg.CHostAbstract
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-cache.ctx.Done():
|
||||
if testing.Testing() {
|
||||
return
|
||||
}
|
||||
os.Exit(2)
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
cache.msg.Verbosef("opening cache at %s", base)
|
||||
cache.c, err = pkg.Open(
|
||||
cache.ctx,
|
||||
cache.msg,
|
||||
flags,
|
||||
cache.cures,
|
||||
cache.jobs,
|
||||
base,
|
||||
)
|
||||
return
|
||||
}
|
||||
|
||||
// Close closes the underlying [pkg.Cache] if it is open.
|
||||
func (cache *cache) Close() {
|
||||
if cache.c != nil {
|
||||
cache.c.Close()
|
||||
}
|
||||
}
|
||||
|
||||
// Do calls f on the underlying cache and returns its error value.
|
||||
func (cache *cache) Do(f func(cache *pkg.Cache) error) error {
|
||||
if cache.c == nil {
|
||||
if err := cache.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return f(cache.c)
|
||||
}
|
||||
37
cmd/mbf/cache_test.go
Normal file
37
cmd/mbf/cache_test.go
Normal file
@@ -0,0 +1,37 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
cm := cache{
|
||||
ctx: t.Context(),
|
||||
msg: message.New(log.New(os.Stderr, "check: ", 0)),
|
||||
base: t.TempDir(),
|
||||
|
||||
hostAbstract: true, idle: true,
|
||||
}
|
||||
defer cm.Close()
|
||||
cm.Close()
|
||||
|
||||
if err := cm.open(); err != nil {
|
||||
t.Fatalf("open: error = %v", err)
|
||||
}
|
||||
if err := cm.open(); err != os.ErrInvalid {
|
||||
t.Errorf("(duplicate) open: error = %v", err)
|
||||
}
|
||||
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.Scrub(0)
|
||||
}); err != nil {
|
||||
t.Errorf("Scrub: error = %v", err)
|
||||
}
|
||||
}
|
||||
354
cmd/mbf/daemon.go
Normal file
354
cmd/mbf/daemon.go
Normal file
@@ -0,0 +1,354 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"math"
|
||||
"net"
|
||||
"os"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
"unique"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
)
|
||||
|
||||
// daemonTimeout is the maximum amount of time cureFromIR will wait on I/O.
|
||||
const daemonTimeout = 30 * time.Second
|
||||
|
||||
// daemonDeadline returns the deadline corresponding to daemonTimeout, or the
|
||||
// zero value when running in a test.
|
||||
func daemonDeadline() time.Time {
|
||||
if testing.Testing() {
|
||||
return time.Time{}
|
||||
}
|
||||
return time.Now().Add(daemonTimeout)
|
||||
}
|
||||
|
||||
const (
|
||||
// remoteNoReply notifies that the client will not receive a cure reply.
|
||||
remoteNoReply = 1 << iota
|
||||
)
|
||||
|
||||
// cureFromIR services an IR curing request.
|
||||
func cureFromIR(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
flags uint64,
|
||||
) (pkg.Artifact, error) {
|
||||
a, decodeErr := cache.NewDecoder(conn).Decode()
|
||||
if decodeErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + decodeErr.Error()))
|
||||
return nil, errors.Join(decodeErr, err, conn.Close())
|
||||
}
|
||||
|
||||
pathname, _, cureErr := cache.Cure(a)
|
||||
if flags&remoteNoReply != 0 {
|
||||
return a, errors.Join(cureErr, conn.Close())
|
||||
}
|
||||
if err := conn.SetWriteDeadline(daemonDeadline()); err != nil {
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
if cureErr != nil {
|
||||
_, err := conn.Write([]byte("\x00" + cureErr.Error()))
|
||||
return a, errors.Join(cureErr, err, conn.Close())
|
||||
}
|
||||
_, err := conn.Write([]byte(pathname.String()))
|
||||
if testing.Testing() && errors.Is(err, io.ErrClosedPipe) {
|
||||
return a, nil
|
||||
}
|
||||
return a, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
const (
|
||||
// specialCancel is a message consisting of a single identifier referring
|
||||
// to a curing artifact to be cancelled.
|
||||
specialCancel = iota
|
||||
// specialAbort requests for all pending cures to be aborted. It has no
|
||||
// message body.
|
||||
specialAbort
|
||||
|
||||
// remoteSpecial denotes a special message with custom layout.
|
||||
remoteSpecial = math.MaxUint64
|
||||
)
|
||||
|
||||
// writeSpecialHeader writes the header of a remoteSpecial message.
|
||||
func writeSpecialHeader(conn net.Conn, kind uint64) error {
|
||||
var sh [16]byte
|
||||
binary.LittleEndian.PutUint64(sh[:], remoteSpecial)
|
||||
binary.LittleEndian.PutUint64(sh[8:], kind)
|
||||
if n, err := conn.Write(sh[:]); err != nil {
|
||||
return err
|
||||
} else if n != len(sh) {
|
||||
return io.ErrShortWrite
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// cancelIdent reads an identifier from conn and cancels the corresponding cure.
|
||||
func cancelIdent(
|
||||
cache *pkg.Cache,
|
||||
conn net.Conn,
|
||||
) (*pkg.ID, bool, error) {
|
||||
var ident pkg.ID
|
||||
if _, err := io.ReadFull(conn, ident[:]); err != nil {
|
||||
return nil, false, errors.Join(err, conn.Close())
|
||||
}
|
||||
ok := cache.Cancel(unique.Make(ident))
|
||||
return &ident, ok, conn.Close()
|
||||
}
|
||||
|
||||
// serve services connections from a [net.UnixListener].
|
||||
func serve(
|
||||
ctx context.Context,
|
||||
log *log.Logger,
|
||||
cm *cache,
|
||||
ul *net.UnixListener,
|
||||
) error {
|
||||
ul.SetUnlinkOnClose(true)
|
||||
if cm.c == nil {
|
||||
if err := cm.open(); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
}
|
||||
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
wg.Go(func() {
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
break
|
||||
}
|
||||
|
||||
conn, err := ul.AcceptUnix()
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
log.Println(err)
|
||||
}
|
||||
continue
|
||||
}
|
||||
wg.Go(func() {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := conn.SetReadDeadline(daemonDeadline()); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
var word [8]byte
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
flags := binary.LittleEndian.Uint64(word[:])
|
||||
|
||||
if flags == remoteSpecial {
|
||||
if _, _err := io.ReadFull(conn, word[:]); _err != nil {
|
||||
log.Println(_err)
|
||||
if _err = conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
return
|
||||
}
|
||||
switch special := binary.LittleEndian.Uint64(word[:]); special {
|
||||
default:
|
||||
log.Printf("invalid special %d", special)
|
||||
|
||||
case specialCancel:
|
||||
if id, ok, _err := cancelIdent(cm.c, conn); _err != nil {
|
||||
log.Println(_err)
|
||||
} else if !ok {
|
||||
log.Println(
|
||||
"attempting to cancel invalid artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
} else {
|
||||
log.Println(
|
||||
"cancelled artifact",
|
||||
pkg.Encode(*id),
|
||||
)
|
||||
}
|
||||
|
||||
case specialAbort:
|
||||
log.Println("aborting all pending cures")
|
||||
cm.c.Abort()
|
||||
if _err := conn.Close(); _err != nil {
|
||||
log.Println(_err)
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if a, _err := cureFromIR(cm.c, conn, flags); _err != nil {
|
||||
log.Println(_err)
|
||||
} else {
|
||||
log.Printf(
|
||||
"fulfilled artifact %s",
|
||||
pkg.Encode(cm.c.Ident(a).Value()),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
<-ctx.Done()
|
||||
if err := ul.SetDeadline(time.Now()); err != nil {
|
||||
return errors.Join(err, ul.Close())
|
||||
}
|
||||
wg.Wait()
|
||||
return ul.Close()
|
||||
}
|
||||
|
||||
// dial wraps [net.DialUnix] with a context.
|
||||
func dial(ctx context.Context, addr *net.UnixAddr) (
|
||||
done chan<- struct{},
|
||||
conn *net.UnixConn,
|
||||
err error,
|
||||
) {
|
||||
conn, err = net.DialUnix("unix", nil, addr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
d := make(chan struct{})
|
||||
done = d
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
_ = conn.SetDeadline(time.Now())
|
||||
|
||||
case <-d:
|
||||
return
|
||||
}
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
// cureRemote cures a [pkg.Artifact] on a daemon.
|
||||
func cureRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
flags uint64,
|
||||
) (*check.Absolute, error) {
|
||||
if flags == remoteSpecial {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if n, flagErr := conn.Write(binary.LittleEndian.AppendUint64(nil, flags)); flagErr != nil {
|
||||
return nil, errors.Join(flagErr, conn.Close())
|
||||
} else if n != 8 {
|
||||
return nil, errors.Join(io.ErrShortWrite, conn.Close())
|
||||
}
|
||||
|
||||
if err = pkg.NewIR().EncodeAll(conn, a); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
} else if err = conn.CloseWrite(); err != nil {
|
||||
return nil, errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
if flags&remoteNoReply != 0 {
|
||||
return nil, conn.Close()
|
||||
}
|
||||
|
||||
payload, recvErr := io.ReadAll(conn)
|
||||
if err = errors.Join(recvErr, conn.Close()); err != nil {
|
||||
if errors.Is(err, os.ErrDeadlineExceeded) {
|
||||
if cancelErr := ctx.Err(); cancelErr != nil {
|
||||
err = cancelErr
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if len(payload) > 0 && payload[0] == 0 {
|
||||
return nil, errors.New(string(payload[1:]))
|
||||
}
|
||||
|
||||
var p *check.Absolute
|
||||
p, err = check.NewAbs(string(payload))
|
||||
return p, err
|
||||
}
|
||||
|
||||
// cancelRemote cancels a [pkg.Artifact] curing on a daemon.
|
||||
func cancelRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
a pkg.Artifact,
|
||||
wait bool,
|
||||
) error {
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
if err = writeSpecialHeader(conn, specialCancel); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
var n int
|
||||
id := pkg.NewIR().Ident(a).Value()
|
||||
if n, err = conn.Write(id[:]); err != nil {
|
||||
return errors.Join(err, conn.Close())
|
||||
} else if n != len(id) {
|
||||
return errors.Join(io.ErrShortWrite, conn.Close())
|
||||
}
|
||||
if wait {
|
||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
|
||||
// abortRemote aborts all [pkg.Artifact] curing on a daemon.
|
||||
func abortRemote(
|
||||
ctx context.Context,
|
||||
addr *net.UnixAddr,
|
||||
wait bool,
|
||||
) error {
|
||||
done, conn, err := dial(ctx, addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer close(done)
|
||||
|
||||
err = writeSpecialHeader(conn, specialAbort)
|
||||
if wait && err == nil {
|
||||
if _, err = conn.Read(make([]byte, 1)); err == io.EOF {
|
||||
err = nil
|
||||
}
|
||||
}
|
||||
return errors.Join(err, conn.Close())
|
||||
}
|
||||
146
cmd/mbf/daemon_test.go
Normal file
146
cmd/mbf/daemon_test.go
Normal file
@@ -0,0 +1,146 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestNoReply(t *testing.T) {
|
||||
t.Parallel()
|
||||
if !daemonDeadline().IsZero() {
|
||||
t.Fatal("daemonDeadline did not return the zero value")
|
||||
}
|
||||
|
||||
c, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(log.New(os.Stderr, "cir: ", 0)),
|
||||
0, 0, 0,
|
||||
check.MustAbs(t.TempDir()),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
defer c.Close()
|
||||
|
||||
client, server := net.Pipe()
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
go func() {
|
||||
<-t.Context().Done()
|
||||
if _err := client.SetDeadline(time.Now()); _err != nil && !errors.Is(_err, io.ErrClosedPipe) {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if _err := c.EncodeAll(
|
||||
client,
|
||||
pkg.NewFile("check", []byte{0}),
|
||||
); _err != nil {
|
||||
panic(_err)
|
||||
} else if _err = client.Close(); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
a, cureErr := cureFromIR(c, server, remoteNoReply)
|
||||
if cureErr != nil {
|
||||
t.Fatalf("cureFromIR: error = %v", cureErr)
|
||||
}
|
||||
|
||||
<-done
|
||||
wantIdent := pkg.MustDecode("fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG")
|
||||
if gotIdent := c.Ident(a).Value(); gotIdent != wantIdent {
|
||||
t.Errorf(
|
||||
"cureFromIR: %s, want %s",
|
||||
pkg.Encode(gotIdent), pkg.Encode(wantIdent),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDaemon(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var buf bytes.Buffer
|
||||
logger := log.New(&buf, "daemon: ", 0)
|
||||
|
||||
addr := net.UnixAddr{
|
||||
Name: filepath.Join(t.TempDir(), "daemon"),
|
||||
Net: "unix",
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
defer cancel()
|
||||
|
||||
cm := cache{
|
||||
ctx: ctx,
|
||||
msg: message.New(logger),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
|
||||
ul, err := net.ListenUnix("unix", &addr)
|
||||
if err != nil {
|
||||
t.Fatalf("ListenUnix: error = %v", err)
|
||||
}
|
||||
|
||||
done := make(chan struct{})
|
||||
go func() {
|
||||
defer close(done)
|
||||
if _err := serve(ctx, logger, &cm, ul); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err = cancelRemote(ctx, &addr, pkg.NewFile("nonexistent", nil), true); err != nil {
|
||||
t.Fatalf("cancelRemote: error = %v", err)
|
||||
}
|
||||
|
||||
if err = abortRemote(ctx, &addr, true); err != nil {
|
||||
t.Fatalf("abortRemote: error = %v", err)
|
||||
}
|
||||
|
||||
// keep this last for synchronisation
|
||||
var p *check.Absolute
|
||||
p, err = cureRemote(ctx, &addr, pkg.NewFile("check", []byte{0}), 0)
|
||||
if err != nil {
|
||||
t.Fatalf("cureRemote: error = %v", err)
|
||||
}
|
||||
|
||||
cancel()
|
||||
<-done
|
||||
|
||||
const want = "fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG"
|
||||
if got := filepath.Base(p.String()); got != want {
|
||||
t.Errorf("cureRemote: %s, want %s", got, want)
|
||||
}
|
||||
|
||||
wantLog := []string{
|
||||
"",
|
||||
"daemon: aborting all pending cures",
|
||||
"daemon: attempting to cancel invalid artifact kQm9fmnCmXST1-MMmxzcau2oKZCXXrlZydo4PkeV5hO_2PKfeC8t98hrbV_ZZx_j",
|
||||
"daemon: fulfilled artifact fiZf-ZY_Yq6qxJNrHbMiIPYCsGkUiKCRsZrcSELXTqZWtCnESlHmzV5ThhWWGGYG",
|
||||
}
|
||||
gotLog := strings.Split(buf.String(), "\n")
|
||||
slices.Sort(gotLog)
|
||||
if !slices.Equal(gotLog, wantLog) {
|
||||
t.Errorf(
|
||||
"serve: logged\n%s\nwant\n%s",
|
||||
strings.Join(gotLog, "\n"), strings.Join(wantLog, "\n"),
|
||||
)
|
||||
}
|
||||
}
|
||||
114
cmd/mbf/info.go
Normal file
114
cmd/mbf/info.go
Normal file
@@ -0,0 +1,114 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// commandInfo implements the info subcommand.
|
||||
func commandInfo(
|
||||
cm *cache,
|
||||
args []string,
|
||||
w io.Writer,
|
||||
writeStatus bool,
|
||||
r *rosa.Report,
|
||||
) (err error) {
|
||||
if len(args) == 0 {
|
||||
return errors.New("info requires at least 1 argument")
|
||||
}
|
||||
|
||||
// recovered by HandleAccess
|
||||
mustPrintln := func(a ...any) {
|
||||
if _, _err := fmt.Fprintln(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
mustPrint := func(a ...any) {
|
||||
if _, _err := fmt.Fprint(w, a...); _err != nil {
|
||||
panic(_err)
|
||||
}
|
||||
}
|
||||
|
||||
for i, name := range args {
|
||||
if p, ok := rosa.ResolveName(name); !ok {
|
||||
return fmt.Errorf("unknown artifact %q", name)
|
||||
} else {
|
||||
var suffix string
|
||||
if version := rosa.Std.Version(p); version != rosa.Unversioned {
|
||||
suffix += "-" + version
|
||||
}
|
||||
mustPrintln("name : " + name + suffix)
|
||||
|
||||
meta := rosa.GetMetadata(p)
|
||||
mustPrintln("description : " + meta.Description)
|
||||
if meta.Website != "" {
|
||||
mustPrintln("website : " +
|
||||
strings.TrimSuffix(meta.Website, "/"))
|
||||
}
|
||||
if len(meta.Dependencies) > 0 {
|
||||
mustPrint("depends on :")
|
||||
for _, d := range meta.Dependencies {
|
||||
s := rosa.GetMetadata(d).Name
|
||||
if version := rosa.Std.Version(d); version != rosa.Unversioned {
|
||||
s += "-" + version
|
||||
}
|
||||
mustPrint(" " + s)
|
||||
}
|
||||
mustPrintln()
|
||||
}
|
||||
|
||||
const statusPrefix = "status : "
|
||||
if writeStatus {
|
||||
if r == nil {
|
||||
var f io.ReadSeekCloser
|
||||
err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
f, err = cache.OpenStatus(rosa.Std.Load(p))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
mustPrintln(
|
||||
statusPrefix + "not yet cured",
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
mustPrint(statusPrefix)
|
||||
_, err = io.Copy(w, f)
|
||||
if err = errors.Join(err, f.Close()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
|
||||
if status == nil {
|
||||
mustPrintln(
|
||||
statusPrefix + "not in report",
|
||||
)
|
||||
} else {
|
||||
mustPrintln("size :", n)
|
||||
mustPrint(statusPrefix)
|
||||
if _, err = w.Write(status); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if i != len(args)-1 {
|
||||
mustPrintln()
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
181
cmd/mbf/info_test.go
Normal file
181
cmd/mbf/info_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
func TestInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
args []string
|
||||
status map[string]string
|
||||
report string
|
||||
want string
|
||||
wantErr any
|
||||
}{
|
||||
{"qemu", []string{"qemu"}, nil, "", `
|
||||
name : qemu-` + rosa.Std.Version(rosa.QEMU) + `
|
||||
description : a generic and open source machine emulator and virtualizer
|
||||
website : https://www.qemu.org
|
||||
depends on : glib-` + rosa.Std.Version(rosa.GLib) + ` zstd-` + rosa.Std.Version(rosa.Zstd) + `
|
||||
`, nil},
|
||||
|
||||
{"multi", []string{"hakurei", "hakurei-dist"}, nil, "", `
|
||||
name : hakurei-` + rosa.Std.Version(rosa.Hakurei) + `
|
||||
description : low-level userspace tooling for Rosa OS
|
||||
website : https://hakurei.app
|
||||
|
||||
name : hakurei-dist-` + rosa.Std.Version(rosa.HakureiDist) + `
|
||||
description : low-level userspace tooling for Rosa OS (distribution tarball)
|
||||
website : https://hakurei.app
|
||||
`, nil},
|
||||
|
||||
{"nonexistent", []string{"zlib", "\x00"}, nil, "", `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
|
||||
`, fmt.Errorf("unknown artifact %q", "\x00")},
|
||||
|
||||
{"status cache", []string{"zlib", "zstd"}, map[string]string{
|
||||
"zstd": "internal/pkg (amd64) on satori\n",
|
||||
"hakurei": "internal/pkg (amd64) on satori\n\n",
|
||||
}, "", `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not yet cured
|
||||
|
||||
name : zstd-` + rosa.Std.Version(rosa.Zstd) + `
|
||||
description : a fast compression algorithm
|
||||
website : https://facebook.github.io/zstd
|
||||
status : internal/pkg (amd64) on satori
|
||||
`, nil},
|
||||
|
||||
{"status cache perm", []string{"zlib"}, map[string]string{
|
||||
"zlib": "\x00",
|
||||
}, "", `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
`, func(cm *cache) error {
|
||||
return &os.PathError{
|
||||
Op: "open",
|
||||
Path: filepath.Join(cm.base, "status", pkg.Encode(cm.c.Ident(rosa.Std.Load(rosa.Zlib)).Value())),
|
||||
Err: syscall.EACCES,
|
||||
}
|
||||
}},
|
||||
|
||||
{"status report", []string{"zlib"}, nil, strings.Repeat("\x00", len(pkg.Checksum{})+8), `
|
||||
name : zlib-` + rosa.Std.Version(rosa.Zlib) + `
|
||||
description : lossless data-compression library
|
||||
website : https://zlib.net
|
||||
status : not in report
|
||||
`, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var (
|
||||
cm *cache
|
||||
buf strings.Builder
|
||||
r *rosa.Report
|
||||
)
|
||||
|
||||
if tc.status != nil || tc.report != "" {
|
||||
cm = &cache{
|
||||
ctx: context.Background(),
|
||||
msg: message.New(log.New(os.Stderr, "info: ", 0)),
|
||||
base: t.TempDir(),
|
||||
}
|
||||
defer cm.Close()
|
||||
}
|
||||
|
||||
if tc.report != "" {
|
||||
pathname := filepath.Join(t.TempDir(), "report")
|
||||
err := os.WriteFile(
|
||||
pathname,
|
||||
unsafe.Slice(unsafe.StringData(tc.report), len(tc.report)),
|
||||
0400,
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
r, err = rosa.OpenReport(pathname)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
if err = r.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if tc.status != nil {
|
||||
for name, status := range tc.status {
|
||||
p, ok := rosa.ResolveName(name)
|
||||
if !ok {
|
||||
t.Fatalf("invalid name %q", name)
|
||||
}
|
||||
perm := os.FileMode(0400)
|
||||
if status == "\x00" {
|
||||
perm = 0
|
||||
}
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
return os.WriteFile(filepath.Join(
|
||||
cm.base,
|
||||
"status",
|
||||
pkg.Encode(cache.Ident(rosa.Std.Load(p)).Value()),
|
||||
), unsafe.Slice(unsafe.StringData(status), len(status)), perm)
|
||||
}); err != nil {
|
||||
t.Fatalf("Do: error = %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var wantErr error
|
||||
switch c := tc.wantErr.(type) {
|
||||
case error:
|
||||
wantErr = c
|
||||
case func(cm *cache) error:
|
||||
wantErr = c(cm)
|
||||
default:
|
||||
if tc.wantErr != nil {
|
||||
t.Fatalf("invalid wantErr %#v", tc.wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
if err := commandInfo(
|
||||
cm,
|
||||
tc.args,
|
||||
&buf,
|
||||
cm != nil,
|
||||
r,
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("commandInfo: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
|
||||
if got := buf.String(); got != strings.TrimPrefix(tc.want, "\n") {
|
||||
t.Errorf("commandInfo:\n%s\nwant\n%s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
202
cmd/mbf/internal/pkgserver/api.go
Normal file
202
cmd/mbf/internal/pkgserver/api.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Package pkgserver implements the package metadata service backend.
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"log"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"path"
|
||||
"strconv"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// for lazy initialisation of serveInfo
|
||||
var (
|
||||
infoPayload struct {
|
||||
// Current package count.
|
||||
Count int `json:"count"`
|
||||
// Hakurei version, set at link time.
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}
|
||||
infoPayloadOnce sync.Once
|
||||
)
|
||||
|
||||
// handleInfo writes constant system information.
|
||||
func handleInfo(w http.ResponseWriter, _ *http.Request) {
|
||||
infoPayloadOnce.Do(func() {
|
||||
infoPayload.Count = int(rosa.PresetUnexportedStart)
|
||||
infoPayload.HakureiVersion = info.Version()
|
||||
})
|
||||
// TODO(mae): cache entire response if no additional fields are planned
|
||||
writeAPIPayload(w, infoPayload)
|
||||
}
|
||||
|
||||
// newStatusHandler returns a [http.HandlerFunc] that offers status files for
|
||||
// viewing or download, if available.
|
||||
func (index *packageIndex) newStatusHandler(disposition bool) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
m, ok := index.names[path.Base(r.URL.Path)]
|
||||
if !ok || !m.HasReport {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
contentType := "text/plain; charset=utf-8"
|
||||
if disposition {
|
||||
contentType = "application/octet-stream"
|
||||
|
||||
// quoting like this is unsound, but okay, because metadata is hardcoded
|
||||
contentDisposition := `attachment; filename="`
|
||||
contentDisposition += m.Name + "-"
|
||||
if m.Version != "" {
|
||||
contentDisposition += m.Version + "-"
|
||||
}
|
||||
contentDisposition += m.ids + `.log"`
|
||||
w.Header().Set("Content-Disposition", contentDisposition)
|
||||
}
|
||||
w.Header().Set("Content-Type", contentType)
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
if err := func() (err error) {
|
||||
defer index.handleAccess(&err)()
|
||||
_, err = w.Write(m.status)
|
||||
return
|
||||
}(); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot deliver status, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// handleGet writes a slice of metadata with specified order.
|
||||
func (index *packageIndex) handleGet(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(
|
||||
w, "limit must be an integer between 1 and 100",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(
|
||||
w, "index must be an integer between 0 and "+
|
||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
sort, err := strconv.Atoi(q.Get("sort"))
|
||||
if err != nil || sort >= len(index.sorts) || sort < 0 {
|
||||
http.Error(
|
||||
w, "sort must be an integer between 0 and "+
|
||||
strconv.Itoa(sortOrderEnd),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
values := index.sorts[sort][i:min(i+limit, len(index.sorts[sort]))]
|
||||
writeAPIPayload(w, &struct {
|
||||
Values []*metadata `json:"values"`
|
||||
}{values})
|
||||
}
|
||||
|
||||
func (index *packageIndex) handleSearch(w http.ResponseWriter, r *http.Request) {
|
||||
q := r.URL.Query()
|
||||
limit, err := strconv.Atoi(q.Get("limit"))
|
||||
if err != nil || limit > 100 || limit < 1 {
|
||||
http.Error(
|
||||
w, "limit must be an integer between 1 and 100",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
i, err := strconv.Atoi(q.Get("index"))
|
||||
if err != nil || i >= len(index.sorts[0]) || i < 0 {
|
||||
http.Error(
|
||||
w, "index must be an integer between 0 and "+
|
||||
strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
search, err := url.QueryUnescape(q.Get("search"))
|
||||
if len(search) > 100 || err != nil {
|
||||
http.Error(
|
||||
w, "search must be a string between 0 and 100 characters long",
|
||||
http.StatusBadRequest,
|
||||
)
|
||||
return
|
||||
}
|
||||
desc := q.Get("desc") == "true"
|
||||
n, res, err := index.performSearchQuery(limit, i, search, desc)
|
||||
if err != nil {
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
}
|
||||
writeAPIPayload(w, &struct {
|
||||
Count int `json:"count"`
|
||||
Values []searchResult `json:"values"`
|
||||
}{n, res})
|
||||
}
|
||||
|
||||
// apiVersion is the name of the current API revision, as part of the pattern.
|
||||
const apiVersion = "v1"
|
||||
|
||||
// registerAPI registers API handler functions.
|
||||
func (index *packageIndex) registerAPI(mux *http.ServeMux) {
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/info", handleInfo)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/get", index.handleGet)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/search", index.handleSearch)
|
||||
mux.HandleFunc("GET /api/"+apiVersion+"/status/", index.newStatusHandler(false))
|
||||
mux.HandleFunc("GET /status/", index.newStatusHandler(true))
|
||||
}
|
||||
|
||||
// Register arranges for mux to service API requests.
|
||||
func Register(ctx context.Context, mux *http.ServeMux, report *rosa.Report) error {
|
||||
var index packageIndex
|
||||
index.search = make(searchCache)
|
||||
if err := index.populate(report); err != nil {
|
||||
return err
|
||||
}
|
||||
ticker := time.NewTicker(1 * time.Minute)
|
||||
go func() {
|
||||
for {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
ticker.Stop()
|
||||
return
|
||||
case <-ticker.C:
|
||||
index.search.clean()
|
||||
}
|
||||
}
|
||||
}()
|
||||
index.registerAPI(mux)
|
||||
return nil
|
||||
}
|
||||
|
||||
// writeAPIPayload sets headers common to API responses and encodes payload as
|
||||
// JSON for the response body.
|
||||
func writeAPIPayload(w http.ResponseWriter, payload any) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
w.Header().Set("Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
w.Header().Set("Pragma", "no-cache")
|
||||
w.Header().Set("Expires", "0")
|
||||
|
||||
if err := json.NewEncoder(w).Encode(payload); err != nil {
|
||||
log.Println(err)
|
||||
http.Error(
|
||||
w, "cannot encode payload, contact maintainers",
|
||||
http.StatusInternalServerError,
|
||||
)
|
||||
}
|
||||
}
|
||||
181
cmd/mbf/internal/pkgserver/api_test.go
Normal file
181
cmd/mbf/internal/pkgserver/api_test.go
Normal file
@@ -0,0 +1,181 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"slices"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
// prefix is prepended to every API path.
|
||||
const prefix = "/api/" + apiVersion + "/"
|
||||
|
||||
func TestAPIInfo(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := httptest.NewRecorder()
|
||||
handleInfo(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
prefix+"info",
|
||||
nil,
|
||||
))
|
||||
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
|
||||
checkPayload(t, resp, struct {
|
||||
Count int `json:"count"`
|
||||
HakureiVersion string `json:"hakurei_version"`
|
||||
}{int(rosa.PresetUnexportedStart), info.Version()})
|
||||
}
|
||||
|
||||
func TestAPIGet(t *testing.T) {
|
||||
t.Parallel()
|
||||
const target = prefix + "get"
|
||||
|
||||
index := newIndex(t)
|
||||
newRequest := func(suffix string) *httptest.ResponseRecorder {
|
||||
w := httptest.NewRecorder()
|
||||
index.handleGet(w, httptest.NewRequestWithContext(
|
||||
t.Context(),
|
||||
http.MethodGet,
|
||||
target+suffix,
|
||||
nil,
|
||||
))
|
||||
return w
|
||||
}
|
||||
|
||||
checkValidate := func(t *testing.T, suffix string, vmin, vmax int, wantErr string) {
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=invalid")
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
})
|
||||
|
||||
t.Run("min", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmin-1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmin))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
|
||||
t.Run("max", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest("?" + suffix + "=" + strconv.Itoa(vmax+1))
|
||||
resp := w.Result()
|
||||
checkError(t, resp, wantErr, http.StatusBadRequest)
|
||||
|
||||
w = newRequest("?" + suffix + "=" + strconv.Itoa(vmax))
|
||||
resp = w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
})
|
||||
}
|
||||
|
||||
t.Run("limit", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&sort=0&limit", 1, 100,
|
||||
"limit must be an integer between 1 and 100",
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("index", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "limit=1&sort=0&index", 0, int(rosa.PresetUnexportedStart-1),
|
||||
"index must be an integer between 0 and "+strconv.Itoa(int(rosa.PresetUnexportedStart-1)),
|
||||
)
|
||||
})
|
||||
|
||||
t.Run("sort", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
checkValidate(
|
||||
t, "index=0&limit=1&sort", 0, int(sortOrderEnd),
|
||||
"sort must be an integer between 0 and "+strconv.Itoa(int(sortOrderEnd)),
|
||||
)
|
||||
})
|
||||
|
||||
checkWithSuffix := func(name, suffix string, want []*metadata) {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
w := newRequest(suffix)
|
||||
resp := w.Result()
|
||||
checkStatus(t, resp, http.StatusOK)
|
||||
checkAPIHeader(t, w.Header())
|
||||
checkPayloadFunc(t, resp, func(got *struct {
|
||||
Values []*metadata `json:"values"`
|
||||
}) bool {
|
||||
return slices.EqualFunc(got.Values, want, func(a, b *metadata) bool {
|
||||
return (a.Version == b.Version ||
|
||||
a.Version == rosa.Unversioned ||
|
||||
b.Version == rosa.Unversioned) &&
|
||||
a.HasReport == b.HasReport &&
|
||||
a.Name == b.Name &&
|
||||
a.Description == b.Description &&
|
||||
a.Website == b.Website
|
||||
})
|
||||
})
|
||||
|
||||
})
|
||||
}
|
||||
|
||||
checkWithSuffix("declarationAscending", "?limit=2&index=1&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(1),
|
||||
Version: rosa.Std.Version(1),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(2),
|
||||
Version: rosa.Std.Version(2),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationAscending offset", "?limit=3&index=5&sort=0", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(5),
|
||||
Version: rosa.Std.Version(5),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(6),
|
||||
Version: rosa.Std.Version(6),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(7),
|
||||
Version: rosa.Std.Version(7),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending", "?limit=3&index=0&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 1),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 1),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 2),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 2),
|
||||
},
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 3),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 3),
|
||||
},
|
||||
})
|
||||
checkWithSuffix("declarationDescending offset", "?limit=1&index=37&sort=1", []*metadata{
|
||||
{
|
||||
Metadata: rosa.GetMetadata(rosa.PresetUnexportedStart - 38),
|
||||
Version: rosa.Std.Version(rosa.PresetUnexportedStart - 38),
|
||||
},
|
||||
})
|
||||
}
|
||||
106
cmd/mbf/internal/pkgserver/index.go
Normal file
106
cmd/mbf/internal/pkgserver/index.go
Normal file
@@ -0,0 +1,106 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"errors"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
const (
|
||||
declarationAscending = iota
|
||||
declarationDescending
|
||||
nameAscending
|
||||
nameDescending
|
||||
sizeAscending
|
||||
sizeDescending
|
||||
|
||||
sortOrderEnd = iota - 1
|
||||
)
|
||||
|
||||
// packageIndex refers to metadata by name and various sort orders.
|
||||
type packageIndex struct {
|
||||
sorts [sortOrderEnd + 1][rosa.PresetUnexportedStart]*metadata
|
||||
names map[string]*metadata
|
||||
search searchCache
|
||||
// Taken from [rosa.Report] if available.
|
||||
handleAccess func(*error) func()
|
||||
}
|
||||
|
||||
// metadata holds [rosa.Metadata] extended with additional information.
|
||||
type metadata struct {
|
||||
p rosa.PArtifact
|
||||
*rosa.Metadata
|
||||
|
||||
// Populated via [rosa.Toolchain.Version], [rosa.Unversioned] is equivalent
|
||||
// to the zero value. Otherwise, the zero value is invalid.
|
||||
Version string `json:"version,omitempty"`
|
||||
// Output data size, available if present in report.
|
||||
Size int64 `json:"size,omitempty"`
|
||||
// Whether the underlying [pkg.Artifact] is present in the report.
|
||||
HasReport bool `json:"report"`
|
||||
|
||||
// Ident string encoded ahead of time.
|
||||
ids string
|
||||
// Backed by [rosa.Report], access must be prepared by HandleAccess.
|
||||
status []byte
|
||||
}
|
||||
|
||||
// populate deterministically populates packageIndex, optionally with a report.
|
||||
func (index *packageIndex) populate(report *rosa.Report) (err error) {
|
||||
if report != nil {
|
||||
defer report.HandleAccess(&err)()
|
||||
index.handleAccess = report.HandleAccess
|
||||
}
|
||||
|
||||
var work [rosa.PresetUnexportedStart]*metadata
|
||||
index.names = make(map[string]*metadata)
|
||||
ir := pkg.NewIR()
|
||||
for p := range rosa.PresetUnexportedStart {
|
||||
m := metadata{
|
||||
p: p,
|
||||
|
||||
Metadata: rosa.GetMetadata(p),
|
||||
Version: rosa.Std.Version(p),
|
||||
}
|
||||
if m.Version == "" {
|
||||
return errors.New("invalid version from " + m.Name)
|
||||
}
|
||||
if m.Version == rosa.Unversioned {
|
||||
m.Version = ""
|
||||
}
|
||||
|
||||
if report != nil {
|
||||
id := ir.Ident(rosa.Std.Load(p))
|
||||
m.ids = pkg.Encode(id.Value())
|
||||
m.status, m.Size = report.ArtifactOf(id)
|
||||
m.HasReport = m.Size >= 0
|
||||
}
|
||||
|
||||
work[p] = &m
|
||||
index.names[m.Name] = &m
|
||||
}
|
||||
|
||||
index.sorts[declarationAscending] = work
|
||||
index.sorts[declarationDescending] = work
|
||||
slices.Reverse(index.sorts[declarationDescending][:])
|
||||
|
||||
index.sorts[nameAscending] = work
|
||||
slices.SortFunc(index.sorts[nameAscending][:], func(a, b *metadata) int {
|
||||
return strings.Compare(a.Name, b.Name)
|
||||
})
|
||||
index.sorts[nameDescending] = index.sorts[nameAscending]
|
||||
slices.Reverse(index.sorts[nameDescending][:])
|
||||
|
||||
index.sorts[sizeAscending] = work
|
||||
slices.SortFunc(index.sorts[sizeAscending][:], func(a, b *metadata) int {
|
||||
return cmp.Compare(a.Size, b.Size)
|
||||
})
|
||||
index.sorts[sizeDescending] = index.sorts[sizeAscending]
|
||||
slices.Reverse(index.sorts[sizeDescending][:])
|
||||
|
||||
return
|
||||
}
|
||||
96
cmd/mbf/internal/pkgserver/index_test.go
Normal file
96
cmd/mbf/internal/pkgserver/index_test.go
Normal file
@@ -0,0 +1,96 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// newIndex returns the address of a newly populated packageIndex.
|
||||
func newIndex(t *testing.T) *packageIndex {
|
||||
t.Helper()
|
||||
|
||||
var index packageIndex
|
||||
if err := index.populate(nil); err != nil {
|
||||
t.Fatalf("populate: error = %v", err)
|
||||
}
|
||||
return &index
|
||||
}
|
||||
|
||||
// checkStatus checks response status code.
|
||||
func checkStatus(t *testing.T, resp *http.Response, want int) {
|
||||
t.Helper()
|
||||
|
||||
if resp.StatusCode != want {
|
||||
t.Errorf(
|
||||
"StatusCode: %s, want %s",
|
||||
http.StatusText(resp.StatusCode),
|
||||
http.StatusText(want),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// checkHeader checks the value of a header entry.
|
||||
func checkHeader(t *testing.T, h http.Header, key, want string) {
|
||||
t.Helper()
|
||||
|
||||
if got := h.Get(key); got != want {
|
||||
t.Errorf("%s: %q, want %q", key, got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// checkAPIHeader checks common entries set for API endpoints.
|
||||
func checkAPIHeader(t *testing.T, h http.Header) {
|
||||
t.Helper()
|
||||
|
||||
checkHeader(t, h, "Content-Type", "application/json; charset=utf-8")
|
||||
checkHeader(t, h, "Cache-Control", "no-cache, no-store, must-revalidate")
|
||||
checkHeader(t, h, "Pragma", "no-cache")
|
||||
checkHeader(t, h, "Expires", "0")
|
||||
}
|
||||
|
||||
// checkPayloadFunc checks the JSON response of an API endpoint by passing it to f.
|
||||
func checkPayloadFunc[T any](
|
||||
t *testing.T,
|
||||
resp *http.Response,
|
||||
f func(got *T) bool,
|
||||
) {
|
||||
t.Helper()
|
||||
|
||||
var got T
|
||||
r := io.Reader(resp.Body)
|
||||
if testing.Verbose() {
|
||||
var buf bytes.Buffer
|
||||
r = io.TeeReader(r, &buf)
|
||||
defer func() { t.Helper(); t.Log(buf.String()) }()
|
||||
}
|
||||
if err := json.NewDecoder(r).Decode(&got); err != nil {
|
||||
t.Fatalf("Decode: error = %v", err)
|
||||
}
|
||||
|
||||
if !f(&got) {
|
||||
t.Errorf("Body: %#v", got)
|
||||
}
|
||||
}
|
||||
|
||||
// checkPayload checks the JSON response of an API endpoint.
|
||||
func checkPayload[T any](t *testing.T, resp *http.Response, want T) {
|
||||
t.Helper()
|
||||
|
||||
checkPayloadFunc(t, resp, func(got *T) bool {
|
||||
return reflect.DeepEqual(got, &want)
|
||||
})
|
||||
}
|
||||
|
||||
func checkError(t *testing.T, resp *http.Response, error string, code int) {
|
||||
t.Helper()
|
||||
|
||||
checkStatus(t, resp, code)
|
||||
if got, _ := io.ReadAll(resp.Body); string(got) != fmt.Sprintln(error) {
|
||||
t.Errorf("Body: %q, want %q", string(got), error)
|
||||
}
|
||||
}
|
||||
81
cmd/mbf/internal/pkgserver/search.go
Normal file
81
cmd/mbf/internal/pkgserver/search.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package pkgserver
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"maps"
|
||||
"regexp"
|
||||
"slices"
|
||||
"time"
|
||||
)
|
||||
|
||||
type searchCache map[string]searchCacheEntry
|
||||
type searchResult struct {
|
||||
NameIndices [][]int `json:"name_matches"`
|
||||
DescIndices [][]int `json:"desc_matches,omitempty"`
|
||||
Score float64 `json:"score"`
|
||||
*metadata
|
||||
}
|
||||
type searchCacheEntry struct {
|
||||
query string
|
||||
results []searchResult
|
||||
expiry time.Time
|
||||
}
|
||||
|
||||
func (index *packageIndex) performSearchQuery(limit int, i int, search string, desc bool) (int, []searchResult, error) {
|
||||
query := search
|
||||
if desc {
|
||||
query += ";withDesc"
|
||||
}
|
||||
entry, ok := index.search[query]
|
||||
if ok && len(entry.results) > 0 {
|
||||
return len(entry.results), entry.results[min(i, len(entry.results)-1):min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
|
||||
regex, err := regexp.Compile(search)
|
||||
if err != nil {
|
||||
return 0, make([]searchResult, 0), err
|
||||
}
|
||||
res := make([]searchResult, 0)
|
||||
for p := range maps.Values(index.names) {
|
||||
nameIndices := regex.FindAllIndex([]byte(p.Name), -1)
|
||||
var descIndices [][]int = nil
|
||||
if desc {
|
||||
descIndices = regex.FindAllIndex([]byte(p.Description), -1)
|
||||
}
|
||||
if nameIndices == nil && descIndices == nil {
|
||||
continue
|
||||
}
|
||||
score := float64(indexsum(nameIndices)) / (float64(len(nameIndices)) + 1)
|
||||
if desc {
|
||||
score += float64(indexsum(descIndices)) / (float64(len(descIndices)) + 1) / 10.0
|
||||
}
|
||||
res = append(res, searchResult{
|
||||
NameIndices: nameIndices,
|
||||
DescIndices: descIndices,
|
||||
Score: score,
|
||||
metadata: p,
|
||||
})
|
||||
}
|
||||
slices.SortFunc(res[:], func(a, b searchResult) int { return -cmp.Compare(a.Score, b.Score) })
|
||||
expiry := time.Now().Add(1 * time.Minute)
|
||||
entry = searchCacheEntry{
|
||||
query: search,
|
||||
results: res,
|
||||
expiry: expiry,
|
||||
}
|
||||
index.search[query] = entry
|
||||
|
||||
return len(res), res[i:min(i+limit, len(entry.results))], nil
|
||||
}
|
||||
func (s *searchCache) clean() {
|
||||
maps.DeleteFunc(*s, func(_ string, v searchCacheEntry) bool {
|
||||
return v.expiry.Before(time.Now())
|
||||
})
|
||||
}
|
||||
func indexsum(in [][]int) int {
|
||||
sum := 0
|
||||
for i := 0; i < len(in); i++ {
|
||||
sum += in[i][1] - in[i][0]
|
||||
}
|
||||
return sum
|
||||
}
|
||||
57
cmd/mbf/internal/pkgserver/ui/index.html
Normal file
57
cmd/mbf/internal/pkgserver/ui/index.html
Normal file
@@ -0,0 +1,57 @@
|
||||
<!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>Hakurei PkgServer</title>
|
||||
<script src="index.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<h1>Hakurei PkgServer</h1>
|
||||
<div class="top-controls" id="top-controls-regular">
|
||||
<p>Showing entries <span id="entry-counter"></span>.</p>
|
||||
<span id="search-bar">
|
||||
<label for="search">Search: </label>
|
||||
<input type="text" name="search" id="search"/>
|
||||
<button onclick="doSearch()">Find</button>
|
||||
<label for="include-desc">Include descriptions: </label>
|
||||
<input type="checkbox" name="include-desc" id="include-desc" checked/>
|
||||
</span>
|
||||
<div><label for="count">Entries per page: </label><select name="count" id="count">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="50">50</option>
|
||||
</select></div>
|
||||
<div><label for="sort">Sort by: </label><select name="sort" id="sort">
|
||||
<option value="0">Definition (ascending)</option>
|
||||
<option value="1">Definition (descending)</option>
|
||||
<option value="2">Name (ascending)</option>
|
||||
<option value="3">Name (descending)</option>
|
||||
<option value="4">Size (ascending)</option>
|
||||
<option value="5">Size (descending)</option>
|
||||
</select></div>
|
||||
</div>
|
||||
<div class="top-controls" id="search-top-controls" hidden>
|
||||
<p>Showing search results <span id="search-entry-counter"></span> for query "<span id="search-query"></span>".</p>
|
||||
<button onclick="exitSearch()">Back</button>
|
||||
<div><label for="search-count">Entries per page: </label><select name="search-count" id="search-count">
|
||||
<option value="10">10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="30">30</option>
|
||||
<option value="50">50</option>
|
||||
</select></div>
|
||||
<p>Sorted by best match</p>
|
||||
</div>
|
||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<table id="pkg-list">
|
||||
<tr><td>Loading...</td></tr>
|
||||
</table>
|
||||
<div class="page-controls"><a href="javascript:prevPage()">« Previous</a> <input type="text" class="page-number" value="1"/> <a href="javascript:nextPage()">Next »</a></div>
|
||||
<footer>
|
||||
<p>©<a href="https://hakurei.app/">Hakurei</a> (<span id="hakurei-version">unknown</span>). Licensed under the MIT license.</p>
|
||||
</footer>
|
||||
<script>main();</script>
|
||||
</body>
|
||||
</html>
|
||||
331
cmd/mbf/internal/pkgserver/ui/index.ts
Normal file
331
cmd/mbf/internal/pkgserver/ui/index.ts
Normal file
@@ -0,0 +1,331 @@
|
||||
interface PackageIndexEntry {
|
||||
name: string
|
||||
size?: number
|
||||
description?: string
|
||||
website?: string
|
||||
version?: string
|
||||
report?: boolean
|
||||
}
|
||||
|
||||
function entryToHTML(entry: PackageIndexEntry | SearchResult): HTMLTableRowElement {
|
||||
let v = entry.version != null ? `<span>${escapeHtml(entry.version)}</span>` : ""
|
||||
let s = entry.size != null && entry.size > 0 ? `<p>Size: ${toByteSizeString(entry.size)} (${entry.size})</p>` : ""
|
||||
let n: string
|
||||
let d: string
|
||||
if ('name_matches' in entry) {
|
||||
n = `<h2>${nameMatches(entry as SearchResult)} ${v}</h2>`
|
||||
} else {
|
||||
n = `<h2>${escapeHtml(entry.name)} ${v}</h2>`
|
||||
}
|
||||
if ('desc_matches' in entry && STATE.getIncludeDescriptions()) {
|
||||
d = descMatches(entry as SearchResult)
|
||||
} else {
|
||||
d = (entry as PackageIndexEntry).description != null ? `<p>${escapeHtml((entry as PackageIndexEntry).description)}</p>` : ""
|
||||
}
|
||||
let w = entry.website != null ? `<a href="${encodeURI(entry.website)}">Website</a>` : ""
|
||||
let r = entry.report ? `Log (<a href=\"${encodeURI('/api/v1/status/' + entry.name)}\">View</a> | <a href=\"${encodeURI('/status/' + entry.name)}\">Download</a>)` : ""
|
||||
let row = <HTMLTableRowElement>(document.createElement('tr'))
|
||||
row.innerHTML = `<td>
|
||||
${n}
|
||||
${d}
|
||||
${s}
|
||||
${w}
|
||||
${r}
|
||||
</td>`
|
||||
return row
|
||||
}
|
||||
|
||||
function nameMatches(sr: SearchResult): string {
|
||||
return markMatches(sr.name, sr.name_matches)
|
||||
}
|
||||
|
||||
function descMatches(sr: SearchResult): string {
|
||||
return markMatches(sr.description!, sr.desc_matches)
|
||||
}
|
||||
|
||||
function markMatches(str: string, indices: [number, number][]): string {
|
||||
if (indices == null) {
|
||||
return str
|
||||
}
|
||||
let out: string = ""
|
||||
let j = 0
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
if (j < indices.length) {
|
||||
if (i === indices[j][0]) {
|
||||
out += `<mark>${escapeHtmlChar(str[i])}`
|
||||
continue
|
||||
}
|
||||
if (i === indices[j][1]) {
|
||||
out += `</mark>${escapeHtmlChar(str[i])}`
|
||||
j++
|
||||
continue
|
||||
}
|
||||
}
|
||||
out += escapeHtmlChar(str[i])
|
||||
}
|
||||
if (indices[j] !== undefined) {
|
||||
out += "</mark>"
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function toByteSizeString(bytes: number): string {
|
||||
if (bytes == null) return `unspecified`
|
||||
if (bytes < 1024) return `${bytes}B`
|
||||
if (bytes < Math.pow(1024, 2)) return `${(bytes / 1024).toFixed(2)}kiB`
|
||||
if (bytes < Math.pow(1024, 3)) return `${(bytes / Math.pow(1024, 2)).toFixed(2)}MiB`
|
||||
if (bytes < Math.pow(1024, 4)) return `${(bytes / Math.pow(1024, 3)).toFixed(2)}GiB`
|
||||
if (bytes < Math.pow(1024, 5)) return `${(bytes / Math.pow(1024, 4)).toFixed(2)}TiB`
|
||||
return "not only is it big, it's large"
|
||||
}
|
||||
|
||||
const API_VERSION = 1
|
||||
const ENDPOINT = `/api/v${API_VERSION}`
|
||||
|
||||
interface InfoPayload {
|
||||
count?: number
|
||||
hakurei_version?: string
|
||||
}
|
||||
|
||||
async function infoRequest(): Promise<InfoPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/info`)
|
||||
const payload = await res.json()
|
||||
return payload as InfoPayload
|
||||
}
|
||||
|
||||
interface GetPayload {
|
||||
values?: PackageIndexEntry[]
|
||||
}
|
||||
|
||||
enum SortOrders {
|
||||
DeclarationAscending,
|
||||
DeclarationDescending,
|
||||
NameAscending,
|
||||
NameDescending
|
||||
}
|
||||
|
||||
async function getRequest(limit: number, index: number, sort: SortOrders): Promise<GetPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/get?limit=${limit}&index=${index}&sort=${sort.valueOf()}`)
|
||||
const payload = await res.json()
|
||||
return payload as GetPayload
|
||||
}
|
||||
|
||||
interface SearchResult extends PackageIndexEntry {
|
||||
name_matches: [number, number][]
|
||||
desc_matches: [number, number][]
|
||||
score: number
|
||||
}
|
||||
|
||||
interface SearchPayload {
|
||||
count?: number
|
||||
values?: SearchResult[]
|
||||
}
|
||||
|
||||
async function searchRequest(limit: number, index: number, search: string, desc: boolean): Promise<SearchPayload> {
|
||||
const res = await fetch(`${ENDPOINT}/search?limit=${limit}&index=${index}&search=${encodeURIComponent(search)}&desc=${desc}`)
|
||||
if (!res.ok) {
|
||||
exitSearch()
|
||||
alert("invalid search query!")
|
||||
return Promise.reject(res.statusText)
|
||||
}
|
||||
const payload = await res.json()
|
||||
return payload as SearchPayload
|
||||
}
|
||||
|
||||
class State {
|
||||
entriesPerPage: number = 10
|
||||
entryIndex: number = 0
|
||||
maxTotal: number = 0
|
||||
maxEntries: number = 0
|
||||
sort: SortOrders = SortOrders.DeclarationAscending
|
||||
search: boolean = false
|
||||
|
||||
getEntriesPerPage(): number {
|
||||
return this.entriesPerPage
|
||||
}
|
||||
|
||||
setEntriesPerPage(entriesPerPage: number) {
|
||||
this.entriesPerPage = entriesPerPage
|
||||
this.setEntryIndex(Math.floor(this.getEntryIndex() / entriesPerPage) * entriesPerPage)
|
||||
}
|
||||
|
||||
getEntryIndex(): number {
|
||||
return this.entryIndex
|
||||
}
|
||||
|
||||
setEntryIndex(entryIndex: number) {
|
||||
this.entryIndex = entryIndex
|
||||
this.updatePage()
|
||||
this.updateRange()
|
||||
this.updateListings()
|
||||
}
|
||||
|
||||
getMaxTotal(): number {
|
||||
return this.maxTotal
|
||||
}
|
||||
|
||||
setMaxTotal(max: number) {
|
||||
this.maxTotal = max
|
||||
}
|
||||
|
||||
getSortOrder(): SortOrders {
|
||||
return this.sort
|
||||
}
|
||||
|
||||
setSortOrder(sortOrder: SortOrders) {
|
||||
this.sort = sortOrder
|
||||
this.setEntryIndex(0)
|
||||
}
|
||||
|
||||
updatePage() {
|
||||
let page = Math.ceil(((this.getEntryIndex() + this.getEntriesPerPage()) - 1) / this.getEntriesPerPage())
|
||||
for (let e of document.getElementsByClassName("page-number")) {
|
||||
(e as HTMLInputElement).value = String(page)
|
||||
}
|
||||
}
|
||||
|
||||
updateRange() {
|
||||
let max = Math.min(this.getEntryIndex() + this.getEntriesPerPage(), this.getMaxTotal())
|
||||
document.getElementById("entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.getMaxTotal()}`
|
||||
if (this.search) {
|
||||
document.getElementById("search-entry-counter")!.textContent = `${this.getEntryIndex() + 1}-${max} of ${this.maxTotal}/${this.maxEntries}`
|
||||
document.getElementById("search-query")!.innerHTML = `<code>${escapeHtml(this.getSearchQuery())}</code>`
|
||||
}
|
||||
}
|
||||
|
||||
getSearchQuery(): string {
|
||||
let queryString = document.getElementById("search")!;
|
||||
return (queryString as HTMLInputElement).value
|
||||
}
|
||||
|
||||
getIncludeDescriptions(): boolean {
|
||||
let includeDesc = document.getElementById("include-desc")!;
|
||||
return (includeDesc as HTMLInputElement).checked
|
||||
}
|
||||
|
||||
updateListings() {
|
||||
if (this.search) {
|
||||
searchRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSearchQuery(), this.getIncludeDescriptions())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")!
|
||||
table.innerHTML = ''
|
||||
for (let row of res.values!) {
|
||||
table.appendChild(entryToHTML(row))
|
||||
}
|
||||
STATE.maxTotal = res.count!
|
||||
STATE.updateRange()
|
||||
if(res.count! < 1) {
|
||||
exitSearch()
|
||||
alert("no results found!")
|
||||
}
|
||||
})
|
||||
} else {
|
||||
getRequest(this.getEntriesPerPage(), this.getEntryIndex(), this.getSortOrder())
|
||||
.then(res => {
|
||||
let table = document.getElementById("pkg-list")!
|
||||
table.innerHTML = ''
|
||||
for (let row of res.values!) {
|
||||
table.appendChild(entryToHTML(row))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let STATE: State
|
||||
|
||||
|
||||
function lastPageIndex(): number {
|
||||
return Math.floor(STATE.getMaxTotal() / STATE.getEntriesPerPage()) * STATE.getEntriesPerPage()
|
||||
}
|
||||
|
||||
function setPage(page: number) {
|
||||
STATE.setEntryIndex(Math.max(0, Math.min(STATE.getEntriesPerPage() * (page - 1), lastPageIndex())))
|
||||
}
|
||||
|
||||
|
||||
function escapeHtml(str?: string): string {
|
||||
let out: string = ''
|
||||
if (str == undefined) return ""
|
||||
for (let i = 0; i < str.length; i++) {
|
||||
out += escapeHtmlChar(str[i])
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
function escapeHtmlChar(char: string): string {
|
||||
if (char.length != 1) return char
|
||||
switch (char[0]) {
|
||||
case '&':
|
||||
return "&"
|
||||
case '<':
|
||||
return "<"
|
||||
case '>':
|
||||
return ">"
|
||||
case '"':
|
||||
return """
|
||||
case "'":
|
||||
return "'"
|
||||
default:
|
||||
return char
|
||||
}
|
||||
}
|
||||
|
||||
function firstPage() {
|
||||
STATE.setEntryIndex(0)
|
||||
}
|
||||
|
||||
function prevPage() {
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.max(0, index - STATE.getEntriesPerPage()))
|
||||
}
|
||||
|
||||
function lastPage() {
|
||||
STATE.setEntryIndex(lastPageIndex())
|
||||
}
|
||||
|
||||
function nextPage() {
|
||||
let index = STATE.getEntryIndex()
|
||||
STATE.setEntryIndex(Math.min(lastPageIndex(), index + STATE.getEntriesPerPage()))
|
||||
}
|
||||
|
||||
function doSearch() {
|
||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||
STATE.search = true;
|
||||
STATE.setEntryIndex(0);
|
||||
}
|
||||
|
||||
function exitSearch() {
|
||||
document.getElementById("top-controls-regular")!.toggleAttribute("hidden");
|
||||
document.getElementById("search-top-controls")!.toggleAttribute("hidden");
|
||||
STATE.search = false;
|
||||
STATE.setMaxTotal(STATE.maxEntries)
|
||||
STATE.setEntryIndex(0)
|
||||
}
|
||||
|
||||
function main() {
|
||||
STATE = new State()
|
||||
infoRequest()
|
||||
.then(res => {
|
||||
STATE.maxEntries = res.count!
|
||||
STATE.setMaxTotal(STATE.maxEntries)
|
||||
document.getElementById("hakurei-version")!.textContent = res.hakurei_version!
|
||||
STATE.updateRange()
|
||||
STATE.updateListings()
|
||||
})
|
||||
for (let e of document.getElementsByClassName("page-number")) {
|
||||
e.addEventListener("change", (_) => {
|
||||
setPage(parseInt((e as HTMLInputElement).value))
|
||||
})
|
||||
}
|
||||
document.getElementById("count")?.addEventListener("change", (event) => {
|
||||
STATE.setEntriesPerPage(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
document.getElementById("sort")?.addEventListener("change", (event) => {
|
||||
STATE.setSortOrder(parseInt((event.target as HTMLSelectElement).value))
|
||||
})
|
||||
document.getElementById("search")?.addEventListener("keyup", (event) => {
|
||||
if (event.key === 'Enter') doSearch()
|
||||
})
|
||||
}
|
||||
21
cmd/mbf/internal/pkgserver/ui/style.css
Normal file
21
cmd/mbf/internal/pkgserver/ui/style.css
Normal file
@@ -0,0 +1,21 @@
|
||||
.page-number {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
.page-number {
|
||||
width: 2em;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
html {
|
||||
background-color: #2c2c2c;
|
||||
color: ghostwhite;
|
||||
}
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
html {
|
||||
background-color: #d3d3d3;
|
||||
color: black;
|
||||
}
|
||||
}
|
||||
8
cmd/mbf/internal/pkgserver/ui/tsconfig.json
Normal file
8
cmd/mbf/internal/pkgserver/ui/tsconfig.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2024",
|
||||
"strict": true,
|
||||
"alwaysStrict": true,
|
||||
"outDir": "static"
|
||||
}
|
||||
}
|
||||
9
cmd/mbf/internal/pkgserver/ui/ui.go
Normal file
9
cmd/mbf/internal/pkgserver/ui/ui.go
Normal file
@@ -0,0 +1,9 @@
|
||||
// Package ui holds the static web UI.
|
||||
package ui
|
||||
|
||||
import "net/http"
|
||||
|
||||
// Register arranges for mux to serve the embedded frontend.
|
||||
func Register(mux *http.ServeMux) {
|
||||
mux.Handle("GET /", http.FileServer(http.FS(static)))
|
||||
}
|
||||
21
cmd/mbf/internal/pkgserver/ui/ui_full.go
Normal file
21
cmd/mbf/internal/pkgserver/ui/ui_full.go
Normal file
@@ -0,0 +1,21 @@
|
||||
//go:build frontend
|
||||
|
||||
package ui
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:generate tsc
|
||||
//go:generate cp index.html style.css static
|
||||
//go:embed static
|
||||
var _static embed.FS
|
||||
|
||||
var static = func() fs.FS {
|
||||
if f, err := fs.Sub(_static, "static"); err != nil {
|
||||
panic(err)
|
||||
} else {
|
||||
return f
|
||||
}
|
||||
}()
|
||||
7
cmd/mbf/internal/pkgserver/ui/ui_stub.go
Normal file
7
cmd/mbf/internal/pkgserver/ui/ui_stub.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !frontend
|
||||
|
||||
package ui
|
||||
|
||||
import "testing/fstest"
|
||||
|
||||
var static fstest.MapFS
|
||||
548
cmd/mbf/main.go
548
cmd/mbf/main.go
@@ -14,16 +14,18 @@ package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
@@ -40,6 +42,9 @@ import (
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/rosa"
|
||||
"hakurei.app/message"
|
||||
|
||||
"hakurei.app/cmd/mbf/internal/pkgserver"
|
||||
"hakurei.app/cmd/mbf/internal/pkgserver/ui"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -53,77 +58,106 @@ func main() {
|
||||
log.Fatal("this program must not run as root")
|
||||
}
|
||||
|
||||
var cache *pkg.Cache
|
||||
ctx, stop := signal.NotifyContext(context.Background(),
|
||||
syscall.SIGINT, syscall.SIGTERM, syscall.SIGHUP)
|
||||
defer stop()
|
||||
defer func() {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
|
||||
if r := recover(); r != nil {
|
||||
fmt.Println(r)
|
||||
log.Fatal("consider scrubbing the on-disk cache")
|
||||
}
|
||||
}()
|
||||
var cm cache
|
||||
defer func() { cm.Close() }()
|
||||
|
||||
var (
|
||||
flagQuiet bool
|
||||
flagCures int
|
||||
flagBase string
|
||||
flagTShift int
|
||||
flagIdle bool
|
||||
flagQuiet bool
|
||||
flagCheck bool
|
||||
flagLTO bool
|
||||
|
||||
addr net.UnixAddr
|
||||
)
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) (err error) {
|
||||
c := command.New(os.Stderr, log.Printf, "mbf", func([]string) error {
|
||||
msg.SwapVerbose(!flagQuiet)
|
||||
|
||||
flagBase = os.ExpandEnv(flagBase)
|
||||
if flagBase == "" {
|
||||
flagBase = "cache"
|
||||
cm.ctx, cm.msg = ctx, msg
|
||||
cm.base = os.ExpandEnv(cm.base)
|
||||
if cm.base == "" {
|
||||
cm.base = "cache"
|
||||
}
|
||||
|
||||
var base *check.Absolute
|
||||
if flagBase, err = filepath.Abs(flagBase); err != nil {
|
||||
return
|
||||
} else if base, err = check.NewAbs(flagBase); err != nil {
|
||||
return
|
||||
}
|
||||
if cache, err = pkg.Open(ctx, msg, flagCures, base); err == nil {
|
||||
if flagTShift < 0 {
|
||||
cache.SetThreshold(0)
|
||||
} else if flagTShift > 31 {
|
||||
cache.SetThreshold(1 << 31)
|
||||
} else {
|
||||
cache.SetThreshold(1 << flagTShift)
|
||||
}
|
||||
addr.Net = "unix"
|
||||
addr.Name = os.ExpandEnv(addr.Name)
|
||||
if addr.Name == "" {
|
||||
addr.Name = filepath.Join(cm.base, "daemon")
|
||||
}
|
||||
|
||||
if flagIdle {
|
||||
pkg.SetSchedIdle = true
|
||||
var flags int
|
||||
if !flagCheck {
|
||||
flags |= rosa.OptSkipCheck
|
||||
}
|
||||
if !flagLTO {
|
||||
flags |= rosa.OptLLVMNoLTO
|
||||
}
|
||||
rosa.DropCaches(flags)
|
||||
|
||||
return
|
||||
return nil
|
||||
}).Flag(
|
||||
&flagQuiet,
|
||||
"q", command.BoolFlag(false),
|
||||
"Do not print cure messages",
|
||||
).Flag(
|
||||
&flagCures,
|
||||
&flagLTO,
|
||||
"lto", command.BoolFlag(false),
|
||||
"Enable LTO in stage2 and stage3 LLVM toolchains",
|
||||
).Flag(
|
||||
&flagCheck,
|
||||
"check", command.BoolFlag(true),
|
||||
"Run test suites",
|
||||
).Flag(
|
||||
&cm.cures,
|
||||
"cures", command.IntFlag(0),
|
||||
"Maximum number of dependencies to cure at any given time",
|
||||
).Flag(
|
||||
&flagBase,
|
||||
&cm.jobs,
|
||||
"jobs", command.IntFlag(0),
|
||||
"Preferred number of jobs to run, when applicable",
|
||||
).Flag(
|
||||
&cm.base,
|
||||
"d", command.StringFlag("$MBF_CACHE_DIR"),
|
||||
"Directory to store cured artifacts",
|
||||
).Flag(
|
||||
&flagTShift,
|
||||
"tshift", command.IntFlag(-1),
|
||||
"Dependency graph size exponent, to the power of 2",
|
||||
).Flag(
|
||||
&flagIdle,
|
||||
&cm.idle,
|
||||
"sched-idle", command.BoolFlag(false),
|
||||
"Set SCHED_IDLE scheduling policy",
|
||||
).Flag(
|
||||
&cm.hostAbstract,
|
||||
"host-abstract", command.BoolFlag(
|
||||
os.Getenv("MBF_HOST_ABSTRACT") != "",
|
||||
),
|
||||
"Do not restrict networked cure containers from connecting to host "+
|
||||
"abstract UNIX sockets",
|
||||
).Flag(
|
||||
&addr.Name,
|
||||
"socket", command.StringFlag("$MBF_DAEMON_SOCKET"),
|
||||
"Pathname of socket to bind to",
|
||||
)
|
||||
|
||||
c.NewCommand(
|
||||
"checksum", "Compute checksum of data read from standard input",
|
||||
func([]string) error {
|
||||
done := make(chan struct{})
|
||||
defer close(done)
|
||||
go func() {
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
os.Exit(1)
|
||||
case <-done:
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
h := sha512.New384()
|
||||
if _, err := io.Copy(h, os.Stdin); err != nil {
|
||||
return err
|
||||
}
|
||||
log.Println(pkg.Encode(pkg.Checksum(h.Sum(nil))))
|
||||
return nil
|
||||
},
|
||||
)
|
||||
|
||||
{
|
||||
@@ -137,7 +171,9 @@ func main() {
|
||||
if flagShifts < 0 || flagShifts > 31 {
|
||||
flagShifts = 12
|
||||
}
|
||||
return cache.Scrub(runtime.NumCPU() << flagShifts)
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.Scrub(runtime.NumCPU() << flagShifts)
|
||||
})
|
||||
},
|
||||
).Flag(
|
||||
&flagShifts,
|
||||
@@ -148,6 +184,7 @@ func main() {
|
||||
|
||||
{
|
||||
var (
|
||||
flagBind string
|
||||
flagStatus bool
|
||||
flagReport string
|
||||
)
|
||||
@@ -155,9 +192,7 @@ func main() {
|
||||
"info",
|
||||
"Display out-of-band metadata of an artifact",
|
||||
func(args []string) (err error) {
|
||||
if len(args) == 0 {
|
||||
return errors.New("info requires at least 1 argument")
|
||||
}
|
||||
const shutdownTimeout = 15 * time.Second
|
||||
|
||||
var r *rosa.Report
|
||||
if flagReport != "" {
|
||||
@@ -172,88 +207,46 @@ func main() {
|
||||
defer r.HandleAccess(&err)()
|
||||
}
|
||||
|
||||
for i, name := range args {
|
||||
if p, ok := rosa.ResolveName(name); !ok {
|
||||
return fmt.Errorf("unknown artifact %q", name)
|
||||
} else {
|
||||
var suffix string
|
||||
if version := rosa.Std.Version(p); version != rosa.Unversioned {
|
||||
suffix += "-" + version
|
||||
}
|
||||
fmt.Println("name : " + name + suffix)
|
||||
|
||||
meta := rosa.GetMetadata(p)
|
||||
fmt.Println("description : " + meta.Description)
|
||||
if meta.Website != "" {
|
||||
fmt.Println("website : " +
|
||||
strings.TrimSuffix(meta.Website, "/"))
|
||||
}
|
||||
if len(meta.Dependencies) > 0 {
|
||||
fmt.Print("depends on :")
|
||||
for _, d := range meta.Dependencies {
|
||||
s := rosa.GetMetadata(d).Name
|
||||
if version := rosa.Std.Version(d); version != rosa.Unversioned {
|
||||
s += "-" + version
|
||||
}
|
||||
fmt.Print(" " + s)
|
||||
}
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
const statusPrefix = "status : "
|
||||
if flagStatus {
|
||||
if r == nil {
|
||||
var f io.ReadSeekCloser
|
||||
f, err = cache.OpenStatus(rosa.Std.Load(p))
|
||||
if err != nil {
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
fmt.Println(
|
||||
statusPrefix + "not yet cured",
|
||||
)
|
||||
} else {
|
||||
return
|
||||
}
|
||||
} else {
|
||||
fmt.Print(statusPrefix)
|
||||
_, err = io.Copy(os.Stdout, f)
|
||||
if err = errors.Join(err, f.Close()); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
} else {
|
||||
status, n := r.ArtifactOf(cache.Ident(rosa.Std.Load(p)))
|
||||
if status == nil {
|
||||
fmt.Println(
|
||||
statusPrefix + "not in report",
|
||||
)
|
||||
} else {
|
||||
fmt.Println("size :", n)
|
||||
fmt.Print(statusPrefix)
|
||||
if _, err = os.Stdout.Write(status); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if i != len(args)-1 {
|
||||
fmt.Println()
|
||||
}
|
||||
}
|
||||
if flagBind == "" {
|
||||
return commandInfo(&cm, args, os.Stdout, flagStatus, r)
|
||||
}
|
||||
return nil
|
||||
|
||||
var mux http.ServeMux
|
||||
ui.Register(&mux)
|
||||
if err = pkgserver.Register(ctx, &mux, r); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
server := http.Server{Addr: flagBind, Handler: &mux}
|
||||
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", flagBind)
|
||||
err = server.ListenAndServe()
|
||||
if errors.Is(err, http.ErrServerClosed) {
|
||||
err = nil
|
||||
}
|
||||
return
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagStatus,
|
||||
"status", command.BoolFlag(false),
|
||||
"Display cure status if available",
|
||||
).
|
||||
Flag(
|
||||
&flagReport,
|
||||
"report", command.StringFlag(""),
|
||||
"Load cure status from this report file instead of cache",
|
||||
)
|
||||
).Flag(
|
||||
&flagBind,
|
||||
"bind", command.StringFlag(""),
|
||||
"TCP address for the server to listen on",
|
||||
).Flag(
|
||||
&flagStatus,
|
||||
"status", command.BoolFlag(false),
|
||||
"Display cure status if available",
|
||||
).Flag(
|
||||
&flagReport,
|
||||
"report", command.StringFlag(""),
|
||||
"Load cure status from this report file instead of cache",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
@@ -287,7 +280,9 @@ func main() {
|
||||
if ext.Isatty(int(w.Fd())) {
|
||||
return errors.New("output appears to be a terminal")
|
||||
}
|
||||
return rosa.WriteReport(msg, w, cache)
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return rosa.WriteReport(msg, w, cache)
|
||||
})
|
||||
},
|
||||
)
|
||||
|
||||
@@ -350,14 +345,26 @@ func main() {
|
||||
" package(s) are out of date"))
|
||||
}
|
||||
return errors.Join(errs...)
|
||||
}).
|
||||
Flag(
|
||||
&flagJobs,
|
||||
"j", command.IntFlag(32),
|
||||
"Maximum number of simultaneous connections",
|
||||
)
|
||||
}).Flag(
|
||||
&flagJobs,
|
||||
"j", command.IntFlag(32),
|
||||
"Maximum number of simultaneous connections",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
"daemon",
|
||||
"Service artifact IR with Rosa OS extensions",
|
||||
func(args []string) error {
|
||||
ul, err := net.ListenUnix("unix", &addr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Printf("listening on pathname socket at %s", addr.Name)
|
||||
return serve(ctx, log.Default(), &cm, ul)
|
||||
},
|
||||
)
|
||||
|
||||
{
|
||||
var (
|
||||
flagGentoo string
|
||||
@@ -382,25 +389,37 @@ func main() {
|
||||
rosa.SetGentooStage3(flagGentoo, checksum)
|
||||
}
|
||||
|
||||
_, _, _, stage1 := (t - 2).NewLLVM()
|
||||
_, _, _, stage2 := (t - 1).NewLLVM()
|
||||
_, _, _, stage3 := t.NewLLVM()
|
||||
var (
|
||||
pathname *check.Absolute
|
||||
checksum [2]unique.Handle[pkg.Checksum]
|
||||
)
|
||||
|
||||
if pathname, _, err = cache.Cure(stage1); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(
|
||||
(t - 2).Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage1:", pathname)
|
||||
|
||||
if pathname, checksum[0], err = cache.Cure(stage2); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, checksum[0], err = cache.Cure(
|
||||
(t - 1).Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage2:", pathname)
|
||||
if pathname, checksum[1], err = cache.Cure(stage3); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, checksum[1], err = cache.Cure(
|
||||
t.Load(rosa.LLVM),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("stage3:", pathname)
|
||||
|
||||
@@ -417,39 +436,44 @@ func main() {
|
||||
}
|
||||
|
||||
if flagStage0 {
|
||||
if pathname, _, err = cache.Cure(
|
||||
t.Load(rosa.Stage0),
|
||||
); err != nil {
|
||||
return err
|
||||
if err = cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(
|
||||
t.Load(rosa.Stage0),
|
||||
)
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
log.Println(pathname)
|
||||
}
|
||||
|
||||
return
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagGentoo,
|
||||
"gentoo", command.StringFlag(""),
|
||||
"Bootstrap from a Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
&flagChecksum,
|
||||
"checksum", command.StringFlag(""),
|
||||
"Checksum of Gentoo stage3 tarball",
|
||||
).
|
||||
Flag(
|
||||
&flagStage0,
|
||||
"stage0", command.BoolFlag(false),
|
||||
"Create bootstrap stage0 tarball",
|
||||
)
|
||||
).Flag(
|
||||
&flagGentoo,
|
||||
"gentoo", command.StringFlag(""),
|
||||
"Bootstrap from a Gentoo stage3 tarball",
|
||||
).Flag(
|
||||
&flagChecksum,
|
||||
"checksum", command.StringFlag(""),
|
||||
"Checksum of Gentoo stage3 tarball",
|
||||
).Flag(
|
||||
&flagStage0,
|
||||
"stage0", command.BoolFlag(false),
|
||||
"Create bootstrap stage0 tarball",
|
||||
)
|
||||
}
|
||||
|
||||
{
|
||||
var (
|
||||
flagDump string
|
||||
flagEnter bool
|
||||
flagExport string
|
||||
flagDump string
|
||||
flagEnter bool
|
||||
flagExport string
|
||||
flagRemote bool
|
||||
flagNoReply bool
|
||||
|
||||
flagBoot bool
|
||||
flagStd bool
|
||||
)
|
||||
c.NewCommand(
|
||||
"cure",
|
||||
@@ -463,9 +487,20 @@ func main() {
|
||||
return fmt.Errorf("unknown artifact %q", args[0])
|
||||
}
|
||||
|
||||
t := rosa.Std
|
||||
if flagBoot {
|
||||
t -= 2
|
||||
} else if flagStd {
|
||||
t -= 1
|
||||
}
|
||||
|
||||
switch {
|
||||
default:
|
||||
pathname, _, err := cache.Cure(rosa.Std.Load(p))
|
||||
var pathname *check.Absolute
|
||||
err := cm.Do(func(cache *pkg.Cache) (err error) {
|
||||
pathname, _, err = cache.Cure(t.Load(p))
|
||||
return
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -505,7 +540,7 @@ func main() {
|
||||
return err
|
||||
}
|
||||
|
||||
if err = cache.EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
||||
if err = pkg.NewIR().EncodeAll(f, rosa.Std.Load(p)); err != nil {
|
||||
_ = f.Close()
|
||||
return err
|
||||
}
|
||||
@@ -513,33 +548,76 @@ func main() {
|
||||
return f.Close()
|
||||
|
||||
case flagEnter:
|
||||
return cache.EnterExec(
|
||||
ctx,
|
||||
rosa.Std.Load(p),
|
||||
true, os.Stdin, os.Stdout, os.Stderr,
|
||||
rosa.AbsSystem.Append("bin", "mksh"),
|
||||
"sh",
|
||||
)
|
||||
return cm.Do(func(cache *pkg.Cache) error {
|
||||
return cache.EnterExec(
|
||||
ctx,
|
||||
t.Load(p),
|
||||
true, os.Stdin, os.Stdout, os.Stderr,
|
||||
rosa.AbsSystem.Append("bin", "mksh"),
|
||||
"sh",
|
||||
)
|
||||
})
|
||||
|
||||
case flagRemote:
|
||||
var flags uint64
|
||||
if flagNoReply {
|
||||
flags |= remoteNoReply
|
||||
}
|
||||
a := t.Load(p)
|
||||
pathname, err := cureRemote(ctx, &addr, a, flags)
|
||||
if !flagNoReply && err == nil {
|
||||
log.Println(pathname)
|
||||
}
|
||||
|
||||
if errors.Is(err, context.Canceled) {
|
||||
cc, cancel := context.WithDeadline(context.Background(), daemonDeadline())
|
||||
defer cancel()
|
||||
|
||||
if _err := cancelRemote(cc, &addr, a, false); _err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagDump,
|
||||
"dump", command.StringFlag(""),
|
||||
"Write IR to specified pathname and terminate",
|
||||
).
|
||||
Flag(
|
||||
&flagExport,
|
||||
"export", command.StringFlag(""),
|
||||
"Export cured artifact to specified pathname",
|
||||
).
|
||||
Flag(
|
||||
&flagEnter,
|
||||
"enter", command.BoolFlag(false),
|
||||
"Enter cure container with an interactive shell",
|
||||
)
|
||||
).Flag(
|
||||
&flagDump,
|
||||
"dump", command.StringFlag(""),
|
||||
"Write IR to specified pathname and terminate",
|
||||
).Flag(
|
||||
&flagExport,
|
||||
"export", command.StringFlag(""),
|
||||
"Export cured artifact to specified pathname",
|
||||
).Flag(
|
||||
&flagEnter,
|
||||
"enter", command.BoolFlag(false),
|
||||
"Enter cure container with an interactive shell",
|
||||
).Flag(
|
||||
&flagRemote,
|
||||
"daemon", command.BoolFlag(false),
|
||||
"Cure artifact on the daemon",
|
||||
).Flag(
|
||||
&flagNoReply,
|
||||
"no-reply", command.BoolFlag(false),
|
||||
"Do not receive a reply from the daemon",
|
||||
).Flag(
|
||||
&flagBoot,
|
||||
"boot", command.BoolFlag(false),
|
||||
"Build on the stage0 toolchain",
|
||||
).Flag(
|
||||
&flagStd,
|
||||
"std", command.BoolFlag(false),
|
||||
"Build on the intermediate toolchain",
|
||||
)
|
||||
}
|
||||
|
||||
c.NewCommand(
|
||||
"abort",
|
||||
"Abort all pending cures on the daemon",
|
||||
func([]string) error { return abortRemote(ctx, &addr, false) },
|
||||
)
|
||||
|
||||
{
|
||||
var (
|
||||
flagNet bool
|
||||
@@ -551,7 +629,7 @@ func main() {
|
||||
"shell",
|
||||
"Interactive shell in the specified Rosa OS environment",
|
||||
func(args []string) error {
|
||||
presets := make([]rosa.PArtifact, len(args))
|
||||
presets := make([]rosa.PArtifact, len(args)+3)
|
||||
for i, arg := range args {
|
||||
p, ok := rosa.ResolveName(arg)
|
||||
if !ok {
|
||||
@@ -559,21 +637,24 @@ func main() {
|
||||
}
|
||||
presets[i] = p
|
||||
}
|
||||
|
||||
base := rosa.LLVM
|
||||
if !flagWithToolchain {
|
||||
base = rosa.Musl
|
||||
}
|
||||
presets = append(presets,
|
||||
base,
|
||||
rosa.Mksh,
|
||||
rosa.Toybox,
|
||||
)
|
||||
|
||||
root := make(pkg.Collect, 0, 6+len(args))
|
||||
root = rosa.Std.AppendPresets(root, presets...)
|
||||
|
||||
if flagWithToolchain {
|
||||
musl, compilerRT, runtimes, clang := (rosa.Std - 1).NewLLVM()
|
||||
root = append(root, musl, compilerRT, runtimes, clang)
|
||||
} else {
|
||||
root = append(root, rosa.Std.Load(rosa.Musl))
|
||||
}
|
||||
root = append(root,
|
||||
rosa.Std.Load(rosa.Mksh),
|
||||
rosa.Std.Load(rosa.Toybox),
|
||||
)
|
||||
|
||||
if _, _, err := cache.Cure(&root); err == nil {
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
_, _, err := cache.Cure(&root)
|
||||
return err
|
||||
}); err == nil {
|
||||
return errors.New("unreachable")
|
||||
} else if !pkg.IsCollected(err) {
|
||||
return err
|
||||
@@ -585,11 +666,22 @@ func main() {
|
||||
}
|
||||
cured := make(map[pkg.Artifact]cureRes)
|
||||
for _, a := range root {
|
||||
pathname, checksum, err := cache.Cure(a)
|
||||
if err != nil {
|
||||
if err := cm.Do(func(cache *pkg.Cache) error {
|
||||
pathname, checksum, err := cache.Cure(a)
|
||||
if err == nil {
|
||||
cured[a] = cureRes{pathname, checksum}
|
||||
}
|
||||
return err
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// explicitly open for direct error-free use from this point
|
||||
if cm.c == nil {
|
||||
if err := cm.open(); err != nil {
|
||||
return err
|
||||
}
|
||||
cured[a] = cureRes{pathname, checksum}
|
||||
}
|
||||
|
||||
layers := pkg.PromoteLayers(root, func(a pkg.Artifact) (
|
||||
@@ -599,7 +691,7 @@ func main() {
|
||||
res := cured[a]
|
||||
return res.pathname, res.checksum
|
||||
}, func(i int, d pkg.Artifact) {
|
||||
r := pkg.Encode(cache.Ident(d).Value())
|
||||
r := pkg.Encode(cm.c.Ident(d).Value())
|
||||
if s, ok := d.(fmt.Stringer); ok {
|
||||
if name := s.String(); name != "" {
|
||||
r += "-" + name
|
||||
@@ -618,6 +710,9 @@ func main() {
|
||||
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
|
||||
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 {
|
||||
@@ -660,22 +755,19 @@ func main() {
|
||||
}
|
||||
return z.Wait()
|
||||
},
|
||||
).
|
||||
Flag(
|
||||
&flagNet,
|
||||
"net", command.BoolFlag(false),
|
||||
"Share host net namespace",
|
||||
).
|
||||
Flag(
|
||||
&flagSession,
|
||||
"session", command.BoolFlag(true),
|
||||
"Retain session",
|
||||
).
|
||||
Flag(
|
||||
&flagWithToolchain,
|
||||
"with-toolchain", command.BoolFlag(false),
|
||||
"Include the stage2 LLVM toolchain",
|
||||
)
|
||||
).Flag(
|
||||
&flagNet,
|
||||
"net", command.BoolFlag(false),
|
||||
"Share host net namespace",
|
||||
).Flag(
|
||||
&flagSession,
|
||||
"session", command.BoolFlag(true),
|
||||
"Retain session",
|
||||
).Flag(
|
||||
&flagWithToolchain,
|
||||
"with-toolchain", command.BoolFlag(false),
|
||||
"Include the stage2 LLVM toolchain",
|
||||
)
|
||||
|
||||
}
|
||||
|
||||
@@ -686,9 +778,7 @@ func main() {
|
||||
)
|
||||
|
||||
c.MustParse(os.Args[1:], func(err error) {
|
||||
if cache != nil {
|
||||
cache.Close()
|
||||
}
|
||||
cm.Close()
|
||||
if w, ok := err.(interface{ Unwrap() []error }); !ok {
|
||||
log.Fatal(err)
|
||||
} else {
|
||||
|
||||
47
cmd/mbf/main_test.go
Normal file
47
cmd/mbf/main_test.go
Normal file
@@ -0,0 +1,47 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/rosa"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
rosa.DropCaches(rosa.OptLLVMNoLTO)
|
||||
os.Exit(m.Run())
|
||||
}
|
||||
|
||||
func TestCureAll(t *testing.T) {
|
||||
t.Parallel()
|
||||
const env = "ROSA_TEST_DAEMON"
|
||||
|
||||
if !testing.Verbose() {
|
||||
t.Skip("verbose flag not set")
|
||||
}
|
||||
|
||||
pathname, ok := os.LookupEnv(env)
|
||||
if !ok {
|
||||
t.Skip(env + " not set")
|
||||
}
|
||||
|
||||
addr := net.UnixAddr{Net: "unix", Name: pathname}
|
||||
t.Cleanup(func() {
|
||||
if t.Failed() {
|
||||
if err := abortRemote(t.Context(), &addr, false); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
for i := range rosa.PresetEnd {
|
||||
p := rosa.PArtifact(i)
|
||||
t.Run(rosa.GetMetadata(p).Name, func(t *testing.T) {
|
||||
_, err := cureRemote(t.Context(), &addr, rosa.Std.Load(p), 0)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@
|
||||
#endif
|
||||
|
||||
#define SHAREFS_MEDIA_RW_ID (1 << 10) - 1 /* owning gid presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0700 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0600 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_PERM_DIR 0770 /* permission bits for directories presented to userspace */
|
||||
#define SHAREFS_PERM_REG 0660 /* permission bits for regular files presented to userspace */
|
||||
#define SHAREFS_FORBIDDEN_FLAGS O_DIRECT /* these open flags are cleared unconditionally */
|
||||
|
||||
/* sharefs_private is populated by sharefs_init and contains process-wide context */
|
||||
|
||||
@@ -19,12 +19,11 @@ import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/cgo"
|
||||
"strconv"
|
||||
@@ -145,9 +144,9 @@ func sharefs_destroy(private_data unsafe.Pointer) {
|
||||
func showHelp(args *fuseArgs) {
|
||||
executableName := sharefsName
|
||||
if args.argc > 0 {
|
||||
executableName = path.Base(C.GoString(*args.argv))
|
||||
executableName = filepath.Base(C.GoString(*args.argv))
|
||||
} else if name, err := os.Executable(); err == nil {
|
||||
executableName = path.Base(name)
|
||||
executableName = filepath.Base(name)
|
||||
}
|
||||
|
||||
fmt.Printf("usage: %s [options] <mountpoint>\n\n", executableName)
|
||||
@@ -470,13 +469,14 @@ func _main(s ...string) (exitCode int) {
|
||||
os.NewFile(uintptr(C.fuse_session_fd(se)), "fuse"),
|
||||
))
|
||||
|
||||
var setupWriter io.WriteCloser
|
||||
if fd, w, err := container.Setup(&z.ExtraFiles); err != nil {
|
||||
var setupPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else {
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(fd))
|
||||
setupWriter = w
|
||||
z.Args = append(z.Args, "-osetup="+strconv.Itoa(3+len(z.ExtraFiles)))
|
||||
z.ExtraFiles = append(z.ExtraFiles, r)
|
||||
setupPipe[0], setupPipe[1] = r, w
|
||||
}
|
||||
|
||||
if err := z.Start(); err != nil {
|
||||
@@ -487,6 +487,9 @@ func _main(s ...string) (exitCode int) {
|
||||
}
|
||||
return 5
|
||||
}
|
||||
if err := setupPipe[0].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
if err := z.Serve(); err != nil {
|
||||
if m, ok := message.GetMessage(err); ok {
|
||||
log.Println(m)
|
||||
@@ -496,10 +499,10 @@ func _main(s ...string) (exitCode int) {
|
||||
return 5
|
||||
}
|
||||
|
||||
if err := gob.NewEncoder(setupWriter).Encode(&setup); err != nil {
|
||||
if err := gob.NewEncoder(setupPipe[1]).Encode(&setup); err != nil {
|
||||
log.Println(err)
|
||||
return 5
|
||||
} else if err = setupWriter.Close(); err != nil {
|
||||
} else if err = setupPipe[1].Close(); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
|
||||
|
||||
122
cmd/sharefs/test/raceattr.go
Normal file
122
cmd/sharefs/test/raceattr.go
Normal file
@@ -0,0 +1,122 @@
|
||||
//go:build raceattr
|
||||
|
||||
// The raceattr program reproduces vfs inode file attribute race.
|
||||
//
|
||||
// Even though libfuse high-level API presents the address of a struct stat
|
||||
// alongside struct fuse_context, file attributes are actually inherent to the
|
||||
// inode, instead of the specific call from userspace. The kernel implementation
|
||||
// in fs/fuse/xattr.c appears to make stale data in the inode (set by a previous
|
||||
// call) impossible or very unlikely to reach userspace via the stat family of
|
||||
// syscalls. However, when using default_permissions to have the VFS check
|
||||
// permissions, this race still happens, despite the resulting struct stat being
|
||||
// correct when overriding the check via capabilities otherwise.
|
||||
//
|
||||
// This program reproduces the failure, but because of its continuous nature, it
|
||||
// is provided independent of the vm integration test suite.
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
"runtime"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
func newStatAs(
|
||||
ctx context.Context, cancel context.CancelFunc,
|
||||
n *atomic.Uint64, ok *atomic.Bool,
|
||||
uid uint32, pathname string,
|
||||
continuous bool,
|
||||
) func() {
|
||||
return func() {
|
||||
runtime.LockOSThread()
|
||||
defer cancel()
|
||||
|
||||
if _, _, errno := syscall.Syscall(
|
||||
syscall.SYS_SETUID, uintptr(uid),
|
||||
0, 0,
|
||||
); errno != 0 {
|
||||
cancel()
|
||||
log.Printf("cannot set uid to %d: %s", uid, errno)
|
||||
}
|
||||
|
||||
var stat syscall.Stat_t
|
||||
for {
|
||||
if ctx.Err() != nil {
|
||||
return
|
||||
}
|
||||
|
||||
if err := syscall.Lstat(pathname, &stat); err != nil {
|
||||
// SHAREFS_PERM_DIR not world executable, or
|
||||
// SHAREFS_PERM_REG not world readable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("uid %d: %v", uid, err)
|
||||
} else if stat.Uid != uid {
|
||||
// appears to be unreachable
|
||||
if !continuous {
|
||||
cancel()
|
||||
}
|
||||
ok.Store(true)
|
||||
log.Printf("got uid %d instead of %d", stat.Uid, uid)
|
||||
}
|
||||
n.Add(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("raceattr: ")
|
||||
|
||||
p := flag.String("target", "/sdcard/raceattr", "pathname of test file")
|
||||
u0 := flag.Int("uid0", 1<<10-1, "first uid")
|
||||
u1 := flag.Int("uid1", 1<<10-2, "second uid")
|
||||
count := flag.Int("count", 1, "threads per uid")
|
||||
continuous := flag.Bool("continuous", false, "keep running even after reproduce")
|
||||
flag.Parse()
|
||||
|
||||
if os.Geteuid() != 0 {
|
||||
log.Fatal("this program must run as root")
|
||||
}
|
||||
|
||||
ctx, cancel := signal.NotifyContext(
|
||||
context.Background(),
|
||||
syscall.SIGINT,
|
||||
syscall.SIGTERM,
|
||||
syscall.SIGHUP,
|
||||
)
|
||||
|
||||
if err := os.WriteFile(*p, nil, 0); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
|
||||
n atomic.Uint64
|
||||
ok atomic.Bool
|
||||
)
|
||||
|
||||
if *count < 1 {
|
||||
*count = 1
|
||||
}
|
||||
for range *count {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u0), *p, *continuous))
|
||||
if *u1 >= 0 {
|
||||
wg.Go(newStatAs(ctx, cancel, &n, &ok, uint32(*u1), *p, *continuous))
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
if !*continuous && ok.Load() {
|
||||
log.Printf("reproduced after %d calls", n.Load())
|
||||
}
|
||||
}
|
||||
@@ -21,6 +21,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -28,9 +29,6 @@ const (
|
||||
// CancelSignal is the signal expected by container init on context cancel.
|
||||
// A custom [Container.Cancel] function must eventually deliver this signal.
|
||||
CancelSignal = SIGUSR2
|
||||
|
||||
// Timeout for writing initParams to Container.setup.
|
||||
initSetupTimeout = 5 * time.Second
|
||||
)
|
||||
|
||||
type (
|
||||
@@ -53,7 +51,7 @@ type (
|
||||
ExtraFiles []*os.File
|
||||
|
||||
// Write end of a pipe connected to the init to deliver [Params].
|
||||
setup *os.File
|
||||
setup [2]*os.File
|
||||
// Cancels the context passed to the underlying cmd.
|
||||
cancel context.CancelFunc
|
||||
// Closed after Wait returns. Keeps the spawning thread alive.
|
||||
@@ -287,14 +285,16 @@ func (p *Container) Start() error {
|
||||
}
|
||||
|
||||
// place setup pipe before user supplied extra files, this is later restored by init
|
||||
if fd, f, err := Setup(&p.cmd.ExtraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: err,
|
||||
}
|
||||
} else {
|
||||
p.setup = f
|
||||
fd := 3 + len(p.cmd.ExtraFiles)
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, r)
|
||||
p.setup[0], p.setup[1] = r, w
|
||||
p.cmd.Env = []string{setupEnv + "=" + strconv.Itoa(fd)}
|
||||
}
|
||||
p.cmd.ExtraFiles = append(p.cmd.ExtraFiles, p.ExtraFiles...)
|
||||
@@ -308,7 +308,7 @@ func (p *Container) Start() error {
|
||||
done <- func() error {
|
||||
// PR_SET_NO_NEW_PRIVS: thread-directed but acts on all processes
|
||||
// created from the calling thread
|
||||
if err := SetNoNewPrivs(); err != nil {
|
||||
if err := setNoNewPrivs(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "prctl(PR_SET_NO_NEW_PRIVS)",
|
||||
@@ -318,15 +318,17 @@ func (p *Container) Start() error {
|
||||
|
||||
// landlock: depends on per-thread state but acts on a process group
|
||||
{
|
||||
rulesetAttr := &RulesetAttr{Scoped: LANDLOCK_SCOPE_SIGNAL}
|
||||
rulesetAttr := &landlock.RulesetAttr{
|
||||
Scoped: landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}
|
||||
if !p.HostAbstract {
|
||||
rulesetAttr.Scoped |= LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
rulesetAttr.Scoped |= landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET
|
||||
}
|
||||
|
||||
if abi, err := LandlockGetABI(); err != nil {
|
||||
if p.HostAbstract {
|
||||
if abi, err := landlock.GetABI(); err != nil {
|
||||
if p.HostAbstract || !p.HostNet {
|
||||
// landlock can be skipped here as it restricts access
|
||||
// to resources already covered by namespaces (pid)
|
||||
// to resources already covered by namespaces (pid, net)
|
||||
goto landlockOut
|
||||
}
|
||||
return &StartError{Step: "get landlock ABI", Err: err}
|
||||
@@ -352,7 +354,7 @@ func (p *Container) Start() error {
|
||||
}
|
||||
} else {
|
||||
p.msg.Verbosef("enforcing landlock ruleset %s", rulesetAttr)
|
||||
if err = LandlockRestrictSelf(rulesetFd, 0); err != nil {
|
||||
if err = landlock.RestrictSelf(rulesetFd, 0); err != nil {
|
||||
_ = Close(rulesetFd)
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
@@ -428,24 +430,33 @@ func (p *Container) Start() error {
|
||||
// Serve serves [Container.Params] to the container init.
|
||||
//
|
||||
// Serve must only be called once.
|
||||
func (p *Container) Serve() error {
|
||||
if p.setup == nil {
|
||||
func (p *Container) Serve() (err error) {
|
||||
if p.setup[0] == nil || p.setup[1] == nil {
|
||||
panic("invalid serve")
|
||||
}
|
||||
|
||||
setup := p.setup
|
||||
p.setup = nil
|
||||
if err := setup.SetDeadline(time.Now().Add(initSetupTimeout)); err != nil {
|
||||
done := make(chan struct{})
|
||||
defer func() {
|
||||
if closeErr := p.setup[1].Close(); err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
close(done)
|
||||
p.setup[0], p.setup[1] = nil, nil
|
||||
}()
|
||||
if err = p.setup[0].Close(); err != nil {
|
||||
return &StartError{
|
||||
Fatal: true,
|
||||
Step: "set init pipe deadline",
|
||||
Step: "close read end of init pipe",
|
||||
Err: err,
|
||||
Passthrough: true,
|
||||
}
|
||||
}
|
||||
|
||||
if p.Path == nil {
|
||||
p.cancel()
|
||||
return &StartError{
|
||||
Step: "invalid executable pathname",
|
||||
Err: EINVAL,
|
||||
@@ -461,18 +472,27 @@ func (p *Container) Serve() error {
|
||||
p.SeccompRules = make([]std.NativeRule, 0)
|
||||
}
|
||||
|
||||
err := gob.NewEncoder(setup).Encode(&initParams{
|
||||
t := time.Now().UTC()
|
||||
go func(f *os.File) {
|
||||
select {
|
||||
case <-p.ctx.Done():
|
||||
if cancelErr := f.SetWriteDeadline(t); cancelErr != nil {
|
||||
p.msg.Verbose(err)
|
||||
}
|
||||
|
||||
case <-done:
|
||||
p.msg.Verbose("setup payload took", time.Since(t))
|
||||
return
|
||||
}
|
||||
}(p.setup[1])
|
||||
|
||||
return gob.NewEncoder(p.setup[1]).Encode(&initParams{
|
||||
p.Params,
|
||||
Getuid(),
|
||||
Getgid(),
|
||||
len(p.ExtraFiles),
|
||||
p.msg.IsVerbose(),
|
||||
})
|
||||
_ = setup.Close()
|
||||
if err != nil {
|
||||
p.cancel()
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Wait blocks until the container init process to exit and releases any
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"strings"
|
||||
"syscall"
|
||||
"testing"
|
||||
"time"
|
||||
_ "unsafe" // for go:linkname
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/command"
|
||||
@@ -26,6 +26,9 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/ldd"
|
||||
"hakurei.app/message"
|
||||
"hakurei.app/vfs"
|
||||
@@ -84,9 +87,9 @@ func TestStartError(t *testing.T) {
|
||||
{"params env", &container.StartError{
|
||||
Fatal: true,
|
||||
Step: "set up params stream",
|
||||
Err: container.ErrReceiveEnv,
|
||||
Err: params.ErrReceiveEnv,
|
||||
}, "set up params stream: environment variable not set",
|
||||
container.ErrReceiveEnv, syscall.EBADF,
|
||||
params.ErrReceiveEnv, syscall.EBADF,
|
||||
"cannot set up params stream: environment variable not set"},
|
||||
|
||||
{"params", &container.StartError{
|
||||
@@ -231,6 +234,9 @@ func earlyMnt(mnt ...*vfs.MountInfoEntry) func(*testing.T, context.Context) []*v
|
||||
return func(*testing.T, context.Context) []*vfs.MountInfoEntry { return mnt }
|
||||
}
|
||||
|
||||
//go:linkname toHost hakurei.app/container.toHost
|
||||
func toHost(name string) string
|
||||
|
||||
var containerTestCases = []struct {
|
||||
name string
|
||||
filter bool
|
||||
@@ -330,13 +336,15 @@ var containerTestCases = []struct {
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||
"rw,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
"rw"+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
",upperdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
||||
toHost(ctx.Value(testVal("upper")).(*check.Absolute).String())+
|
||||
",workdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
||||
toHost(ctx.Value(testVal("work")).(*check.Absolute).String())+
|
||||
",redirect_dir=nofollow,uuid=on,userxattr"),
|
||||
}
|
||||
},
|
||||
@@ -386,9 +394,11 @@ var containerTestCases = []struct {
|
||||
func(t *testing.T, ctx context.Context) []*vfs.MountInfoEntry {
|
||||
return []*vfs.MountInfoEntry{
|
||||
ent("/", hst.PrivateTmp, "rw", "overlay", "overlay",
|
||||
"ro,lowerdir="+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower0")).(*check.Absolute).String())+":"+
|
||||
container.InternalToHostOvlEscape(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
"ro"+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower0")).(*check.Absolute).String())+
|
||||
",lowerdir+="+
|
||||
toHost(ctx.Value(testVal("lower1")).(*check.Absolute).String())+
|
||||
",redirect_dir=nofollow,userxattr"),
|
||||
}
|
||||
},
|
||||
@@ -436,11 +446,8 @@ func TestContainer(t *testing.T) {
|
||||
wantOps, wantOpsCtx := tc.ops(t)
|
||||
wantMnt := tc.mnt(t, wantOpsCtx)
|
||||
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
defer cancel()
|
||||
|
||||
var libPaths []*check.Absolute
|
||||
c := helperNewContainerLibPaths(ctx, &libPaths, "container", strconv.Itoa(i))
|
||||
c := helperNewContainerLibPaths(t.Context(), &libPaths, "container", strconv.Itoa(i))
|
||||
c.Uid = tc.uid
|
||||
c.Gid = tc.gid
|
||||
c.Hostname = hostnameFromTestCase(tc.name)
|
||||
@@ -450,7 +457,6 @@ func TestContainer(t *testing.T) {
|
||||
} else {
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
}
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
*c.Ops = append(*c.Ops, *wantOps...)
|
||||
c.SeccompRules = tc.rules
|
||||
c.SeccompFlags = tc.flags | seccomp.AllowMultiarch
|
||||
@@ -458,6 +464,15 @@ func TestContainer(t *testing.T) {
|
||||
c.SeccompDisable = !tc.filter
|
||||
c.RetainSession = tc.session
|
||||
c.HostNet = tc.net
|
||||
if info.CanDegrade {
|
||||
if _, err := landlock.GetABI(); err != nil {
|
||||
if !errors.Is(err, syscall.ENOSYS) {
|
||||
t.Fatalf("LandlockGetABI: error = %v", err)
|
||||
}
|
||||
c.HostAbstract = true
|
||||
t.Log("Landlock LSM is unavailable, enabling HostAbstract")
|
||||
}
|
||||
}
|
||||
|
||||
c.
|
||||
Readonly(check.MustAbs(pathReadonly), 0755).
|
||||
@@ -553,11 +568,10 @@ func testContainerCancel(
|
||||
) func(t *testing.T) {
|
||||
return func(t *testing.T) {
|
||||
t.Parallel()
|
||||
ctx, cancel := context.WithTimeout(t.Context(), helperDefaultTimeout)
|
||||
ctx, cancel := context.WithCancel(t.Context())
|
||||
|
||||
c := helperNewContainer(ctx, "block")
|
||||
c.Stdout, c.Stderr = os.Stdout, os.Stderr
|
||||
c.WaitDelay = helperDefaultTimeout
|
||||
if containerExtra != nil {
|
||||
containerExtra(c)
|
||||
}
|
||||
@@ -738,8 +752,7 @@ func init() {
|
||||
const (
|
||||
envDoCheck = "HAKUREI_TEST_DO_CHECK"
|
||||
|
||||
helperDefaultTimeout = 5 * time.Second
|
||||
helperInnerPath = "/usr/bin/helper"
|
||||
helperInnerPath = "/usr/bin/helper"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/netlink"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -56,7 +57,7 @@ type syscallDispatcher interface {
|
||||
// isatty provides [Isatty].
|
||||
isatty(fd int) bool
|
||||
// receive provides [Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// bindMount provides procPaths.bindMount.
|
||||
bindMount(msg message.Msg, source, target string, flags uintptr) error
|
||||
@@ -64,6 +65,8 @@ type syscallDispatcher interface {
|
||||
remount(msg message.Msg, target string, flags uintptr) error
|
||||
// mountTmpfs provides mountTmpfs.
|
||||
mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error
|
||||
// mountOverlay provides mountOverlay.
|
||||
mountOverlay(target string, options [][2]string) error
|
||||
// ensureFile provides ensureFile.
|
||||
ensureFile(name string, perm, pperm os.FileMode) error
|
||||
// mustLoopback provides mustLoopback.
|
||||
@@ -147,7 +150,7 @@ func (direct) lockOSThread() { runtime.LockOSThread() }
|
||||
|
||||
func (direct) setPtracer(pid uintptr) error { return ext.SetPtracer(pid) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) setNoNewPrivs() error { return SetNoNewPrivs() }
|
||||
func (direct) setNoNewPrivs() error { return setNoNewPrivs() }
|
||||
|
||||
func (direct) lastcap(msg message.Msg) uintptr { return LastCap(msg) }
|
||||
func (direct) capset(hdrp *capHeader, datap *[2]capData) error { return capset(hdrp, datap) }
|
||||
@@ -155,8 +158,8 @@ func (direct) capBoundingSetDrop(cap uintptr) error { return capBound
|
||||
func (direct) capAmbientClearAll() error { return capAmbientClearAll() }
|
||||
func (direct) capAmbientRaise(cap uintptr) error { return capAmbientRaise(cap) }
|
||||
func (direct) isatty(fd int) bool { return ext.Isatty(fd) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) bindMount(msg message.Msg, source, target string, flags uintptr) error {
|
||||
@@ -168,6 +171,9 @@ func (direct) remount(msg message.Msg, target string, flags uintptr) error {
|
||||
func (k direct) mountTmpfs(fsname, target string, flags uintptr, size int, perm os.FileMode) error {
|
||||
return mountTmpfs(k, fsname, target, flags, size, perm)
|
||||
}
|
||||
func (k direct) mountOverlay(target string, options [][2]string) error {
|
||||
return mountOverlay(target, options)
|
||||
}
|
||||
func (direct) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
return ensureFile(name, perm, pperm)
|
||||
}
|
||||
@@ -179,7 +185,7 @@ func (direct) mustLoopback(ctx context.Context, msg message.Msg) {
|
||||
lo = ifi.Index
|
||||
}
|
||||
|
||||
c, err := netlink.DialRoute()
|
||||
c, err := netlink.DialRoute(0)
|
||||
if err != nil {
|
||||
msg.GetLogger().Fatalln(err)
|
||||
}
|
||||
|
||||
@@ -390,7 +390,7 @@ func (k *kstub) isatty(fd int) bool {
|
||||
return expect.Ret.(bool)
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
|
||||
@@ -408,10 +408,17 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// avoid changing test cases
|
||||
var fdpComp *uintptr
|
||||
if fdp != nil {
|
||||
fdpComp = new(uintptr(*fdp))
|
||||
}
|
||||
|
||||
err = expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0),
|
||||
stub.CheckArgReflect(k.Stub, "e", e, 1),
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdp, 2))
|
||||
stub.CheckArgReflect(k.Stub, "fdp", fdpComp, 2))
|
||||
|
||||
// 3 is unused so stores params
|
||||
if expect.Args[3] != nil {
|
||||
@@ -426,7 +433,7 @@ func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error
|
||||
if expect.Args[4] != nil {
|
||||
if v, ok := expect.Args[4].(uintptr); ok && v >= 3 {
|
||||
if fdp != nil {
|
||||
*fdp = v
|
||||
*fdp = int(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -461,6 +468,14 @@ func (k *kstub) mountTmpfs(fsname, target string, flags uintptr, size int, perm
|
||||
stub.CheckArg(k.Stub, "perm", perm, 4))
|
||||
}
|
||||
|
||||
func (k *kstub) mountOverlay(target string, options [][2]string) error {
|
||||
k.Helper()
|
||||
return k.Expects("mountOverlay").Error(
|
||||
stub.CheckArg(k.Stub, "target", target, 0),
|
||||
stub.CheckArgReflect(k.Stub, "options", options, 1),
|
||||
)
|
||||
}
|
||||
|
||||
func (k *kstub) ensureFile(name string, perm, pperm os.FileMode) error {
|
||||
k.Helper()
|
||||
return k.Expects("ensureFile").Error(
|
||||
|
||||
@@ -118,6 +118,10 @@ func errnoFallback(op, path string, err error) (syscall.Errno, *os.PathError) {
|
||||
|
||||
// mount wraps syscall.Mount for error handling.
|
||||
func mount(source, target, fstype string, flags uintptr, data string) error {
|
||||
if max(len(source), len(target), len(data))+1 > os.Getpagesize() {
|
||||
return &MountError{source, target, fstype, flags, data, syscall.ENOMEM}
|
||||
}
|
||||
|
||||
err := syscall.Mount(source, target, fstype, flags, data)
|
||||
if err == nil {
|
||||
return nil
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/signal"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
@@ -19,6 +19,7 @@ import (
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -147,35 +148,33 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var (
|
||||
params initParams
|
||||
closeSetup func() error
|
||||
setupFd uintptr
|
||||
offsetSetup int
|
||||
param initParams
|
||||
closeSetup func() error
|
||||
setupFd int
|
||||
)
|
||||
if f, err := k.receive(setupEnv, ¶ms, &setupFd); err != nil {
|
||||
if f, err := k.receive(setupEnv, ¶m, &setupFd); err != nil {
|
||||
if errors.Is(err, EBADF) {
|
||||
k.fatal(msg, "invalid setup descriptor")
|
||||
}
|
||||
if errors.Is(err, ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(msg, setupEnv+" not set")
|
||||
}
|
||||
|
||||
k.fatalf(msg, "cannot decode init setup payload: %v", err)
|
||||
} else {
|
||||
if params.Ops == nil {
|
||||
if param.Ops == nil {
|
||||
k.fatal(msg, "invalid setup parameters")
|
||||
}
|
||||
if params.ParentPerm == 0 {
|
||||
params.ParentPerm = 0755
|
||||
if param.ParentPerm == 0 {
|
||||
param.ParentPerm = 0755
|
||||
}
|
||||
|
||||
msg.SwapVerbose(params.Verbose)
|
||||
msg.SwapVerbose(param.Verbose)
|
||||
msg.Verbose("received setup parameters")
|
||||
closeSetup = f
|
||||
offsetSetup = int(setupFd + 1)
|
||||
}
|
||||
|
||||
if !params.HostNet {
|
||||
if !param.HostNet {
|
||||
ctx, cancel := signal.NotifyContext(context.Background(), CancelSignal,
|
||||
os.Interrupt, SIGTERM, SIGQUIT)
|
||||
defer cancel() // for panics
|
||||
@@ -188,7 +187,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot set SUID_DUMP_USER: %v", err)
|
||||
}
|
||||
if err := k.writeFile(fhs.Proc+"self/uid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Uid)+" "+strconv.Itoa(params.HostUid)+" 1\n"...),
|
||||
append([]byte{}, strconv.Itoa(param.Uid)+" "+strconv.Itoa(param.HostUid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -198,7 +197,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
if err := k.writeFile(fhs.Proc+"self/gid_map",
|
||||
append([]byte{}, strconv.Itoa(params.Gid)+" "+strconv.Itoa(params.HostGid)+" 1\n"...),
|
||||
append([]byte{}, strconv.Itoa(param.Gid)+" "+strconv.Itoa(param.HostGid)+" 1\n"...),
|
||||
0); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -207,8 +206,8 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
oldmask := k.umask(0)
|
||||
if params.Hostname != "" {
|
||||
if err := k.sethostname([]byte(params.Hostname)); err != nil {
|
||||
if param.Hostname != "" {
|
||||
if err := k.sethostname([]byte(param.Hostname)); err != nil {
|
||||
k.fatalf(msg, "cannot set hostname: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -221,7 +220,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶ms.Params, Msg: msg, Context: ctx}
|
||||
state := &setupState{process: make(map[int]WaitStatus), Params: ¶m.Params, Msg: msg, Context: ctx}
|
||||
defer cancel()
|
||||
|
||||
/* early is called right before pivot_root into intermediate root;
|
||||
@@ -229,7 +228,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
difficult to obtain via library functions after pivot_root, and
|
||||
implementations are expected to avoid changing the state of the mount
|
||||
namespace */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
if op == nil || !op.Valid() {
|
||||
k.fatalf(msg, "invalid op at index %d", i)
|
||||
}
|
||||
@@ -272,7 +271,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
step sets up the container filesystem, and implementations are expected to
|
||||
keep the host root and sysroot mount points intact but otherwise can do
|
||||
whatever they need to. Calling chdir is allowed but discouraged. */
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if prefix, ok := op.prefix(); ok {
|
||||
msg.Verbosef("%s %s", prefix, op)
|
||||
@@ -328,7 +327,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot clear the ambient capability set: %v", err)
|
||||
}
|
||||
for i := uintptr(0); i <= lastcap; i++ {
|
||||
if params.Privileged && i == CAP_SYS_ADMIN {
|
||||
if param.Privileged && i == CAP_SYS_ADMIN {
|
||||
continue
|
||||
}
|
||||
if err := k.capBoundingSetDrop(i); err != nil {
|
||||
@@ -337,7 +336,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
}
|
||||
|
||||
var keep [2]uint32
|
||||
if params.Privileged {
|
||||
if param.Privileged {
|
||||
keep[capToIndex(CAP_SYS_ADMIN)] |= capToMask(CAP_SYS_ADMIN)
|
||||
|
||||
if err := k.capAmbientRaise(CAP_SYS_ADMIN); err != nil {
|
||||
@@ -351,13 +350,13 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot capset: %v", err)
|
||||
}
|
||||
|
||||
if !params.SeccompDisable {
|
||||
rules := params.SeccompRules
|
||||
if !param.SeccompDisable {
|
||||
rules := param.SeccompRules
|
||||
if len(rules) == 0 { // non-empty rules slice always overrides presets
|
||||
msg.Verbosef("resolving presets %#x", params.SeccompPresets)
|
||||
rules = seccomp.Preset(params.SeccompPresets, params.SeccompFlags)
|
||||
msg.Verbosef("resolving presets %#x", param.SeccompPresets)
|
||||
rules = seccomp.Preset(param.SeccompPresets, param.SeccompFlags)
|
||||
}
|
||||
if err := k.seccompLoad(rules, params.SeccompFlags); err != nil {
|
||||
if err := k.seccompLoad(rules, param.SeccompFlags); err != nil {
|
||||
// this also indirectly asserts PR_SET_NO_NEW_PRIVS
|
||||
k.fatalf(msg, "cannot load syscall filter: %v", err)
|
||||
}
|
||||
@@ -366,10 +365,10 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
msg.Verbose("syscall filter not configured")
|
||||
}
|
||||
|
||||
extraFiles := make([]*os.File, params.Count)
|
||||
extraFiles := make([]*os.File, param.Count)
|
||||
for i := range extraFiles {
|
||||
// setup fd is placed before all extra files
|
||||
extraFiles[i] = k.newFile(uintptr(offsetSetup+i), "extra file "+strconv.Itoa(i))
|
||||
extraFiles[i] = k.newFile(uintptr(setupFd+1+i), "extra file "+strconv.Itoa(i))
|
||||
}
|
||||
k.umask(oldmask)
|
||||
|
||||
@@ -447,7 +446,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
|
||||
// called right before startup of initial process, all state changes to the
|
||||
// current process is prohibited during late
|
||||
for i, op := range *params.Ops {
|
||||
for i, op := range *param.Ops {
|
||||
// ops already checked during early setup
|
||||
if err := op.late(state, k); err != nil {
|
||||
if m, ok := messageFromError(err); ok {
|
||||
@@ -468,14 +467,14 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
k.fatalf(msg, "cannot close setup pipe: %v", err)
|
||||
}
|
||||
|
||||
cmd := exec.Command(params.Path.String())
|
||||
cmd := exec.Command(param.Path.String())
|
||||
cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
|
||||
cmd.Args = params.Args
|
||||
cmd.Env = params.Env
|
||||
cmd.Args = param.Args
|
||||
cmd.Env = param.Env
|
||||
cmd.ExtraFiles = extraFiles
|
||||
cmd.Dir = params.Dir.String()
|
||||
cmd.Dir = param.Dir.String()
|
||||
|
||||
msg.Verbosef("starting initial process %s", params.Path)
|
||||
msg.Verbosef("starting initial process %s", param.Path)
|
||||
if err := k.start(cmd); err != nil {
|
||||
k.fatalf(msg, "%v", err)
|
||||
}
|
||||
@@ -493,9 +492,9 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
for {
|
||||
select {
|
||||
case s := <-sig:
|
||||
if s == CancelSignal && params.ForwardCancel && cmd.Process != nil {
|
||||
if s == CancelSignal && param.ForwardCancel && cmd.Process != nil {
|
||||
msg.Verbose("forwarding context cancellation")
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil {
|
||||
if err := k.signal(cmd, os.Interrupt); err != nil && !errors.Is(err, os.ErrProcessDone) {
|
||||
k.printf(msg, "cannot forward cancellation: %v", err)
|
||||
}
|
||||
continue
|
||||
@@ -525,7 +524,7 @@ func initEntrypoint(k syscallDispatcher, msg message.Msg) {
|
||||
cancel()
|
||||
|
||||
// start timeout early
|
||||
go func() { time.Sleep(params.AdoptWaitDelay); close(timeout) }()
|
||||
go func() { time.Sleep(param.AdoptWaitDelay); close(timeout) }()
|
||||
|
||||
// close initial process files; this also keeps them alive
|
||||
for _, f := range extraFiles {
|
||||
@@ -569,7 +568,7 @@ func TryArgv0(msg message.Msg) {
|
||||
msg = message.New(log.Default())
|
||||
}
|
||||
|
||||
if len(os.Args) > 0 && path.Base(os.Args[0]) == initName {
|
||||
if len(os.Args) > 0 && filepath.Base(os.Args[0]) == initName {
|
||||
Init(msg)
|
||||
msg.BeforeExit()
|
||||
os.Exit(0)
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container/seccomp"
|
||||
"hakurei.app/container/std"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -40,7 +41,7 @@ func TestInitEntrypoint(t *testing.T) {
|
||||
call("lockOSThread", stub.ExpectArgs{}, nil, nil),
|
||||
call("getpid", stub.ExpectArgs{}, 1, nil),
|
||||
call("setPtracer", stub.ExpectArgs{uintptr(0)}, nil, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SETUP", new(initParams), new(uintptr)}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SETUP not set"}}, nil, nil),
|
||||
},
|
||||
}, nil},
|
||||
|
||||
@@ -3,7 +3,7 @@ package container
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
. "syscall"
|
||||
|
||||
"hakurei.app/check"
|
||||
@@ -46,7 +46,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
|
||||
for _, name := range []string{"null", "zero", "full", "random", "urandom", "tty"} {
|
||||
targetPath := path.Join(target, name)
|
||||
targetPath := filepath.Join(target, name)
|
||||
if err := k.ensureFile(targetPath, 0444, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -62,7 +62,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
for i, name := range []string{"stdin", "stdout", "stderr"} {
|
||||
if err := k.symlink(
|
||||
fhs.Proc+"self/fd/"+string(rune(i+'0')),
|
||||
path.Join(target, name),
|
||||
filepath.Join(target, name),
|
||||
); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -72,13 +72,13 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
{fhs.Proc + "kcore", "core"},
|
||||
{"pts/ptmx", "ptmx"},
|
||||
} {
|
||||
if err := k.symlink(pair[0], path.Join(target, pair[1])); err != nil {
|
||||
if err := k.symlink(pair[0], filepath.Join(target, pair[1])); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
devShmPath := path.Join(target, "shm")
|
||||
devPtsPath := path.Join(target, "pts")
|
||||
devShmPath := filepath.Join(target, "shm")
|
||||
devPtsPath := filepath.Join(target, "pts")
|
||||
for _, name := range []string{devShmPath, devPtsPath} {
|
||||
if err := k.mkdir(name, state.ParentPerm); err != nil {
|
||||
return err
|
||||
@@ -92,7 +92,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
|
||||
if state.RetainSession {
|
||||
if k.isatty(Stdout) {
|
||||
consolePath := path.Join(target, "console")
|
||||
consolePath := filepath.Join(target, "console")
|
||||
if err := k.ensureFile(consolePath, 0444, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
@@ -110,7 +110,7 @@ func (d *MountDevOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
|
||||
if d.Mqueue {
|
||||
mqueueTarget := path.Join(target, "mqueue")
|
||||
mqueueTarget := filepath.Join(target, "mqueue")
|
||||
if err := k.mkdir(mqueueTarget, state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -4,9 +4,9 @@ import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
)
|
||||
|
||||
@@ -150,7 +150,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if v, err := k.evalSymlinks(o.Upper.String()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
o.upper = check.EscapeOverlayDataSegment(toHost(v))
|
||||
o.upper = toHost(v)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -158,7 +158,7 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if v, err := k.evalSymlinks(o.Work.String()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
o.work = check.EscapeOverlayDataSegment(toHost(v))
|
||||
o.work = toHost(v)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,12 +168,39 @@ func (o *MountOverlayOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if v, err := k.evalSymlinks(a.String()); err != nil {
|
||||
return err
|
||||
} else {
|
||||
o.lower[i] = check.EscapeOverlayDataSegment(toHost(v))
|
||||
o.lower[i] = toHost(v)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mountOverlay sets up an overlay mount via [ext.FS].
|
||||
func mountOverlay(target string, options [][2]string) error {
|
||||
fs, err := ext.OpenFS(SourceOverlay, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err = fs.SetString("source", SourceOverlay); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
for _, option := range options {
|
||||
if err = fs.SetString(option[0], option[1]); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
}
|
||||
if err = fs.SetFlag(OptionOverlayUserxattr); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
if err = fs.Mount(target, 0); err != nil {
|
||||
_ = fs.Close()
|
||||
return err
|
||||
}
|
||||
return fs.Close()
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := o.Target.String()
|
||||
if !o.noPrefix {
|
||||
@@ -194,7 +221,7 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
}
|
||||
}
|
||||
|
||||
options := make([]string, 0, 4)
|
||||
options := make([][2]string, 0, 2+len(o.lower))
|
||||
|
||||
if o.upper == zeroString && o.work == zeroString { // readonly
|
||||
if len(o.Lower) < 2 {
|
||||
@@ -205,15 +232,16 @@ func (o *MountOverlayOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
if len(o.Lower) == 0 {
|
||||
return &OverlayArgumentError{OverlayEmptyLower, zeroString}
|
||||
}
|
||||
options = append(options,
|
||||
OptionOverlayUpperdir+"="+o.upper,
|
||||
OptionOverlayWorkdir+"="+o.work)
|
||||
options = append(options, [][2]string{
|
||||
{OptionOverlayUpperdir, o.upper},
|
||||
{OptionOverlayWorkdir, o.work},
|
||||
}...)
|
||||
}
|
||||
for _, lower := range o.lower {
|
||||
options = append(options, [2]string{OptionOverlayLowerdir + "+", lower})
|
||||
}
|
||||
options = append(options,
|
||||
OptionOverlayLowerdir+"="+strings.Join(o.lower, check.SpecialOverlayPath),
|
||||
OptionOverlayUserxattr)
|
||||
|
||||
return k.mount(SourceOverlay, target, FstypeOverlay, 0, strings.Join(options, check.SpecialOverlayOption))
|
||||
return k.mountOverlay(target, options)
|
||||
}
|
||||
|
||||
func (o *MountOverlayOp) late(*setupState, syscallDispatcher) error { return nil }
|
||||
|
||||
@@ -97,13 +97,12 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot", os.FileMode(0705)}, nil, nil),
|
||||
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.upper.*"}, "overlay.upper.32768", nil),
|
||||
call("mkdirTemp", stub.ExpectArgs{"/", "overlay.work.*"}, "overlay.work.32768", nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot", "overlay", uintptr(0), "" +
|
||||
"upperdir=overlay.upper.32768," +
|
||||
"workdir=overlay.work.32768," +
|
||||
"lowerdir=" +
|
||||
`/host/var/lib/planterette/base/debian\:f92c9052:` +
|
||||
`/host/var/lib/planterette/app/org.chromium.Chromium@debian\:f92c9052,` +
|
||||
"userxattr"}, nil, nil),
|
||||
call("mountOverlay", stub.ExpectArgs{"/sysroot", [][2]string{
|
||||
{"upperdir", "overlay.upper.32768"},
|
||||
{"workdir", "overlay.work.32768"},
|
||||
{"lowerdir+", `/host/var/lib/planterette/base/debian:f92c9052`},
|
||||
{"lowerdir+", `/host/var/lib/planterette/app/org.chromium.Chromium@debian:f92c9052`},
|
||||
}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"short lower ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||
@@ -129,11 +128,10 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/nix/store", os.FileMode(0755)}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/nix/store", "overlay", uintptr(0), "" +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/.ro-store:" +
|
||||
"/host/mnt-root/nix/.ro-store0," +
|
||||
"userxattr"}, nil, nil),
|
||||
call("mountOverlay", stub.ExpectArgs{"/nix/store", [][2]string{
|
||||
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
|
||||
}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success ro", &Params{ParentPerm: 0755}, &MountOverlayOp{
|
||||
@@ -147,11 +145,10 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store0"}, "/mnt-root/nix/.ro-store0", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0755)}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/.ro-store:" +
|
||||
"/host/mnt-root/nix/.ro-store0," +
|
||||
"userxattr"}, nil, nil),
|
||||
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||
{"lowerdir+", "/host/mnt-root/nix/.ro-store"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/.ro-store0"},
|
||||
}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"nil lower", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
@@ -219,7 +216,11 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "upperdir=/host/mnt-root/nix/.rw-store/.upper,workdir=/host/mnt-root/nix/.rw-store/.work,lowerdir=/host/mnt-root/nix/ro-store,userxattr"}, nil, stub.UniqueError(0)),
|
||||
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||
}}, nil, stub.UniqueError(0)),
|
||||
}, stub.UniqueError(0)},
|
||||
|
||||
{"success single layer", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
@@ -233,11 +234,11 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store"}, "/mnt-root/nix/ro-store", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||
"lowerdir=/host/mnt-root/nix/ro-store," +
|
||||
"userxattr"}, nil, nil),
|
||||
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||
}}, nil, nil),
|
||||
}, nil},
|
||||
|
||||
{"success", &Params{ParentPerm: 0700}, &MountOverlayOp{
|
||||
@@ -261,16 +262,15 @@ func TestMountOverlayOp(t *testing.T) {
|
||||
call("evalSymlinks", stub.ExpectArgs{"/mnt-root/nix/.ro-store3"}, "/mnt-root/nix/ro-store3", nil),
|
||||
}, nil, []stub.Call{
|
||||
call("mkdirAll", stub.ExpectArgs{"/sysroot/nix/store", os.FileMode(0700)}, nil, nil),
|
||||
call("mount", stub.ExpectArgs{"overlay", "/sysroot/nix/store", "overlay", uintptr(0), "" +
|
||||
"upperdir=/host/mnt-root/nix/.rw-store/.upper," +
|
||||
"workdir=/host/mnt-root/nix/.rw-store/.work," +
|
||||
"lowerdir=" +
|
||||
"/host/mnt-root/nix/ro-store:" +
|
||||
"/host/mnt-root/nix/ro-store0:" +
|
||||
"/host/mnt-root/nix/ro-store1:" +
|
||||
"/host/mnt-root/nix/ro-store2:" +
|
||||
"/host/mnt-root/nix/ro-store3," +
|
||||
"userxattr"}, nil, nil),
|
||||
call("mountOverlay", stub.ExpectArgs{"/sysroot/nix/store", [][2]string{
|
||||
{"upperdir", "/host/mnt-root/nix/.rw-store/.upper"},
|
||||
{"workdir", "/host/mnt-root/nix/.rw-store/.work"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store0"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store1"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store2"},
|
||||
{"lowerdir+", "/host/mnt-root/nix/ro-store3"},
|
||||
}}, nil, nil),
|
||||
}, nil},
|
||||
})
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package container
|
||||
import (
|
||||
"encoding/gob"
|
||||
"fmt"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
@@ -30,7 +30,7 @@ func (l *SymlinkOp) Valid() bool { return l != nil && l.Target != nil && l.LinkN
|
||||
|
||||
func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
if l.Dereference {
|
||||
if !path.IsAbs(l.LinkName) {
|
||||
if !filepath.IsAbs(l.LinkName) {
|
||||
return check.AbsoluteError(l.LinkName)
|
||||
}
|
||||
if name, err := k.readlink(l.LinkName); err != nil {
|
||||
@@ -44,7 +44,7 @@ func (l *SymlinkOp) early(_ *setupState, k syscallDispatcher) error {
|
||||
|
||||
func (l *SymlinkOp) apply(state *setupState, k syscallDispatcher) error {
|
||||
target := toSysroot(l.Target.String())
|
||||
if err := k.mkdirAll(path.Dir(target), state.ParentPerm); err != nil {
|
||||
if err := k.mkdirAll(filepath.Dir(target), state.ParentPerm); err != nil {
|
||||
return err
|
||||
}
|
||||
return k.symlink(l.LinkName, target)
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
package container_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/container"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *container.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(container.RulesetAttr), "0"},
|
||||
{"some", &container.RulesetAttr{Scoped: container.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_MAKE_SYM | container.LANDLOCK_ACCESS_FS_IOCTL_DEV | container.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &container.RulesetAttr{
|
||||
HandledAccessFS: container.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
container.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
container.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
container.LANDLOCK_ACCESS_FS_REFER |
|
||||
container.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
container.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: container.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
container.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: container.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
container.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(container.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -1,47 +0,0 @@
|
||||
package container
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Setup appends the read end of a pipe for setup params transmission and returns its fd.
|
||||
func Setup(extraFiles *[]*os.File) (int, *os.File, error) {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return -1, nil, err
|
||||
} else {
|
||||
fd := 3 + len(*extraFiles)
|
||||
*extraFiles = append(*extraFiles, r)
|
||||
return fd, w, nil
|
||||
}
|
||||
}
|
||||
|
||||
var (
|
||||
ErrReceiveEnv = errors.New("environment variable not set")
|
||||
)
|
||||
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
func Receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
return nil, ErrReceiveEnv
|
||||
} else {
|
||||
if fd, err := strconv.Atoi(s); err != nil {
|
||||
return nil, optionalErrorUnwrap(err)
|
||||
} else {
|
||||
setup = os.NewFile(uintptr(fd), "setup")
|
||||
if setup == nil {
|
||||
return nil, syscall.EDOM
|
||||
}
|
||||
if fdp != nil {
|
||||
*fdp = setup.Fd()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(e)
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
@@ -29,16 +29,16 @@ const (
|
||||
|
||||
func toSysroot(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(sysrootPath, name)
|
||||
return filepath.Join(sysrootPath, name)
|
||||
}
|
||||
|
||||
func toHost(name string) string {
|
||||
name = strings.TrimLeftFunc(name, func(r rune) bool { return r == '/' })
|
||||
return path.Join(hostPath, name)
|
||||
return filepath.Join(hostPath, name)
|
||||
}
|
||||
|
||||
func createFile(name string, perm, pperm os.FileMode, content []byte) error {
|
||||
if err := os.MkdirAll(path.Dir(name), pperm); err != nil {
|
||||
if err := os.MkdirAll(filepath.Dir(name), pperm); err != nil {
|
||||
return err
|
||||
}
|
||||
f, err := os.OpenFile(name, syscall.O_CREAT|syscall.O_EXCL|syscall.O_WRONLY, perm)
|
||||
|
||||
@@ -4,13 +4,12 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/vfs"
|
||||
)
|
||||
|
||||
@@ -50,9 +49,6 @@ func TestToHost(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// InternalToHostOvlEscape exports toHost passed to [check.EscapeOverlayDataSegment].
|
||||
func InternalToHostOvlEscape(s string) string { return check.EscapeOverlayDataSegment(toHost(s)) }
|
||||
|
||||
func TestCreateFile(t *testing.T) {
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
t.Run("mkdir", func(t *testing.T) {
|
||||
@@ -61,7 +57,7 @@ func TestCreateFile(t *testing.T) {
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOENT,
|
||||
}
|
||||
if err := createFile(path.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||
if err := createFile(filepath.Join(Nonexistent, ":3"), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
})
|
||||
@@ -72,7 +68,7 @@ func TestCreateFile(t *testing.T) {
|
||||
Path: "/proc/nonexistent",
|
||||
Err: syscall.ENOENT,
|
||||
}
|
||||
if err := createFile(path.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||
if err := createFile(filepath.Join(Nonexistent), 0644, 0755, nil); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("createFile: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
})
|
||||
@@ -80,7 +76,7 @@ func TestCreateFile(t *testing.T) {
|
||||
|
||||
t.Run("touch", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "empty")
|
||||
pathname := filepath.Join(tempDir, "empty")
|
||||
if err := createFile(pathname, 0644, 0755, nil); err != nil {
|
||||
t.Fatalf("createFile: error = %v", err)
|
||||
}
|
||||
@@ -93,7 +89,7 @@ func TestCreateFile(t *testing.T) {
|
||||
|
||||
t.Run("write", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "zero")
|
||||
pathname := filepath.Join(tempDir, "zero")
|
||||
if err := createFile(pathname, 0644, 0755, []byte{0}); err != nil {
|
||||
t.Fatalf("createFile: error = %v", err)
|
||||
}
|
||||
@@ -107,7 +103,7 @@ func TestCreateFile(t *testing.T) {
|
||||
|
||||
func TestEnsureFile(t *testing.T) {
|
||||
t.Run("create", func(t *testing.T) {
|
||||
if err := ensureFile(path.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
|
||||
if err := ensureFile(filepath.Join(t.TempDir(), "ensure"), 0644, 0755); err != nil {
|
||||
t.Errorf("ensureFile: error = %v", err)
|
||||
}
|
||||
})
|
||||
@@ -115,7 +111,7 @@ func TestEnsureFile(t *testing.T) {
|
||||
t.Run("stat", func(t *testing.T) {
|
||||
t.Run("inaccessible", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "inaccessible")
|
||||
pathname := filepath.Join(tempDir, "inaccessible")
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
t.Fatalf("Create: error = %v", err)
|
||||
} else {
|
||||
@@ -150,7 +146,7 @@ func TestEnsureFile(t *testing.T) {
|
||||
|
||||
t.Run("ensure", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
pathname := path.Join(tempDir, "ensure")
|
||||
pathname := filepath.Join(tempDir, "ensure")
|
||||
if f, err := os.Create(pathname); err != nil {
|
||||
t.Fatalf("Create: error = %v", err)
|
||||
} else {
|
||||
@@ -195,12 +191,12 @@ func TestProcPaths(t *testing.T) {
|
||||
|
||||
t.Run("sample", func(t *testing.T) {
|
||||
tempDir := t.TempDir()
|
||||
if err := os.MkdirAll(path.Join(tempDir, "proc/self"), 0755); err != nil {
|
||||
if err := os.MkdirAll(filepath.Join(tempDir, "proc/self"), 0755); err != nil {
|
||||
t.Fatalf("MkdirAll: error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("clean", func(t *testing.T) {
|
||||
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte(`15 20 0:3 / /proc rw,relatime - proc /proc rw
|
||||
16 20 0:15 / /sys rw,relatime - sysfs /sys rw
|
||||
17 20 0:5 / /dev rw,relatime - devtmpfs udev rw,size=1983516k,nr_inodes=495879,mode=755`), 0644); err != nil {
|
||||
t.Fatalf("WriteFile: error = %v", err)
|
||||
@@ -243,8 +239,8 @@ func TestProcPaths(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("malformed", func(t *testing.T) {
|
||||
path.Join(tempDir, "proc/self/mountinfo")
|
||||
if err := os.WriteFile(path.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
|
||||
filepath.Join(tempDir, "proc/self/mountinfo")
|
||||
if err := os.WriteFile(filepath.Join(tempDir, "proc/self/mountinfo"), []byte{0}, 0644); err != nil {
|
||||
t.Fatalf("WriteFile: error = %v", err)
|
||||
}
|
||||
|
||||
|
||||
@@ -7,8 +7,8 @@ import (
|
||||
"hakurei.app/ext"
|
||||
)
|
||||
|
||||
// SetNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func SetNoNewPrivs() error {
|
||||
// setNoNewPrivs sets the calling thread's no_new_privs attribute.
|
||||
func setNoNewPrivs() error {
|
||||
return ext.Prctl(PR_SET_NO_NEW_PRIVS, 1, 0)
|
||||
}
|
||||
|
||||
|
||||
1
dist/hsurc.default
vendored
1
dist/hsurc.default
vendored
@@ -1 +0,0 @@
|
||||
1000 0
|
||||
12
dist/install.sh
vendored
12
dist/install.sh
vendored
@@ -1,12 +0,0 @@
|
||||
#!/bin/sh
|
||||
cd "$(dirname -- "$0")" || exit 1
|
||||
|
||||
install -vDm0755 "bin/hakurei" "${DESTDIR}/usr/bin/hakurei"
|
||||
install -vDm0755 "bin/sharefs" "${DESTDIR}/usr/bin/sharefs"
|
||||
|
||||
install -vDm4511 "bin/hsu" "${DESTDIR}/usr/bin/hsu"
|
||||
if [ ! -f "${DESTDIR}/etc/hsurc" ]; then
|
||||
install -vDm0400 "hsurc.default" "${DESTDIR}/etc/hsurc"
|
||||
fi
|
||||
|
||||
install -vDm0644 "comp/_hakurei" "${DESTDIR}/usr/share/zsh/site-functions/_hakurei"
|
||||
31
dist/release.sh
vendored
31
dist/release.sh
vendored
@@ -1,31 +0,0 @@
|
||||
#!/bin/sh -e
|
||||
cd "$(dirname -- "$0")/.."
|
||||
VERSION="${HAKUREI_VERSION:-untagged}"
|
||||
pname="hakurei-${VERSION}-$(go env GOARCH)"
|
||||
out="${DESTDIR:-dist}/${pname}"
|
||||
|
||||
echo '# Preparing distribution files.'
|
||||
mkdir -p "${out}"
|
||||
cp -v "README.md" "dist/hsurc.default" "dist/install.sh" "${out}"
|
||||
cp -rv "dist/comp" "${out}"
|
||||
echo
|
||||
|
||||
echo '# Building hakurei.'
|
||||
go generate ./...
|
||||
go build -trimpath -v -o "${out}/bin/" -ldflags "-s -w
|
||||
-buildid= -linkmode external -extldflags=-static
|
||||
-X hakurei.app/internal/info.buildVersion=${VERSION}
|
||||
-X hakurei.app/internal/info.hakureiPath=/usr/bin/hakurei
|
||||
-X hakurei.app/internal/info.hsuPath=/usr/bin/hsu
|
||||
-X main.hakureiPath=/usr/bin/hakurei" ./...
|
||||
echo
|
||||
|
||||
echo '# Testing hakurei.'
|
||||
go test -ldflags='-buildid= -linkmode external -extldflags=-static' ./...
|
||||
echo
|
||||
|
||||
echo '# Creating distribution.'
|
||||
rm -f "${out}.tar.gz" && tar -C "${out}/.." -vczf "${out}.tar.gz" "${pname}"
|
||||
rm -rf "${out}"
|
||||
(cd "${out}/.." && sha512sum "${pname}.tar.gz" > "${pname}.tar.gz.sha512")
|
||||
echo
|
||||
267
ext/fs.go
Normal file
267
ext/fs.go
Normal file
@@ -0,0 +1,267 @@
|
||||
package ext
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
// include/uapi/linux/mount.h
|
||||
|
||||
/*
|
||||
* move_mount() flags.
|
||||
*/
|
||||
const (
|
||||
MOVE_MOUNT_F_SYMLINKS = 1 << iota /* Follow symlinks on from path */
|
||||
MOVE_MOUNT_F_AUTOMOUNTS /* Follow automounts on from path */
|
||||
MOVE_MOUNT_F_EMPTY_PATH /* Empty from path permitted */
|
||||
_
|
||||
MOVE_MOUNT_T_SYMLINKS /* Follow symlinks on to path */
|
||||
MOVE_MOUNT_T_AUTOMOUNTS /* Follow automounts on to path */
|
||||
MOVE_MOUNT_T_EMPTY_PATH /* Empty to path permitted */
|
||||
_
|
||||
MOVE_MOUNT_SET_GROUP /* Set sharing group instead */
|
||||
MOVE_MOUNT_BENEATH /* Mount beneath top mount */
|
||||
)
|
||||
|
||||
/*
|
||||
* fsopen() flags.
|
||||
*/
|
||||
const (
|
||||
FSOPEN_CLOEXEC = 1 << iota
|
||||
)
|
||||
|
||||
/*
|
||||
* fspick() flags.
|
||||
*/
|
||||
const (
|
||||
FSPICK_CLOEXEC = 1 << iota
|
||||
FSPICK_SYMLINK_NOFOLLOW
|
||||
FSPICK_NO_AUTOMOUNT
|
||||
FSPICK_EMPTY_PATH
|
||||
)
|
||||
|
||||
/*
|
||||
* The type of fsconfig() call made.
|
||||
*/
|
||||
const (
|
||||
FSCONFIG_SET_FLAG = iota /* Set parameter, supplying no value */
|
||||
FSCONFIG_SET_STRING /* Set parameter, supplying a string value */
|
||||
FSCONFIG_SET_BINARY /* Set parameter, supplying a binary blob value */
|
||||
FSCONFIG_SET_PATH /* Set parameter, supplying an object by path */
|
||||
FSCONFIG_SET_PATH_EMPTY /* Set parameter, supplying an object by (empty) path */
|
||||
FSCONFIG_SET_FD /* Set parameter, supplying an object by fd */
|
||||
FSCONFIG_CMD_CREATE /* Create new or reuse existing superblock */
|
||||
FSCONFIG_CMD_RECONFIGURE /* Invoke superblock reconfiguration */
|
||||
FSCONFIG_CMD_CREATE_EXCL /* Create new superblock, fail if reusing existing superblock */
|
||||
)
|
||||
|
||||
/*
|
||||
* fsmount() flags.
|
||||
*/
|
||||
const (
|
||||
FSMOUNT_CLOEXEC = 1 << iota
|
||||
)
|
||||
|
||||
/*
|
||||
* Mount attributes.
|
||||
*/
|
||||
const (
|
||||
MOUNT_ATTR_RDONLY = 0x00000001 /* Mount read-only */
|
||||
MOUNT_ATTR_NOSUID = 0x00000002 /* Ignore suid and sgid bits */
|
||||
MOUNT_ATTR_NODEV = 0x00000004 /* Disallow access to device special files */
|
||||
MOUNT_ATTR_NOEXEC = 0x00000008 /* Disallow program execution */
|
||||
MOUNT_ATTR__ATIME = 0x00000070 /* Setting on how atime should be updated */
|
||||
MOUNT_ATTR_RELATIME = 0x00000000 /* - Update atime relative to mtime/ctime. */
|
||||
MOUNT_ATTR_NOATIME = 0x00000010 /* - Do not update access times. */
|
||||
MOUNT_ATTR_STRICTATIME = 0x00000020 /* - Always perform atime updates */
|
||||
MOUNT_ATTR_NODIRATIME = 0x00000080 /* Do not update directory access times */
|
||||
MOUNT_ATTR_IDMAP = 0x00100000 /* Idmap mount to @userns_fd in struct mount_attr. */
|
||||
MOUNT_ATTR_NOSYMFOLLOW = 0x00200000 /* Do not follow symlinks */
|
||||
)
|
||||
|
||||
// FS provides low-level wrappers around the suite of file-descriptor-based
|
||||
// mount facilities in Linux.
|
||||
type FS struct {
|
||||
fd uintptr
|
||||
c runtime.Cleanup
|
||||
}
|
||||
|
||||
// newFS allocates a new [FS] for the specified fd.
|
||||
func newFS(fd uintptr) *FS {
|
||||
fs := FS{fd: fd}
|
||||
fs.c = runtime.AddCleanup(&fs, func(fd uintptr) {
|
||||
_ = syscall.Close(int(fd))
|
||||
}, fd)
|
||||
return &fs
|
||||
}
|
||||
|
||||
// Close closes the underlying filesystem context.
|
||||
func (fs *FS) Close() error {
|
||||
if fs == nil {
|
||||
return syscall.EINVAL
|
||||
}
|
||||
err := syscall.Close(int(fs.fd))
|
||||
fs.c.Stop()
|
||||
return err
|
||||
}
|
||||
|
||||
// OpenFS creates a new filesystem context.
|
||||
func OpenFS(fsname string, flags int) (fs *FS, err error) {
|
||||
var s *byte
|
||||
s, err = syscall.BytePtrFromString(fsname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fd, _, errno := syscall.Syscall(
|
||||
SYS_FSOPEN,
|
||||
uintptr(unsafe.Pointer(s)),
|
||||
uintptr(flags|FSOPEN_CLOEXEC),
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
err = os.NewSyscallError("fsopen", errno)
|
||||
} else {
|
||||
fs = newFS(fd)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// PickFS selects filesystem for reconfiguration.
|
||||
func PickFS(dirfd int, pathname string, flags int) (fs *FS, err error) {
|
||||
var s *byte
|
||||
s, err = syscall.BytePtrFromString(pathname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fd, _, errno := syscall.Syscall(
|
||||
SYS_FSPICK,
|
||||
uintptr(dirfd),
|
||||
uintptr(unsafe.Pointer(s)),
|
||||
uintptr(flags|FSPICK_CLOEXEC),
|
||||
)
|
||||
if errno != 0 {
|
||||
err = os.NewSyscallError("fspick", errno)
|
||||
} else {
|
||||
fs = newFS(fd)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// config configures new or existing filesystem context.
|
||||
func (fs *FS) config(cmd uint, key *byte, value unsafe.Pointer, aux int) (err error) {
|
||||
_, _, errno := syscall.Syscall6(
|
||||
SYS_FSCONFIG,
|
||||
fs.fd,
|
||||
uintptr(cmd),
|
||||
uintptr(unsafe.Pointer(key)),
|
||||
uintptr(value),
|
||||
uintptr(aux),
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
err = os.NewSyscallError("fsconfig", errno)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// SetFlag sets the flag parameter named by key. ([FSCONFIG_SET_FLAG])
|
||||
func (fs *FS) SetFlag(key string) (err error) {
|
||||
var s *byte
|
||||
s, err = syscall.BytePtrFromString(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return fs.config(FSCONFIG_SET_FLAG, s, nil, 0)
|
||||
}
|
||||
|
||||
// SetString sets the string parameter named by key to the value specified by
|
||||
// value. ([FSCONFIG_SET_STRING])
|
||||
func (fs *FS) SetString(key, value string) (err error) {
|
||||
var s0 *byte
|
||||
s0, err = syscall.BytePtrFromString(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var s1 *byte
|
||||
s1, err = syscall.BytePtrFromString(value)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
return fs.config(FSCONFIG_SET_STRING, s0, unsafe.Pointer(s1), 0)
|
||||
}
|
||||
|
||||
// mount instantiates mount object from filesystem context.
|
||||
func (fs *FS) mount(flags, attrFlags int) (fsfd int, err error) {
|
||||
r, _, errno := syscall.Syscall(
|
||||
SYS_FSMOUNT,
|
||||
fs.fd,
|
||||
uintptr(flags|FSMOUNT_CLOEXEC),
|
||||
uintptr(attrFlags),
|
||||
)
|
||||
fsfd = int(r)
|
||||
if errno != 0 {
|
||||
err = os.NewSyscallError("fsmount", errno)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// MoveMount moves or attaches mount object to filesystem.
|
||||
func MoveMount(
|
||||
fromDirfd int,
|
||||
fromPathname string,
|
||||
toDirfd int,
|
||||
toPathname string,
|
||||
flags int,
|
||||
) (err error) {
|
||||
var s0 *byte
|
||||
s0, err = syscall.BytePtrFromString(fromPathname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var s1 *byte
|
||||
s1, err = syscall.BytePtrFromString(toPathname)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
_, _, errno := syscall.Syscall6(
|
||||
SYS_MOVE_MOUNT,
|
||||
uintptr(fromDirfd),
|
||||
uintptr(unsafe.Pointer(s0)),
|
||||
uintptr(toDirfd),
|
||||
uintptr(unsafe.Pointer(s1)),
|
||||
uintptr(flags),
|
||||
0,
|
||||
)
|
||||
if errno != 0 {
|
||||
err = os.NewSyscallError("move_mount", errno)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// Mount attaches the underlying filesystem context to the specified pathname.
|
||||
func (fs *FS) Mount(pathname string, attrFlags int) error {
|
||||
if err := fs.config(FSCONFIG_CMD_CREATE_EXCL, nil, nil, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
fd, err := fs.mount(0, attrFlags)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = MoveMount(
|
||||
fd, "",
|
||||
-1, pathname,
|
||||
MOVE_MOUNT_F_EMPTY_PATH,
|
||||
)
|
||||
closeErr := syscall.Close(fd)
|
||||
if err == nil {
|
||||
err = closeErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
@@ -137,11 +137,10 @@
|
||||
|
||||
CC="musl-clang -O3 -Werror -Qunused-arguments" \
|
||||
GOCACHE="$(mktemp -d)" \
|
||||
HAKUREI_TEST_SKIP_ACL=1 \
|
||||
PATH="${pkgs.pkgsStatic.musl.bin}/bin:$PATH" \
|
||||
DESTDIR="$out" \
|
||||
HAKUREI_VERSION="v${hakurei.version}" \
|
||||
./dist/release.sh
|
||||
./all.sh
|
||||
'';
|
||||
}
|
||||
);
|
||||
@@ -196,6 +195,7 @@
|
||||
./test/interactive/vm.nix
|
||||
./test/interactive/hakurei.nix
|
||||
./test/interactive/trace.nix
|
||||
./test/interactive/raceattr.nix
|
||||
|
||||
self.nixosModules.hakurei
|
||||
home-manager.nixosModules.home-manager
|
||||
|
||||
@@ -140,21 +140,29 @@ var (
|
||||
ErrInsecure = errors.New("configuration is insecure")
|
||||
)
|
||||
|
||||
const (
|
||||
// VAllowInsecure allows use of compatibility options considered insecure
|
||||
// under any configuration, to work around ecosystem-wide flaws.
|
||||
VAllowInsecure = 1 << iota
|
||||
)
|
||||
|
||||
// Validate checks [Config] and returns [AppError] if an invalid value is encountered.
|
||||
func (config *Config) Validate() error {
|
||||
func (config *Config) Validate(flags int) error {
|
||||
const step = "validate configuration"
|
||||
|
||||
if config == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "invalid configuration"}
|
||||
}
|
||||
|
||||
// this is checked again in hsu
|
||||
if config.Identity < IdentityStart || config.Identity > IdentityEnd {
|
||||
return &AppError{Step: "validate configuration", Err: ErrIdentityBounds,
|
||||
return &AppError{Step: step, Err: ErrIdentityBounds,
|
||||
Msg: "identity " + strconv.Itoa(config.Identity) + " out of range"}
|
||||
}
|
||||
|
||||
if config.SchedPolicy < 0 || config.SchedPolicy > ext.SCHED_LAST {
|
||||
return &AppError{Step: "validate configuration", Err: ErrSchedPolicyBounds,
|
||||
return &AppError{Step: step, Err: ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy " +
|
||||
strconv.Itoa(int(config.SchedPolicy)) +
|
||||
" out of range"}
|
||||
@@ -168,34 +176,51 @@ func (config *Config) Validate() error {
|
||||
}
|
||||
|
||||
if config.Container == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "configuration missing container state"}
|
||||
}
|
||||
if config.Container.Home == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}
|
||||
}
|
||||
if config.Container.Shell == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}
|
||||
}
|
||||
if config.Container.Path == nil {
|
||||
return &AppError{Step: "validate configuration", Err: ErrConfigNull,
|
||||
return &AppError{Step: step, Err: ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}
|
||||
}
|
||||
|
||||
for key := range config.Container.Env {
|
||||
if strings.IndexByte(key, '=') != -1 || strings.IndexByte(key, 0) != -1 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrEnviron,
|
||||
return &AppError{Step: step, Err: ErrEnviron,
|
||||
Msg: "invalid environment variable " + strconv.Quote(key)}
|
||||
}
|
||||
}
|
||||
|
||||
if et := config.Enablements.Unwrap(); !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: "validate configuration", Err: ErrInsecure,
|
||||
et := config.Enablements.Unwrap()
|
||||
if !config.DirectPulse && et&EPulse != 0 {
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}
|
||||
}
|
||||
|
||||
if flags&VAllowInsecure == 0 {
|
||||
switch {
|
||||
case et&EWayland != 0 && config.DirectWayland:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_wayland is insecure and no longer supported"}
|
||||
|
||||
case et&EPipeWire != 0 && config.DirectPipeWire:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_pipewire is insecure and no longer supported"}
|
||||
|
||||
case et&EPulse != 0 && config.DirectPulse:
|
||||
return &AppError{Step: step, Err: ErrInsecure,
|
||||
Msg: "direct_pulse is insecure and no longer supported"}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -14,65 +14,109 @@ func TestConfigValidate(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
config *hst.Config
|
||||
flags int
|
||||
wantErr error
|
||||
}{
|
||||
{"nil", nil, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"nil", nil, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "invalid configuration"}},
|
||||
{"identity lower", &hst.Config{Identity: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
|
||||
{"identity lower", &hst.Config{Identity: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
Msg: "identity -1 out of range"}},
|
||||
{"identity upper", &hst.Config{Identity: 10000}, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
{"identity upper", &hst.Config{Identity: 10000}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrIdentityBounds,
|
||||
Msg: "identity 10000 out of range"}},
|
||||
{"sched lower", &hst.Config{SchedPolicy: -1}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
|
||||
{"sched lower", &hst.Config{SchedPolicy: -1}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy -1 out of range"}},
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
{"sched upper", &hst.Config{SchedPolicy: 0xcafe}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrSchedPolicyBounds,
|
||||
Msg: "scheduling policy 51966 out of range"}},
|
||||
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}},
|
||||
|
||||
{"dbus session", &hst.Config{SessionBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||
&hst.BadInterfaceError{Interface: "", Segment: "session"}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}},
|
||||
{"dbus system", &hst.Config{SystemBus: &hst.BusConfig{See: []string{""}}}, 0,
|
||||
&hst.BadInterfaceError{Interface: "", Segment: "system"}},
|
||||
{"container", &hst.Config{}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
|
||||
{"container", &hst.Config{}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "configuration missing container state"}},
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
{"home", &hst.Config{Container: &hst.ContainerConfig{}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to home directory"}},
|
||||
{"shell", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to shell"}},
|
||||
{"path", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrConfigNull,
|
||||
Msg: "container configuration missing path to initial program"}},
|
||||
|
||||
{"env equals", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM=": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM="`}},
|
||||
{"env NUL", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
Env: map[string]string{"TERM\x00": ""},
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrEnviron,
|
||||
Msg: `invalid environment variable "TERM\x00"`}},
|
||||
{"insecure pulse", &hst.Config{Enablements: hst.NewEnablements(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
|
||||
{"insecure pulse", &hst.Config{Enablements: new(hst.EPulse), Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "enablement PulseAudio is insecure and no longer supported"}},
|
||||
|
||||
{"direct wayland", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_wayland is insecure and no longer supported"}},
|
||||
{"direct wayland allow", &hst.Config{Enablements: new(hst.EWayland), DirectWayland: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"direct pipewire", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_pipewire is insecure and no longer supported"}},
|
||||
{"direct pipewire allow", &hst.Config{Enablements: new(hst.EPipeWire), DirectPipeWire: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"direct pulse", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, 0, &hst.AppError{Step: "validate configuration", Err: hst.ErrInsecure,
|
||||
Msg: "direct_pulse is insecure and no longer supported"}},
|
||||
{"direct pulse allow", &hst.Config{Enablements: new(hst.EPulse), DirectPulse: true, Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, hst.VAllowInsecure, nil},
|
||||
|
||||
{"valid", &hst.Config{Container: &hst.ContainerConfig{
|
||||
Home: fhs.AbsTmp,
|
||||
Shell: fhs.AbsTmp,
|
||||
Path: fhs.AbsTmp,
|
||||
}}, nil},
|
||||
}}, 0, nil},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if err := tc.config.Validate(); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
if err := tc.config.Validate(tc.flags); !reflect.DeepEqual(err, tc.wantErr) {
|
||||
t.Errorf("Validate: error = %#v, want %#v", err, tc.wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -7,12 +7,12 @@ import (
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// Enablement represents an optional host service to export to the target user.
|
||||
type Enablement byte
|
||||
// Enablements denotes optional host service to export to the target user.
|
||||
type Enablements byte
|
||||
|
||||
const (
|
||||
// EWayland exposes a Wayland pathname socket via security-context-v1.
|
||||
EWayland Enablement = 1 << iota
|
||||
EWayland Enablements = 1 << iota
|
||||
// EX11 adds the target user via X11 ChangeHosts and exposes the X11
|
||||
// pathname socket.
|
||||
EX11
|
||||
@@ -28,8 +28,8 @@ const (
|
||||
EM
|
||||
)
|
||||
|
||||
// String returns a string representation of the flags set on [Enablement].
|
||||
func (e Enablement) String() string {
|
||||
// String returns a string representation of the flags set on [Enablements].
|
||||
func (e Enablements) String() string {
|
||||
switch e {
|
||||
case 0:
|
||||
return "(no enablements)"
|
||||
@@ -47,7 +47,7 @@ func (e Enablement) String() string {
|
||||
buf := new(strings.Builder)
|
||||
buf.Grow(32)
|
||||
|
||||
for i := Enablement(1); i < EM; i <<= 1 {
|
||||
for i := Enablements(1); i < EM; i <<= 1 {
|
||||
if e&i != 0 {
|
||||
buf.WriteString(", " + i.String())
|
||||
}
|
||||
@@ -60,12 +60,6 @@ func (e Enablement) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// NewEnablements returns the address of [Enablement] as [Enablements].
|
||||
func NewEnablements(e Enablement) *Enablements { return (*Enablements)(&e) }
|
||||
|
||||
// Enablements is the [json] adapter for [Enablement].
|
||||
type Enablements Enablement
|
||||
|
||||
// enablementsJSON is the [json] representation of [Enablements].
|
||||
type enablementsJSON = struct {
|
||||
Wayland bool `json:"wayland,omitempty"`
|
||||
@@ -75,24 +69,21 @@ type enablementsJSON = struct {
|
||||
Pulse bool `json:"pulse,omitempty"`
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [Enablement].
|
||||
func (e *Enablements) Unwrap() Enablement {
|
||||
// Unwrap returns the value pointed to by e.
|
||||
func (e *Enablements) Unwrap() Enablements {
|
||||
if e == nil {
|
||||
return 0
|
||||
}
|
||||
return Enablement(*e)
|
||||
return *e
|
||||
}
|
||||
|
||||
func (e *Enablements) MarshalJSON() ([]byte, error) {
|
||||
if e == nil {
|
||||
return nil, syscall.EINVAL
|
||||
}
|
||||
func (e Enablements) MarshalJSON() ([]byte, error) {
|
||||
return json.Marshal(&enablementsJSON{
|
||||
Wayland: Enablement(*e)&EWayland != 0,
|
||||
X11: Enablement(*e)&EX11 != 0,
|
||||
DBus: Enablement(*e)&EDBus != 0,
|
||||
PipeWire: Enablement(*e)&EPipeWire != 0,
|
||||
Pulse: Enablement(*e)&EPulse != 0,
|
||||
Wayland: e&EWayland != 0,
|
||||
X11: e&EX11 != 0,
|
||||
DBus: e&EDBus != 0,
|
||||
PipeWire: e&EPipeWire != 0,
|
||||
Pulse: e&EPulse != 0,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -106,22 +97,21 @@ func (e *Enablements) UnmarshalJSON(data []byte) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var ve Enablement
|
||||
*e = 0
|
||||
if v.Wayland {
|
||||
ve |= EWayland
|
||||
*e |= EWayland
|
||||
}
|
||||
if v.X11 {
|
||||
ve |= EX11
|
||||
*e |= EX11
|
||||
}
|
||||
if v.DBus {
|
||||
ve |= EDBus
|
||||
*e |= EDBus
|
||||
}
|
||||
if v.PipeWire {
|
||||
ve |= EPipeWire
|
||||
*e |= EPipeWire
|
||||
}
|
||||
if v.Pulse {
|
||||
ve |= EPulse
|
||||
*e |= EPulse
|
||||
}
|
||||
*e = Enablements(ve)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ func TestEnablementString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
flags hst.Enablement
|
||||
flags hst.Enablements
|
||||
want string
|
||||
}{
|
||||
{0, "(no enablements)"},
|
||||
@@ -59,13 +59,13 @@ func TestEnablements(t *testing.T) {
|
||||
sData string
|
||||
}{
|
||||
{"nil", nil, "null", `{"value":null,"magic":3236757504}`},
|
||||
{"zero", hst.NewEnablements(0), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", hst.NewEnablements(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", hst.NewEnablements(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", hst.NewEnablements(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", hst.NewEnablements(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", hst.NewEnablements(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", hst.NewEnablements(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
{"zero", new(hst.Enablements(0)), `{}`, `{"value":{},"magic":3236757504}`},
|
||||
{"wayland", new(hst.EWayland), `{"wayland":true}`, `{"value":{"wayland":true},"magic":3236757504}`},
|
||||
{"x11", new(hst.EX11), `{"x11":true}`, `{"value":{"x11":true},"magic":3236757504}`},
|
||||
{"dbus", new(hst.EDBus), `{"dbus":true}`, `{"value":{"dbus":true},"magic":3236757504}`},
|
||||
{"pipewire", new(hst.EPipeWire), `{"pipewire":true}`, `{"value":{"pipewire":true},"magic":3236757504}`},
|
||||
{"pulse", new(hst.EPulse), `{"pulse":true}`, `{"value":{"pulse":true},"magic":3236757504}`},
|
||||
{"all", new(hst.EM - 1), `{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true}`, `{"value":{"wayland":true,"x11":true,"dbus":true,"pipewire":true,"pulse":true},"magic":3236757504}`},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
@@ -137,7 +137,7 @@ func TestEnablements(t *testing.T) {
|
||||
})
|
||||
|
||||
t.Run("val", func(t *testing.T) {
|
||||
if got := hst.NewEnablements(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
if got := new(hst.EWayland | hst.EPulse).Unwrap(); got != hst.EWayland|hst.EPulse {
|
||||
t.Errorf("Unwrap: %v", got)
|
||||
}
|
||||
})
|
||||
@@ -146,9 +146,6 @@ func TestEnablements(t *testing.T) {
|
||||
t.Run("passthrough", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
if _, err := (*hst.Enablements)(nil).MarshalJSON(); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("MarshalJSON: error = %v", err)
|
||||
}
|
||||
if err := (*hst.Enablements)(nil).UnmarshalJSON(nil); !errors.Is(err, syscall.EINVAL) {
|
||||
t.Errorf("UnmarshalJSON: error = %v", err)
|
||||
}
|
||||
|
||||
@@ -56,8 +56,10 @@ type Ops interface {
|
||||
|
||||
// ApplyState holds the address of [Ops] and any relevant application state.
|
||||
type ApplyState struct {
|
||||
// AutoEtcPrefix is the prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
||||
// Prefix for [FSBind] in autoetc [FSBind.Special] condition.
|
||||
AutoEtcPrefix string
|
||||
// Whether to skip remounting root.
|
||||
NoRemountRoot bool
|
||||
|
||||
Ops
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ package hst
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"path"
|
||||
"path/filepath"
|
||||
|
||||
"hakurei.app/check"
|
||||
)
|
||||
@@ -28,7 +28,7 @@ func (l *FSLink) Valid() bool {
|
||||
if l == nil || l.Target == nil || l.Linkname == "" {
|
||||
return false
|
||||
}
|
||||
return !l.Dereference || path.IsAbs(l.Linkname)
|
||||
return !l.Dereference || filepath.IsAbs(l.Linkname)
|
||||
}
|
||||
|
||||
func (l *FSLink) Path() *check.Absolute {
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"strings"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
)
|
||||
|
||||
func init() { gob.Register(new(FSOverlay)) }
|
||||
@@ -69,9 +70,12 @@ func (o *FSOverlay) Apply(z *ApplyState) {
|
||||
return
|
||||
}
|
||||
|
||||
if o.Upper != nil && o.Work != nil { // rw
|
||||
if o.Upper != nil && o.Work != nil {
|
||||
z.Overlay(o.Target, o.Upper, o.Work, o.Lower...)
|
||||
} else { // ro
|
||||
if o.Target.Is(fhs.AbsRoot) {
|
||||
z.NoRemountRoot = true
|
||||
}
|
||||
} else {
|
||||
z.OverlayReadonly(o.Target, o.Lower...)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,5 +49,18 @@ func TestFSOverlay(t *testing.T) {
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
}}, m("/mnt/src"), ms("/tmp/.src0", "/tmp/.src1"),
|
||||
"*/mnt/src:/tmp/.src0:/tmp/.src1"},
|
||||
|
||||
{"no remount root", &hst.FSOverlay{
|
||||
Target: m("/"),
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
Upper: m("/tmp/upper"),
|
||||
Work: m("/tmp/work"),
|
||||
}, true, container.Ops{&container.MountOverlayOp{
|
||||
Target: m("/"),
|
||||
Lower: ms("/tmp/.src0", "/tmp/.src1"),
|
||||
Upper: m("/tmp/upper"),
|
||||
Work: m("/tmp/work"),
|
||||
}}, m("/"), ms("/tmp/upper", "/tmp/work", "/tmp/.src0", "/tmp/.src1"),
|
||||
"w*/:/tmp/upper:/tmp/work:/tmp/.src0:/tmp/.src1"},
|
||||
})
|
||||
}
|
||||
|
||||
@@ -72,7 +72,7 @@ func Template() *Config {
|
||||
return &Config{
|
||||
ID: "org.chromium.Chromium",
|
||||
|
||||
Enablements: NewEnablements(EWayland | EDBus | EPipeWire),
|
||||
Enablements: new(EWayland | EDBus | EPipeWire),
|
||||
|
||||
SessionBus: &BusConfig{
|
||||
See: nil,
|
||||
|
||||
@@ -8,12 +8,14 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/acl"
|
||||
"hakurei.app/internal/info"
|
||||
)
|
||||
|
||||
const testFileName = "acl.test"
|
||||
@@ -24,11 +26,17 @@ var (
|
||||
)
|
||||
|
||||
func TestUpdate(t *testing.T) {
|
||||
if os.Getenv("HAKUREI_TEST_SKIP_ACL") == "1" {
|
||||
t.Skip("acl test skipped")
|
||||
if info.CanDegrade {
|
||||
name := filepath.Join(t.TempDir(), "check-degrade")
|
||||
if err := os.WriteFile(name, nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := acl.Update(name, os.Geteuid()); errors.Is(err, syscall.ENOTSUP) {
|
||||
t.Skip(err)
|
||||
}
|
||||
}
|
||||
|
||||
testFilePath := path.Join(t.TempDir(), testFileName)
|
||||
testFilePath := filepath.Join(t.TempDir(), testFileName)
|
||||
|
||||
if f, err := os.Create(testFilePath); err != nil {
|
||||
t.Fatalf("Create: error = %v", err)
|
||||
|
||||
7
internal/info/optional_skip.go
Normal file
7
internal/info/optional_skip.go
Normal file
@@ -0,0 +1,7 @@
|
||||
//go:build !noskip
|
||||
|
||||
package info
|
||||
|
||||
// CanDegrade is whether tests are allowed to transparently degrade or skip due
|
||||
// to required system features being denied or unavailable.
|
||||
const CanDegrade = true
|
||||
5
internal/info/optional_strict.go
Normal file
5
internal/info/optional_strict.go
Normal file
@@ -0,0 +1,5 @@
|
||||
//go:build noskip
|
||||
|
||||
package info
|
||||
|
||||
const CanDegrade = false
|
||||
90
internal/kobject/event.go
Normal file
90
internal/kobject/event.go
Normal file
@@ -0,0 +1,90 @@
|
||||
package kobject
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strconv"
|
||||
"strings"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
// Event is a [uevent.Message] with known environment variables processed.
|
||||
type Event struct {
|
||||
// alloc_uevent_skb: action_string
|
||||
Action uevent.KobjectAction `json:"action"`
|
||||
// alloc_uevent_skb: devpath
|
||||
DevPath string `json:"devpath"`
|
||||
|
||||
// Uninterpreted environment variable pairs. An entry missing a separator
|
||||
// gains the value "\x00".
|
||||
Env map[string]string `json:"env"`
|
||||
|
||||
// SEQNUM value set by the kernel.
|
||||
Sequence uint64 `json:"seqnum"`
|
||||
// SYNTH_UUID value set on trigger, nil denotes a non-synthetic event.
|
||||
Synth *uevent.UUID `json:"synth_uuid,omitempty"`
|
||||
// SUBSYSTEM value set by the kernel.
|
||||
Subsystem string `json:"subsystem"`
|
||||
}
|
||||
|
||||
// Populate populates e with the contents of a [uevent.Message].
|
||||
//
|
||||
// The ACTION and DEVPATH environment variables are ignored and assumed to be
|
||||
// consistent with the header.
|
||||
func (e *Event) Populate(reportErr func(error), m *uevent.Message) {
|
||||
if reportErr == nil {
|
||||
reportErr = func(error) {}
|
||||
}
|
||||
|
||||
*e = Event{
|
||||
Action: m.Action,
|
||||
DevPath: m.DevPath,
|
||||
Env: make(map[string]string),
|
||||
}
|
||||
|
||||
for _, s := range m.Env {
|
||||
k, v, ok := strings.Cut(s, "=")
|
||||
if !ok {
|
||||
if _, ok = e.Env[s]; !ok {
|
||||
e.Env[s] = "\x00"
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
switch k {
|
||||
case "ACTION", "DEVPATH":
|
||||
continue
|
||||
|
||||
case "SEQNUM":
|
||||
seq, err := strconv.ParseUint(v, 10, 64)
|
||||
if err != nil {
|
||||
if _e := errors.Unwrap(err); _e != nil {
|
||||
err = _e
|
||||
}
|
||||
reportErr(err)
|
||||
|
||||
e.Env[k] = v
|
||||
continue
|
||||
}
|
||||
e.Sequence = seq
|
||||
|
||||
case "SYNTH_UUID":
|
||||
var uuid uevent.UUID
|
||||
err := uuid.UnmarshalText(unsafe.Slice(unsafe.StringData(v), len(v)))
|
||||
if err != nil {
|
||||
reportErr(err)
|
||||
|
||||
e.Env[k] = v
|
||||
continue
|
||||
}
|
||||
e.Synth = &uuid
|
||||
|
||||
case "SUBSYSTEM":
|
||||
e.Subsystem = v
|
||||
|
||||
default:
|
||||
e.Env[k] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
92
internal/kobject/event_test.go
Normal file
92
internal/kobject/event_test.go
Normal file
@@ -0,0 +1,92 @@
|
||||
package kobject_test
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/internal/kobject"
|
||||
"hakurei.app/internal/uevent"
|
||||
)
|
||||
|
||||
func TestEvent(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
msg uevent.Message
|
||||
want kobject.Event
|
||||
errs []error
|
||||
}{
|
||||
{"sample coldboot qemu", uevent.Message{
|
||||
Action: uevent.KOBJ_ADD,
|
||||
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
Env: []string{
|
||||
"ACTION=add",
|
||||
"DEVPATH=/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
"SUBSYSTEM=acpi",
|
||||
"SYNTH_UUID=fe4d7c9d-b8c6-4a70-9ef1-3d8a58d18eed",
|
||||
"MODALIAS=acpi:LNXPWRBN:",
|
||||
"SEQNUM=777",
|
||||
}}, kobject.Event{
|
||||
Action: uevent.KOBJ_ADD,
|
||||
DevPath: "/devices/LNXSYSTM:00/LNXPWRBN:00",
|
||||
Env: map[string]string{
|
||||
"MODALIAS": "acpi:LNXPWRBN:",
|
||||
},
|
||||
Sequence: 777,
|
||||
Synth: &uevent.UUID{
|
||||
0xfe, 0x4d, 0x7c, 0x9d,
|
||||
0xb8, 0xc6,
|
||||
0x4a, 0x70,
|
||||
0x9e, 0xf1,
|
||||
0x3d, 0x8a, 0x58, 0xd1, 0x8e, 0xed,
|
||||
},
|
||||
Subsystem: "acpi",
|
||||
}, []error{}},
|
||||
|
||||
{"nil reportErr", uevent.Message{Env: []string{
|
||||
"SEQNUM=\x00",
|
||||
}}, kobject.Event{Env: map[string]string{
|
||||
"SEQNUM": "\x00",
|
||||
}}, nil},
|
||||
|
||||
{"bad SEQNUM SYNTH_UUID", uevent.Message{Env: []string{
|
||||
"SEQNUM=\x00",
|
||||
"SYNTH_UUID=\x00",
|
||||
"SUBSYSTEM=\x00",
|
||||
}}, kobject.Event{Subsystem: "\x00", Env: map[string]string{
|
||||
"SEQNUM": "\x00",
|
||||
"SYNTH_UUID": "\x00",
|
||||
}}, []error{strconv.ErrSyntax, uevent.UUIDSizeError(1)}},
|
||||
|
||||
{"bad sep", uevent.Message{Env: []string{
|
||||
"SYNTH_UUID",
|
||||
}}, kobject.Event{Env: map[string]string{
|
||||
"SYNTH_UUID": "\x00",
|
||||
}}, []error{}},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
var f func(error)
|
||||
gotErrs := make([]error, 0)
|
||||
if tc.errs != nil {
|
||||
f = func(err error) {
|
||||
gotErrs = append(gotErrs, err)
|
||||
}
|
||||
}
|
||||
|
||||
var got kobject.Event
|
||||
got.Populate(f, &tc.msg)
|
||||
|
||||
if !reflect.DeepEqual(&got, &tc.want) {
|
||||
t.Errorf("Populate: %#v, want %#v", got, tc.want)
|
||||
}
|
||||
if tc.errs != nil && !reflect.DeepEqual(gotErrs, tc.errs) {
|
||||
t.Errorf("Populate: errs = %v, want %v", gotErrs, tc.errs)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container
|
||||
package landlock
|
||||
|
||||
import (
|
||||
"strings"
|
||||
@@ -14,11 +14,11 @@ const (
|
||||
LANDLOCK_CREATE_RULESET_VERSION = 1 << iota
|
||||
)
|
||||
|
||||
// LandlockAccessFS is bitmask of handled filesystem actions.
|
||||
type LandlockAccessFS uint64
|
||||
// AccessFS is bitmask of handled filesystem actions.
|
||||
type AccessFS uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_FS_EXECUTE LandlockAccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_EXECUTE AccessFS = 1 << iota
|
||||
LANDLOCK_ACCESS_FS_WRITE_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_FILE
|
||||
LANDLOCK_ACCESS_FS_READ_DIR
|
||||
@@ -38,8 +38,8 @@ const (
|
||||
_LANDLOCK_ACCESS_FS_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessFS] flags.
|
||||
func (f LandlockAccessFS) String() string {
|
||||
// String returns a space-separated string of [AccessFS] flags.
|
||||
func (f AccessFS) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_FS_EXECUTE:
|
||||
return "execute"
|
||||
@@ -90,8 +90,8 @@ func (f LandlockAccessFS) String() string {
|
||||
return "fs_ioctl_dev"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessFS
|
||||
for i := LandlockAccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
var c []AccessFS
|
||||
for i := AccessFS(1); i < _LANDLOCK_ACCESS_FS_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -107,18 +107,18 @@ func (f LandlockAccessFS) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockAccessNet is bitmask of handled network actions.
|
||||
type LandlockAccessNet uint64
|
||||
// AccessNet is bitmask of handled network actions.
|
||||
type AccessNet uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP LandlockAccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_BIND_TCP AccessNet = 1 << iota
|
||||
LANDLOCK_ACCESS_NET_CONNECT_TCP
|
||||
|
||||
_LANDLOCK_ACCESS_NET_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockAccessNet] flags.
|
||||
func (f LandlockAccessNet) String() string {
|
||||
// String returns a space-separated string of [AccessNet] flags.
|
||||
func (f AccessNet) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_ACCESS_NET_BIND_TCP:
|
||||
return "bind_tcp"
|
||||
@@ -127,8 +127,8 @@ func (f LandlockAccessNet) String() string {
|
||||
return "connect_tcp"
|
||||
|
||||
default:
|
||||
var c []LandlockAccessNet
|
||||
for i := LandlockAccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
var c []AccessNet
|
||||
for i := AccessNet(1); i < _LANDLOCK_ACCESS_NET_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -144,18 +144,18 @@ func (f LandlockAccessNet) String() string {
|
||||
}
|
||||
}
|
||||
|
||||
// LandlockScope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type LandlockScope uint64
|
||||
// Scope is bitmask of scopes restricting a Landlock domain from accessing outside resources.
|
||||
type Scope uint64
|
||||
|
||||
const (
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET LandlockScope = 1 << iota
|
||||
LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET Scope = 1 << iota
|
||||
LANDLOCK_SCOPE_SIGNAL
|
||||
|
||||
_LANDLOCK_SCOPE_DELIM
|
||||
)
|
||||
|
||||
// String returns a space-separated string of [LandlockScope] flags.
|
||||
func (f LandlockScope) String() string {
|
||||
// String returns a space-separated string of [Scope] flags.
|
||||
func (f Scope) String() string {
|
||||
switch f {
|
||||
case LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET:
|
||||
return "abstract_unix_socket"
|
||||
@@ -164,8 +164,8 @@ func (f LandlockScope) String() string {
|
||||
return "signal"
|
||||
|
||||
default:
|
||||
var c []LandlockScope
|
||||
for i := LandlockScope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
var c []Scope
|
||||
for i := Scope(1); i < _LANDLOCK_SCOPE_DELIM; i <<= 1 {
|
||||
if f&i != 0 {
|
||||
c = append(c, i)
|
||||
}
|
||||
@@ -184,12 +184,12 @@ func (f LandlockScope) String() string {
|
||||
// RulesetAttr is equivalent to struct landlock_ruleset_attr.
|
||||
type RulesetAttr struct {
|
||||
// Bitmask of handled filesystem actions.
|
||||
HandledAccessFS LandlockAccessFS
|
||||
HandledAccessFS AccessFS
|
||||
// Bitmask of handled network actions.
|
||||
HandledAccessNet LandlockAccessNet
|
||||
HandledAccessNet AccessNet
|
||||
// Bitmask of scopes restricting a Landlock domain from accessing outside
|
||||
// resources (e.g. IPCs).
|
||||
Scoped LandlockScope
|
||||
Scoped Scope
|
||||
}
|
||||
|
||||
// String returns a user-facing description of [RulesetAttr].
|
||||
@@ -239,13 +239,13 @@ func (rulesetAttr *RulesetAttr) Create(flags uintptr) (fd int, err error) {
|
||||
return fd, nil
|
||||
}
|
||||
|
||||
// LandlockGetABI returns the ABI version supported by the kernel.
|
||||
func LandlockGetABI() (int, error) {
|
||||
// GetABI returns the ABI version supported by the kernel.
|
||||
func GetABI() (int, error) {
|
||||
return (*RulesetAttr)(nil).Create(LANDLOCK_CREATE_RULESET_VERSION)
|
||||
}
|
||||
|
||||
// LandlockRestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func LandlockRestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
// RestrictSelf applies a loaded ruleset to the calling thread.
|
||||
func RestrictSelf(rulesetFd int, flags uintptr) error {
|
||||
r, _, errno := syscall.Syscall(
|
||||
ext.SYS_LANDLOCK_RESTRICT_SELF,
|
||||
uintptr(rulesetFd),
|
||||
65
internal/landlock/landlock_test.go
Normal file
65
internal/landlock/landlock_test.go
Normal file
@@ -0,0 +1,65 @@
|
||||
package landlock_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/internal/landlock"
|
||||
)
|
||||
|
||||
func TestLandlockString(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
rulesetAttr *landlock.RulesetAttr
|
||||
want string
|
||||
}{
|
||||
{"nil", nil, "NULL"},
|
||||
{"zero", new(landlock.RulesetAttr), "0"},
|
||||
{"some", &landlock.RulesetAttr{Scoped: landlock.LANDLOCK_SCOPE_SIGNAL}, "scoped: signal"},
|
||||
{"set", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_MAKE_SYM | landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV | landlock.LANDLOCK_ACCESS_FS_WRITE_FILE,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET | landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: write_file make_sym fs_ioctl_dev, net: bind_tcp, scoped: abstract_unix_socket signal"},
|
||||
{"all", &landlock.RulesetAttr{
|
||||
HandledAccessFS: landlock.LANDLOCK_ACCESS_FS_EXECUTE |
|
||||
landlock.LANDLOCK_ACCESS_FS_WRITE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_READ_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_REMOVE_FILE |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_CHAR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_DIR |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_REG |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_FIFO |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_BLOCK |
|
||||
landlock.LANDLOCK_ACCESS_FS_MAKE_SYM |
|
||||
landlock.LANDLOCK_ACCESS_FS_REFER |
|
||||
landlock.LANDLOCK_ACCESS_FS_TRUNCATE |
|
||||
landlock.LANDLOCK_ACCESS_FS_IOCTL_DEV,
|
||||
HandledAccessNet: landlock.LANDLOCK_ACCESS_NET_BIND_TCP |
|
||||
landlock.LANDLOCK_ACCESS_NET_CONNECT_TCP,
|
||||
Scoped: landlock.LANDLOCK_SCOPE_ABSTRACT_UNIX_SOCKET |
|
||||
landlock.LANDLOCK_SCOPE_SIGNAL,
|
||||
}, "fs: execute write_file read_file read_dir remove_dir remove_file make_char make_dir make_reg make_sock make_fifo make_block make_sym fs_refer fs_truncate fs_ioctl_dev, net: bind_tcp connect_tcp, scoped: abstract_unix_socket signal"},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
if got := tc.rulesetAttr.String(); got != tc.want {
|
||||
t.Errorf("String: %s, want %s", got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLandlockAttrSize(t *testing.T) {
|
||||
t.Parallel()
|
||||
want := 24
|
||||
if got := unsafe.Sizeof(landlock.RulesetAttr{}); got != uintptr(want) {
|
||||
t.Errorf("Sizeof: %d, want %d", got, want)
|
||||
}
|
||||
}
|
||||
@@ -46,7 +46,11 @@ type Conn struct {
|
||||
|
||||
// Dial returns the address of a newly connected generic netlink connection of
|
||||
// specified family and groups.
|
||||
func Dial(family int, groups uint32) (*Conn, error) {
|
||||
//
|
||||
// For a nonzero rcvbuf, the socket receive buffer size is set to its absolute
|
||||
// value via SO_RCVBUF for a positive value, or SO_RCVBUFFORCE for a negative
|
||||
// value.
|
||||
func Dial(family int, groups uint32, rcvbuf int64) (*Conn, error) {
|
||||
var c Conn
|
||||
if fd, err := syscall.Socket(
|
||||
syscall.AF_NETLINK,
|
||||
@@ -75,6 +79,23 @@ func Dial(family int, groups uint32) (*Conn, error) {
|
||||
return nil, syscall.ENOTRECOVERABLE
|
||||
}
|
||||
|
||||
if rcvbuf != 0 {
|
||||
opt := syscall.SO_RCVBUF
|
||||
if rcvbuf < 0 {
|
||||
opt = syscall.SO_RCVBUFFORCE
|
||||
rcvbuf = -rcvbuf
|
||||
}
|
||||
if err = syscall.SetsockoptInt(
|
||||
fd,
|
||||
syscall.SOL_SOCKET,
|
||||
opt,
|
||||
int(rcvbuf),
|
||||
); err != nil {
|
||||
_ = syscall.Close(fd)
|
||||
return nil, os.NewSyscallError("setsockopt", err)
|
||||
}
|
||||
}
|
||||
|
||||
c.family = family
|
||||
c.f = os.NewFile(uintptr(fd), "netlink")
|
||||
if c.raw, err = c.f.SyscallConn(); err != nil {
|
||||
@@ -101,23 +122,40 @@ func (c *Conn) Close() error {
|
||||
return c.f.Close()
|
||||
}
|
||||
|
||||
// Recvfrom wraps recv(2) with nonblocking behaviour via the runtime network poller.
|
||||
// Recvmsg wraps recv(2) with nonblocking behaviour via the runtime network poller.
|
||||
//
|
||||
// The returned slice is valid until the next call to Recvfrom.
|
||||
func (c *Conn) Recvfrom(
|
||||
// The returned slice is valid until the next call to Recvmsg.
|
||||
func (c *Conn) Recvmsg(
|
||||
ctx context.Context,
|
||||
flags int,
|
||||
) (data []byte, from syscall.Sockaddr, err error) {
|
||||
) (data []byte, recvflags int, from syscall.Sockaddr, err error) {
|
||||
if err = c.f.SetReadDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
var n int
|
||||
data = c.buf[:]
|
||||
|
||||
if ctx == nil {
|
||||
rcErr := c.raw.Control(func(fd uintptr) {
|
||||
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
|
||||
})
|
||||
if n >= 0 {
|
||||
data = data[:n]
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
err = os.NewSyscallError("recvmsg", err)
|
||||
} else {
|
||||
err = rcErr
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
rcErr := c.raw.Read(func(fd uintptr) (done bool) {
|
||||
n, from, err = syscall.Recvfrom(int(fd), data, flags)
|
||||
n, _, recvflags, from, err = syscall.Recvmsg(int(fd), data, nil, flags)
|
||||
return err != syscall.EWOULDBLOCK
|
||||
})
|
||||
if n >= 0 {
|
||||
@@ -129,7 +167,7 @@ func (c *Conn) Recvfrom(
|
||||
select {
|
||||
case rcErr := <-done:
|
||||
if err != nil {
|
||||
err = os.NewSyscallError("recvfrom", err)
|
||||
err = os.NewSyscallError("recvmsg", err)
|
||||
} else {
|
||||
err = rcErr
|
||||
}
|
||||
@@ -147,12 +185,12 @@ func (c *Conn) Recvfrom(
|
||||
}
|
||||
}
|
||||
|
||||
// Sendto wraps send(2) with nonblocking behaviour via the runtime network poller.
|
||||
func (c *Conn) Sendto(
|
||||
// Sendmsg wraps send(2) with nonblocking behaviour via the runtime network poller.
|
||||
func (c *Conn) Sendmsg(
|
||||
ctx context.Context,
|
||||
p []byte,
|
||||
flags int,
|
||||
to syscall.Sockaddr,
|
||||
flags int,
|
||||
) (err error) {
|
||||
if err = c.f.SetWriteDeadline(time.Time{}); err != nil {
|
||||
return
|
||||
@@ -161,7 +199,7 @@ func (c *Conn) Sendto(
|
||||
done := make(chan error, 1)
|
||||
go func() {
|
||||
done <- c.raw.Write(func(fd uintptr) (done bool) {
|
||||
err = syscall.Sendto(int(fd), p, flags, to)
|
||||
err = syscall.Sendmsg(int(fd), p, nil, to, flags)
|
||||
return err != syscall.EWOULDBLOCK
|
||||
})
|
||||
}()
|
||||
@@ -169,7 +207,7 @@ func (c *Conn) Sendto(
|
||||
select {
|
||||
case rcErr := <-done:
|
||||
if err != nil {
|
||||
err = os.NewSyscallError("sendto", err)
|
||||
err = os.NewSyscallError("sendmsg", err)
|
||||
} else {
|
||||
err = rcErr
|
||||
}
|
||||
@@ -278,7 +316,7 @@ type HandlerFunc func(resp []syscall.NetlinkMessage) error
|
||||
func (c *Conn) receive(ctx context.Context, f HandlerFunc, flags int) error {
|
||||
for {
|
||||
var resp []syscall.NetlinkMessage
|
||||
if data, _, err := c.Recvfrom(ctx, flags); err != nil {
|
||||
if data, _, _, err := c.Recvmsg(ctx, flags); err != nil {
|
||||
return err
|
||||
} else if len(data) < syscall.NLMSG_HDRLEN {
|
||||
return syscall.EBADE
|
||||
@@ -302,9 +340,9 @@ func (c *Conn) Roundtrip(ctx context.Context, f HandlerFunc) error {
|
||||
}
|
||||
defer func() { c.seq++ }()
|
||||
|
||||
if err := c.Sendto(ctx, c.pending(), 0, &syscall.SockaddrNetlink{
|
||||
if err := c.Sendmsg(ctx, c.pending(), &syscall.SockaddrNetlink{
|
||||
Family: syscall.AF_NETLINK,
|
||||
}); err != nil {
|
||||
}, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,8 @@ type RouteConn struct{ conn *Conn }
|
||||
func (c *RouteConn) Close() error { return c.conn.Close() }
|
||||
|
||||
// DialRoute returns the address of a newly connected [RouteConn].
|
||||
func DialRoute() (*RouteConn, error) {
|
||||
c, err := Dial(syscall.NETLINK_ROUTE, 0)
|
||||
func DialRoute(rcvbuf int64) (*RouteConn, error) {
|
||||
c, err := Dial(syscall.NETLINK_ROUTE, 0, rcvbuf)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/internal/dbus"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
|
||||
@@ -84,7 +85,7 @@ type syscallDispatcher interface {
|
||||
// setDumpable provides [container.SetDumpable].
|
||||
setDumpable(dumpable uintptr) error
|
||||
// receive provides [container.Receive].
|
||||
receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error)
|
||||
receive(key string, e any, fdp *int) (closeFunc func() error, err error)
|
||||
|
||||
// containerStart provides the Start method of [container.Container].
|
||||
containerStart(z *container.Container) error
|
||||
@@ -154,8 +155,8 @@ func (direct) prctl(op, arg2, arg3 uintptr) error { return ext.Prctl(op, arg2, a
|
||||
func (direct) overflowUid(msg message.Msg) int { return container.OverflowUid(msg) }
|
||||
func (direct) overflowGid(msg message.Msg) int { return container.OverflowGid(msg) }
|
||||
func (direct) setDumpable(dumpable uintptr) error { return ext.SetDumpable(dumpable) }
|
||||
func (direct) receive(key string, e any, fdp *uintptr) (func() error, error) {
|
||||
return container.Receive(key, e, fdp)
|
||||
func (direct) receive(key string, e any, fdp *int) (func() error, error) {
|
||||
return params.Receive(key, e, fdp)
|
||||
}
|
||||
|
||||
func (direct) containerStart(z *container.Container) error { return z.Start() }
|
||||
|
||||
@@ -401,12 +401,12 @@ func (k *kstub) setDumpable(dumpable uintptr) error {
|
||||
stub.CheckArg(k.Stub, "dumpable", dumpable, 0))
|
||||
}
|
||||
|
||||
func (k *kstub) receive(key string, e any, fdp *uintptr) (closeFunc func() error, err error) {
|
||||
func (k *kstub) receive(key string, e any, fdp *int) (closeFunc func() error, err error) {
|
||||
k.Helper()
|
||||
expect := k.Expects("receive")
|
||||
reflect.ValueOf(e).Elem().Set(reflect.ValueOf(expect.Args[1]))
|
||||
if expect.Args[2] != nil {
|
||||
*fdp = expect.Args[2].(uintptr)
|
||||
*fdp = int(expect.Args[2].(uintptr))
|
||||
}
|
||||
return func() error { return k.Expects("closeReceive").Err }, expect.Error(
|
||||
stub.CheckArg(k.Stub, "key", key, 0))
|
||||
@@ -690,38 +690,38 @@ func (panicMsgContext) Value(any) any { panic("unreachable") }
|
||||
// This type is meant to be embedded in partial syscallDispatcher implementations.
|
||||
type panicDispatcher struct{}
|
||||
|
||||
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
|
||||
func (panicDispatcher) getppid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getpid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getuid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
|
||||
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
|
||||
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *uintptr) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
|
||||
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
|
||||
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
|
||||
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
|
||||
func (panicDispatcher) fatal(...any) { panic("unreachable") }
|
||||
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
|
||||
func (panicDispatcher) new(func(k syscallDispatcher, msg message.Msg)) { panic("unreachable") }
|
||||
func (panicDispatcher) getppid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getpid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getuid() int { panic("unreachable") }
|
||||
func (panicDispatcher) getgid() int { panic("unreachable") }
|
||||
func (panicDispatcher) lookupEnv(string) (string, bool) { panic("unreachable") }
|
||||
func (panicDispatcher) pipe() (*os.File, *os.File, error) { panic("unreachable") }
|
||||
func (panicDispatcher) stat(string) (os.FileInfo, error) { panic("unreachable") }
|
||||
func (panicDispatcher) open(string) (osFile, error) { panic("unreachable") }
|
||||
func (panicDispatcher) readdir(string) ([]os.DirEntry, error) { panic("unreachable") }
|
||||
func (panicDispatcher) tempdir() string { panic("unreachable") }
|
||||
func (panicDispatcher) mkdir(string, os.FileMode) error { panic("unreachable") }
|
||||
func (panicDispatcher) removeAll(string) error { panic("unreachable") }
|
||||
func (panicDispatcher) exit(int) { panic("unreachable") }
|
||||
func (panicDispatcher) evalSymlinks(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) prctl(uintptr, uintptr, uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) lookupGroupId(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) lookPath(string) (string, error) { panic("unreachable") }
|
||||
func (panicDispatcher) cmdOutput(*exec.Cmd) ([]byte, error) { panic("unreachable") }
|
||||
func (panicDispatcher) overflowUid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) overflowGid(message.Msg) int { panic("unreachable") }
|
||||
func (panicDispatcher) setDumpable(uintptr) error { panic("unreachable") }
|
||||
func (panicDispatcher) receive(string, any, *int) (func() error, error) { panic("unreachable") }
|
||||
func (panicDispatcher) containerStart(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerServe(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) containerWait(*container.Container) error { panic("unreachable") }
|
||||
func (panicDispatcher) mustHsuPath() *check.Absolute { panic("unreachable") }
|
||||
func (panicDispatcher) dbusAddress() (string, string) { panic("unreachable") }
|
||||
func (panicDispatcher) setupContSignal(int) (io.ReadCloser, func(), error) { panic("unreachable") }
|
||||
func (panicDispatcher) getMsg() message.Msg { panic("unreachable") }
|
||||
func (panicDispatcher) fatal(...any) { panic("unreachable") }
|
||||
func (panicDispatcher) fatalf(string, ...any) { panic("unreachable") }
|
||||
|
||||
func (panicDispatcher) notifyContext(context.Context, ...os.Signal) (context.Context, context.CancelFunc) {
|
||||
panic("unreachable")
|
||||
|
||||
@@ -32,7 +32,14 @@ type outcome struct {
|
||||
syscallDispatcher
|
||||
}
|
||||
|
||||
func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, config *hst.Config) error {
|
||||
// finalise prepares an outcome for main.
|
||||
func (k *outcome) finalise(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
id *hst.ID,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
) error {
|
||||
if ctx == nil || id == nil {
|
||||
// unreachable
|
||||
panic("invalid call to finalise")
|
||||
@@ -43,7 +50,7 @@ func (k *outcome) finalise(ctx context.Context, msg message.Msg, id *hst.ID, con
|
||||
}
|
||||
k.ctx = ctx
|
||||
|
||||
if err := config.Validate(); err != nil {
|
||||
if err := config.Validate(flags); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
|
||||
@@ -194,7 +194,7 @@ type outcomeStateSys struct {
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
appId string
|
||||
// Copied from [hst.Config]. Safe for read by outcomeOp.toSystem.
|
||||
et hst.Enablement
|
||||
et hst.Enablements
|
||||
|
||||
// Copied from [hst.Config]. Safe for read by spWaylandOp.toSystem only.
|
||||
directWayland bool
|
||||
|
||||
@@ -13,7 +13,6 @@ import (
|
||||
"time"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/info"
|
||||
@@ -298,12 +297,12 @@ func (k *outcome) main(msg message.Msg, identifierFd int) {
|
||||
// accumulate enablements of remaining instances
|
||||
var (
|
||||
// alive enablement bits
|
||||
rt hst.Enablement
|
||||
rt hst.Enablements
|
||||
// alive instance count
|
||||
n int
|
||||
)
|
||||
for eh := range entries {
|
||||
var et hst.Enablement
|
||||
var et hst.Enablements
|
||||
if et, err = eh.Load(nil); err != nil {
|
||||
perror(err, "read state header of instance "+eh.ID.String())
|
||||
} else {
|
||||
@@ -372,17 +371,18 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
// shim runs in the same session as monitor; see shim.go for behaviour
|
||||
cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) }
|
||||
|
||||
var shimPipe *os.File
|
||||
if fd, w, err := container.Setup(&cmd.ExtraFiles); err != nil {
|
||||
var shimPipe [2]*os.File
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
return cmd, nil, &hst.AppError{Step: "create shim setup pipe", Err: err}
|
||||
} else {
|
||||
shimPipe = w
|
||||
cmd.Env = []string{
|
||||
// passed through to shim by hsu
|
||||
shimEnv + "=" + strconv.Itoa(fd),
|
||||
shimEnv + "=" + strconv.Itoa(3+len(cmd.ExtraFiles)),
|
||||
// interpreted by hsu
|
||||
"HAKUREI_IDENTITY=" + k.state.identity.String(),
|
||||
}
|
||||
cmd.ExtraFiles = append(cmd.ExtraFiles, r)
|
||||
shimPipe[0], shimPipe[1] = r, w
|
||||
}
|
||||
|
||||
if len(k.supp) > 0 {
|
||||
@@ -393,12 +393,16 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
|
||||
|
||||
msg.Verbosef("setuid helper at %s", hsuPath)
|
||||
if err := cmd.Start(); err != nil {
|
||||
_, _ = shimPipe[0].Close(), shimPipe[1].Close()
|
||||
msg.Resume()
|
||||
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
return cmd, nil, &hst.AppError{Step: "start setuid wrapper", Err: err}
|
||||
}
|
||||
if err := shimPipe[0].Close(); err != nil {
|
||||
msg.Verbose(err)
|
||||
}
|
||||
|
||||
*startTime = time.Now().UTC()
|
||||
return cmd, shimPipe, nil
|
||||
return cmd, shimPipe[1], nil
|
||||
}
|
||||
|
||||
// serveShim serves outcomeState through the shim setup pipe.
|
||||
@@ -411,11 +415,11 @@ func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
|
||||
msg.Verbose(err.Error())
|
||||
}
|
||||
if err := gob.NewEncoder(shimPipe).Encode(state); err != nil {
|
||||
_ = shimPipe.Close()
|
||||
msg.Resume()
|
||||
return &hst.AppError{Step: "transmit shim config", Err: err}
|
||||
}
|
||||
_ = shimPipe.Close()
|
||||
return nil
|
||||
return shimPipe.Close()
|
||||
}
|
||||
|
||||
// printMessageError prints the error message according to [message.GetMessage],
|
||||
|
||||
@@ -18,7 +18,13 @@ import (
|
||||
func IsPollDescriptor(fd uintptr) bool
|
||||
|
||||
// Main runs an app according to [hst.Config] and terminates. Main does not return.
|
||||
func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
func Main(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
config *hst.Config,
|
||||
flags int,
|
||||
fd int,
|
||||
) {
|
||||
// avoids runtime internals or standard streams
|
||||
if fd >= 0 {
|
||||
if IsPollDescriptor(uintptr(fd)) || fd < 3 {
|
||||
@@ -34,7 +40,7 @@ func Main(ctx context.Context, msg message.Msg, config *hst.Config, fd int) {
|
||||
k := outcome{syscallDispatcher: direct{msg}}
|
||||
|
||||
finaliseTime := time.Now()
|
||||
if err := k.finalise(ctx, msg, &id, config); err != nil {
|
||||
if err := k.finalise(ctx, msg, &id, config, flags); err != nil {
|
||||
printMessageError(msg.GetLogger().Fatalln, "cannot seal app:", err)
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
@@ -288,7 +288,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
},
|
||||
Filter: true,
|
||||
},
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
|
||||
Container: &hst.ContainerConfig{
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
@@ -427,7 +427,7 @@ func TestOutcomeRun(t *testing.T) {
|
||||
DirectPipeWire: true,
|
||||
|
||||
ID: "org.chromium.Chromium",
|
||||
Enablements: hst.NewEnablements(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Enablements: new(hst.EWayland | hst.EDBus | hst.EPipeWire | hst.EPulse),
|
||||
Container: &hst.ContainerConfig{
|
||||
Env: nil,
|
||||
Filesystem: []hst.FilesystemConfigJSON{
|
||||
|
||||
@@ -20,6 +20,7 @@ import (
|
||||
"hakurei.app/ext"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/pipewire"
|
||||
"hakurei.app/message"
|
||||
)
|
||||
@@ -197,7 +198,7 @@ func shimEntrypoint(k syscallDispatcher) {
|
||||
if errors.Is(err, syscall.EBADF) {
|
||||
k.fatal("invalid config descriptor")
|
||||
}
|
||||
if errors.Is(err, container.ErrReceiveEnv) {
|
||||
if errors.Is(err, params.ErrReceiveEnv) {
|
||||
k.fatal(shimEnv + " not set")
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/env"
|
||||
"hakurei.app/internal/params"
|
||||
"hakurei.app/internal/stub"
|
||||
)
|
||||
|
||||
@@ -172,7 +173,7 @@ func TestShimEntrypoint(t *testing.T) {
|
||||
call("setDumpable", stub.ExpectArgs{uintptr(ext.SUID_DUMP_DISABLE)}, nil, nil),
|
||||
call("getppid", stub.ExpectArgs{}, 0xbad, nil),
|
||||
call("setupContSignal", stub.ExpectArgs{0xbad}, 0, nil),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, container.ErrReceiveEnv),
|
||||
call("receive", stub.ExpectArgs{"HAKUREI_SHIM", outcomeState{}, nil}, nil, params.ErrReceiveEnv),
|
||||
call("fatal", stub.ExpectArgs{[]any{"HAKUREI_SHIM not set"}}, nil, nil),
|
||||
|
||||
// deferred
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
"errors"
|
||||
"io/fs"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
@@ -165,9 +165,9 @@ func (s *spFilesystemOp) toSystem(state *outcomeStateSys) error {
|
||||
}
|
||||
for _, pair := range entry.Values {
|
||||
if pair[0] == "path" {
|
||||
if path.IsAbs(pair[1]) {
|
||||
if filepath.IsAbs(pair[1]) {
|
||||
// get parent dir of socket
|
||||
dir := path.Dir(pair[1])
|
||||
dir := filepath.Dir(pair[1])
|
||||
if dir == "." || dir == fhs.Root {
|
||||
state.msg.Verbosef("dbus socket %q is in an unusual location", pair[1])
|
||||
}
|
||||
@@ -290,7 +290,9 @@ func (s *spFilesystemOp) toContainer(state *outcomeStateParams) error {
|
||||
if state.Container.Flags&hst.FDevice == 0 {
|
||||
state.params.Remount(fhs.AbsDev, syscall.MS_RDONLY)
|
||||
}
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
if !state.as.NoRemountRoot {
|
||||
state.params.Remount(fhs.AbsRoot, syscall.MS_RDONLY)
|
||||
}
|
||||
|
||||
state.params.Env = make([]string, 0, len(state.env))
|
||||
for key, value := range state.env {
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestSpPulseOp(t *testing.T) {
|
||||
newConfig := func() *hst.Config {
|
||||
config := hst.Template()
|
||||
config.DirectPulse = true
|
||||
config.Enablements = hst.NewEnablements(hst.EPulse)
|
||||
config.Enablements = new(hst.EPulse)
|
||||
return config
|
||||
}
|
||||
|
||||
|
||||
42
internal/params/params.go
Normal file
42
internal/params/params.go
Normal file
@@ -0,0 +1,42 @@
|
||||
// Package params provides helpers for receiving setup payload from parent.
|
||||
package params
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"errors"
|
||||
"os"
|
||||
"strconv"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
// ErrReceiveEnv is returned by [Receive] if setup fd is not present in environment.
|
||||
var ErrReceiveEnv = errors.New("environment variable not set")
|
||||
|
||||
// Receive retrieves setup fd from the environment and receives params.
|
||||
//
|
||||
// The file descriptor written to the value pointed to by fdp must not be passed
|
||||
// to any system calls. It is made available for ordering file descriptor only.
|
||||
func Receive(key string, v any, fdp *int) (func() error, error) {
|
||||
var setup *os.File
|
||||
|
||||
if s, ok := os.LookupEnv(key); !ok {
|
||||
return nil, ErrReceiveEnv
|
||||
} else {
|
||||
if fd, err := strconv.Atoi(s); err != nil {
|
||||
if _err := errors.Unwrap(err); _err != nil {
|
||||
err = _err
|
||||
}
|
||||
return nil, err
|
||||
} else {
|
||||
setup = os.NewFile(uintptr(fd), "setup")
|
||||
if setup == nil {
|
||||
return nil, syscall.EDOM
|
||||
}
|
||||
if fdp != nil {
|
||||
*fdp = fd
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return setup.Close, gob.NewDecoder(setup).Decode(v)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
package container_test
|
||||
package params_test
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
@@ -9,7 +9,7 @@ import (
|
||||
"syscall"
|
||||
"testing"
|
||||
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/internal/params"
|
||||
)
|
||||
|
||||
func TestSetupReceive(t *testing.T) {
|
||||
@@ -30,8 +30,8 @@ func TestSetupReceive(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, container.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, container.ErrReceiveEnv)
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, params.ErrReceiveEnv) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, params.ErrReceiveEnv)
|
||||
}
|
||||
})
|
||||
|
||||
@@ -39,7 +39,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
const key = "TEST_ENV_FORMAT"
|
||||
t.Setenv(key, "")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, strconv.ErrSyntax) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, strconv.ErrSyntax)
|
||||
}
|
||||
})
|
||||
@@ -48,7 +48,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
const key = "TEST_ENV_RANGE"
|
||||
t.Setenv(key, "-1")
|
||||
|
||||
if _, err := container.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
if _, err := params.Receive(key, nil, nil); !errors.Is(err, syscall.EDOM) {
|
||||
t.Errorf("Receive: error = %v, want %v", err, syscall.EDOM)
|
||||
}
|
||||
})
|
||||
@@ -60,16 +60,22 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
encoderDone := make(chan error, 1)
|
||||
extraFiles := make([]*os.File, 0, 1)
|
||||
deadline, _ := t.Deadline()
|
||||
if fd, f, err := container.Setup(&extraFiles); err != nil {
|
||||
if r, w, err := os.Pipe(); err != nil {
|
||||
t.Fatalf("Setup: error = %v", err)
|
||||
} else if fd != 3 {
|
||||
t.Fatalf("Setup: fd = %d, want 3", fd)
|
||||
} else {
|
||||
if err = f.SetDeadline(deadline); err != nil {
|
||||
t.Fatal(err.Error())
|
||||
t.Cleanup(func() {
|
||||
if err = errors.Join(r.Close(), w.Close()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
|
||||
extraFiles = append(extraFiles, r)
|
||||
if deadline, ok := t.Deadline(); ok {
|
||||
if err = w.SetDeadline(deadline); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
go func() { encoderDone <- gob.NewEncoder(f).Encode(payload) }()
|
||||
go func() { encoderDone <- gob.NewEncoder(w).Encode(payload) }()
|
||||
}
|
||||
|
||||
if len(extraFiles) != 1 {
|
||||
@@ -87,13 +93,13 @@ func TestSetupReceive(t *testing.T) {
|
||||
|
||||
var (
|
||||
gotPayload []uint64
|
||||
fdp *uintptr
|
||||
fdp *int
|
||||
)
|
||||
if !useNilFdp {
|
||||
fdp = new(uintptr)
|
||||
fdp = new(int)
|
||||
}
|
||||
var closeFile func() error
|
||||
if f, err := container.Receive(key, &gotPayload, fdp); err != nil {
|
||||
if f, err := params.Receive(key, &gotPayload, fdp); err != nil {
|
||||
t.Fatalf("Receive: error = %v", err)
|
||||
} else {
|
||||
closeFile = f
|
||||
@@ -103,7 +109,7 @@ func TestSetupReceive(t *testing.T) {
|
||||
}
|
||||
}
|
||||
if !useNilFdp {
|
||||
if int(*fdp) != dupFd {
|
||||
if *fdp != dupFd {
|
||||
t.Errorf("Fd: %d, want %d", *fdp, dupFd)
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
@@ -973,23 +973,23 @@ func connectName(name string, manager bool) (conn Conn, err error) {
|
||||
return connectName(name+"-manager", false)
|
||||
}
|
||||
|
||||
if path.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
||||
if filepath.IsAbs(name) || (len(name) > 0 && name[0] == '@') {
|
||||
return Dial(name)
|
||||
} else {
|
||||
runtimeDir, ok := os.LookupEnv("PIPEWIRE_RUNTIME_DIR")
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
if !ok || !filepath.IsAbs(runtimeDir) {
|
||||
runtimeDir, ok = os.LookupEnv("XDG_RUNTIME_DIR")
|
||||
}
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
if !ok || !filepath.IsAbs(runtimeDir) {
|
||||
// this is cargo culted from windows stuff and has no effect normally;
|
||||
// keeping it to maintain compatibility in case someone sets this
|
||||
runtimeDir, ok = os.LookupEnv("USERPROFILE")
|
||||
}
|
||||
|
||||
if !ok || !path.IsAbs(runtimeDir) {
|
||||
if !ok || !filepath.IsAbs(runtimeDir) {
|
||||
runtimeDir = DEFAULT_SYSTEM_RUNTIME_DIR
|
||||
}
|
||||
return Dial(path.Join(runtimeDir, name))
|
||||
return Dial(filepath.Join(runtimeDir, name))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -27,6 +27,31 @@ func TestFlatten(t *testing.T) {
|
||||
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},
|
||||
@@ -159,6 +184,32 @@ func TestFlatten(t *testing.T) {
|
||||
{Mode: fs.ModeDir | 0700, Path: "work"},
|
||||
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d"), nil},
|
||||
|
||||
{"sample no assume checksum", fstest.MapFS{
|
||||
".": {Mode: fs.ModeDir | 0700},
|
||||
|
||||
"checksum": {Mode: fs.ModeDir | 0700},
|
||||
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M": {Mode: fs.ModeDir | 0500},
|
||||
"checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check": {Mode: 0400, Data: []byte{}},
|
||||
|
||||
"identifier": {Mode: fs.ModeDir | 0700},
|
||||
"identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
|
||||
"identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA": {Mode: fs.ModeSymlink | 0777, Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
|
||||
|
||||
"work": {Mode: fs.ModeDir | 0700},
|
||||
}, []pkg.FlatEntry{
|
||||
{Mode: fs.ModeDir | 0700, Path: "."},
|
||||
|
||||
{Mode: fs.ModeDir | 0700, Path: "checksum"},
|
||||
{Mode: fs.ModeDir | 0500, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M"},
|
||||
{Mode: 0400, Path: "checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M/check", Data: []byte{}},
|
||||
|
||||
{Mode: fs.ModeDir | 0700, Path: "identifier"},
|
||||
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
|
||||
{Mode: fs.ModeSymlink | 0777, Path: "identifier/_wEAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", Data: []byte("../checksum/Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")},
|
||||
|
||||
{Mode: fs.ModeDir | 0700, Path: "work"},
|
||||
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w"), nil},
|
||||
|
||||
{"sample tar step unpack", fstest.MapFS{
|
||||
".": {Mode: fs.ModeDir | 0500},
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"slices"
|
||||
"strconv"
|
||||
"syscall"
|
||||
@@ -27,6 +27,11 @@ import (
|
||||
// AbsWork is the container pathname [TContext.GetWorkDir] is mounted on.
|
||||
var AbsWork = fhs.AbsRoot.Append("work/")
|
||||
|
||||
// EnvJobs is the name of the environment variable holding a decimal
|
||||
// representation of the preferred job count. Its value must not affect cure
|
||||
// outcome.
|
||||
const EnvJobs = "CURE_JOBS"
|
||||
|
||||
// ExecPath is a slice of [Artifact] and the [check.Absolute] pathname to make
|
||||
// it available at under in the container.
|
||||
type ExecPath struct {
|
||||
@@ -40,9 +45,6 @@ type ExecPath struct {
|
||||
W bool
|
||||
}
|
||||
|
||||
// SetSchedIdle is whether to set [ext.SCHED_IDLE] scheduling priority.
|
||||
var SetSchedIdle bool
|
||||
|
||||
// GetArtifactFunc is the function signature of [FContext.GetArtifact].
|
||||
type GetArtifactFunc func(Artifact) (*check.Absolute, unique.Handle[Checksum])
|
||||
|
||||
@@ -189,7 +191,7 @@ func NewExec(
|
||||
paths ...ExecPath,
|
||||
) Artifact {
|
||||
if name == "" {
|
||||
name = "exec-" + path.Base(pathname.String())
|
||||
name = "exec-" + filepath.Base(pathname.String())
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = ExecTimeoutDefault
|
||||
@@ -400,6 +402,7 @@ const SeccompPresets = std.PresetStrict &
|
||||
func (a *execArtifact) makeContainer(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
flags, jobs int,
|
||||
hostNet bool,
|
||||
temp, work *check.Absolute,
|
||||
getArtifact GetArtifactFunc,
|
||||
@@ -426,15 +429,16 @@ func (a *execArtifact) makeContainer(
|
||||
z.SeccompFlags |= seccomp.AllowMultiarch
|
||||
z.ParentPerm = 0700
|
||||
z.HostNet = hostNet
|
||||
z.HostAbstract = flags&CHostAbstract != 0
|
||||
z.Hostname = "cure"
|
||||
z.SetScheduler = SetSchedIdle
|
||||
z.SetScheduler = flags&CSchedIdle != 0
|
||||
z.SchedPolicy = ext.SCHED_IDLE
|
||||
if z.HostNet {
|
||||
z.Hostname = "cure-net"
|
||||
}
|
||||
z.Uid, z.Gid = (1<<10)-1, (1<<10)-1
|
||||
|
||||
z.Dir, z.Env, z.Path, z.Args = a.dir, a.env, a.path, a.args
|
||||
z.Dir, z.Path, z.Args = a.dir, a.path, a.args
|
||||
z.Env = slices.Concat(a.env, []string{EnvJobs + "=" + strconv.Itoa(jobs)})
|
||||
z.Grow(len(a.paths) + 4)
|
||||
|
||||
for i, b := range a.paths {
|
||||
@@ -563,6 +567,8 @@ func (c *Cache) EnterExec(
|
||||
var z *container.Container
|
||||
z, err = e.makeContainer(
|
||||
ctx, c.msg,
|
||||
c.flags,
|
||||
c.jobs,
|
||||
hostNet,
|
||||
temp, work,
|
||||
func(a Artifact) (*check.Absolute, unique.Handle[Checksum]) {
|
||||
@@ -579,6 +585,11 @@ func (c *Cache) EnterExec(
|
||||
z.Stdin, z.Stdout, z.Stderr = stdin, stdout, stderr
|
||||
z.Path, z.Args = path, args
|
||||
z.RetainSession = retainSession
|
||||
if stdin == os.Stdin {
|
||||
if s, ok := os.LookupEnv("TERM"); ok {
|
||||
z.Env = append(z.Env, "TERM="+s)
|
||||
}
|
||||
}
|
||||
|
||||
if err = z.Start(); err != nil {
|
||||
return
|
||||
@@ -597,7 +608,7 @@ func (a *execArtifact) cure(f *FContext, hostNet bool) (err error) {
|
||||
msg := f.GetMessage()
|
||||
var z *container.Container
|
||||
if z, err = a.makeContainer(
|
||||
ctx, msg, hostNet,
|
||||
ctx, msg, f.cache.flags, f.GetJobs(), hostNet,
|
||||
f.GetTempDir(), f.GetWorkDir(),
|
||||
f.GetArtifact,
|
||||
f.cache.Ident,
|
||||
|
||||
@@ -33,8 +33,7 @@ func TestExec(t *testing.T) {
|
||||
)
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"offline", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"offline", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
@@ -111,8 +110,7 @@ func TestExec(t *testing.T) {
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("Q5DluWQCAeohLoiGRImurwFp3vdz9IfQCoj7Fuhh73s4KQPRHpEQEnHTdNHmB8Fx")},
|
||||
|
||||
{"net", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"net", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
wantChecksum := pkg.MustDecode(
|
||||
@@ -146,8 +144,7 @@ func TestExec(t *testing.T) {
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("bPYvvqxpfV7xcC1EptqyKNK1klLJgYHMDkzBcoOyK6j_Aj5hb0mXNPwTwPSK5F6Z")},
|
||||
|
||||
{"overlay root", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"overlay root", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
@@ -172,8 +169,7 @@ func TestExec(t *testing.T) {
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("PO2DSSCa4yoSgEYRcCSZfQfwow1yRigL3Ry-hI0RDI4aGuFBha-EfXeSJnG_5_Rl")},
|
||||
|
||||
{"overlay work", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"overlay work", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
@@ -203,8 +199,7 @@ func TestExec(t *testing.T) {
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("iaRt6l_Wm2n-h5UsDewZxQkCmjZjyL8r7wv32QT2kyV55-Lx09Dq4gfg9BiwPnKs")},
|
||||
|
||||
{"multiple layers", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"multiple layers", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
@@ -256,8 +251,7 @@ func TestExec(t *testing.T) {
|
||||
testtoolDestroy(t, base, c)
|
||||
}, pkg.MustDecode("O2YzyR7IUGU5J2CADy0hUZ3A5NkP_Vwzs4UadEdn2oMZZVWRtH0xZGJ3HXiimTnZ")},
|
||||
|
||||
{"overlay layer promotion", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
{"overlay layer promotion", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
testtool, testtoolDestroy := newTesttool()
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
|
||||
@@ -11,9 +11,7 @@ func TestFile(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
|
||||
{"file", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
cureMany(t, c, []cureStep{
|
||||
{"short", pkg.NewFile("null", []byte{0}), base.Append(
|
||||
"identifier",
|
||||
|
||||
@@ -3,7 +3,6 @@ package pkg
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha512"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
@@ -11,6 +10,7 @@ import (
|
||||
"io"
|
||||
"slices"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"unique"
|
||||
"unsafe"
|
||||
@@ -39,22 +39,45 @@ func panicToError(errP *error) {
|
||||
}
|
||||
}
|
||||
|
||||
// irCache implements [IRCache].
|
||||
type irCache struct {
|
||||
// Artifact to [unique.Handle] of identifier cache.
|
||||
artifact sync.Map
|
||||
// Identifier free list, must not be accessed directly.
|
||||
identPool sync.Pool
|
||||
}
|
||||
|
||||
// zeroIRCache returns the initialised value of irCache.
|
||||
func zeroIRCache() irCache {
|
||||
return irCache{
|
||||
identPool: sync.Pool{New: func() any { return new(extIdent) }},
|
||||
}
|
||||
}
|
||||
|
||||
// IRCache provides memory management and caching primitives for IR and
|
||||
// identifier operations against [Artifact] implementations.
|
||||
//
|
||||
// The zero value is not safe for use.
|
||||
type IRCache struct{ irCache }
|
||||
|
||||
// NewIR returns the address of a new [IRCache].
|
||||
func NewIR() *IRCache {
|
||||
return &IRCache{zeroIRCache()}
|
||||
}
|
||||
|
||||
// IContext is passed to [Artifact.Params] and provides methods for writing
|
||||
// values to the IR writer. It does not expose the underlying [io.Writer].
|
||||
//
|
||||
// IContext is valid until [Artifact.Params] returns.
|
||||
type IContext struct {
|
||||
// Address of underlying [Cache], should be zeroed or made unusable after
|
||||
// Address of underlying irCache, should be zeroed or made unusable after
|
||||
// [Artifact.Params] returns and must not be exposed directly.
|
||||
cache *Cache
|
||||
ic *irCache
|
||||
// Written to by various methods, should be zeroed after [Artifact.Params]
|
||||
// returns and must not be exposed directly.
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [context.Context].
|
||||
func (i *IContext) Unwrap() context.Context { return i.cache.ctx }
|
||||
|
||||
// irZero is a zero IR word.
|
||||
var irZero [wordSize]byte
|
||||
|
||||
@@ -136,11 +159,11 @@ func (i *IContext) mustWrite(p []byte) {
|
||||
// WriteIdent is not defined for an [Artifact] not part of the slice returned by
|
||||
// [Artifact.Dependencies].
|
||||
func (i *IContext) WriteIdent(a Artifact) {
|
||||
buf := i.cache.getIdentBuf()
|
||||
defer i.cache.putIdentBuf(buf)
|
||||
buf := i.ic.getIdentBuf()
|
||||
defer i.ic.putIdentBuf(buf)
|
||||
|
||||
IRKindIdent.encodeHeader(0).put(buf[:])
|
||||
*(*ID)(buf[wordSize:]) = i.cache.Ident(a).Value()
|
||||
*(*ID)(buf[wordSize:]) = i.ic.Ident(a).Value()
|
||||
i.mustWrite(buf[:])
|
||||
}
|
||||
|
||||
@@ -183,19 +206,19 @@ func (i *IContext) WriteString(s string) {
|
||||
|
||||
// Encode writes a deterministic, efficient representation of a to w and returns
|
||||
// the first non-nil error encountered while writing to w.
|
||||
func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
func (ic *irCache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
deps := a.Dependencies()
|
||||
idents := make([]*extIdent, len(deps))
|
||||
for i, d := range deps {
|
||||
dbuf, did := c.unsafeIdent(d, true)
|
||||
dbuf, did := ic.unsafeIdent(d, true)
|
||||
if dbuf == nil {
|
||||
dbuf = c.getIdentBuf()
|
||||
dbuf = ic.getIdentBuf()
|
||||
binary.LittleEndian.PutUint64(dbuf[:], uint64(d.Kind()))
|
||||
*(*ID)(dbuf[wordSize:]) = did.Value()
|
||||
} else {
|
||||
c.storeIdent(d, dbuf)
|
||||
ic.storeIdent(d, dbuf)
|
||||
}
|
||||
defer c.putIdentBuf(dbuf)
|
||||
defer ic.putIdentBuf(dbuf)
|
||||
idents[i] = dbuf
|
||||
}
|
||||
slices.SortFunc(idents, func(a, b *extIdent) int {
|
||||
@@ -221,10 +244,10 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
}
|
||||
|
||||
func() {
|
||||
i := IContext{c, w}
|
||||
i := IContext{ic, w}
|
||||
|
||||
defer panicToError(&err)
|
||||
defer func() { i.cache, i.w = nil, nil }()
|
||||
defer func() { i.ic, i.w = nil, nil }()
|
||||
|
||||
a.Params(&i)
|
||||
}()
|
||||
@@ -233,7 +256,7 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
}
|
||||
|
||||
var f IREndFlag
|
||||
kcBuf := c.getIdentBuf()
|
||||
kcBuf := ic.getIdentBuf()
|
||||
sz := wordSize
|
||||
if kc, ok := a.(KnownChecksum); ok {
|
||||
f |= IREndKnownChecksum
|
||||
@@ -243,13 +266,13 @@ func (c *Cache) Encode(w io.Writer, a Artifact) (err error) {
|
||||
IRKindEnd.encodeHeader(uint32(f)).put(kcBuf[:])
|
||||
|
||||
_, err = w.Write(kcBuf[:sz])
|
||||
c.putIdentBuf(kcBuf)
|
||||
ic.putIdentBuf(kcBuf)
|
||||
return
|
||||
}
|
||||
|
||||
// encodeAll implements EncodeAll by recursively encoding dependencies and
|
||||
// performs deduplication by value via the encoded map.
|
||||
func (c *Cache) encodeAll(
|
||||
func (ic *irCache) encodeAll(
|
||||
w io.Writer,
|
||||
a Artifact,
|
||||
encoded map[Artifact]struct{},
|
||||
@@ -259,13 +282,13 @@ func (c *Cache) encodeAll(
|
||||
}
|
||||
|
||||
for _, d := range a.Dependencies() {
|
||||
if err = c.encodeAll(w, d, encoded); err != nil {
|
||||
if err = ic.encodeAll(w, d, encoded); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
encoded[a] = struct{}{}
|
||||
return c.Encode(w, a)
|
||||
return ic.Encode(w, a)
|
||||
}
|
||||
|
||||
// EncodeAll writes a self-describing IR stream of a to w and returns the first
|
||||
@@ -283,8 +306,8 @@ func (c *Cache) encodeAll(
|
||||
// the ident cache, nor does it contribute identifiers it computes back to the
|
||||
// ident cache. Because of this, multiple invocations of EncodeAll will have
|
||||
// similar cost and does not amortise when combined with a call to Cure.
|
||||
func (c *Cache) EncodeAll(w io.Writer, a Artifact) error {
|
||||
return c.encodeAll(w, a, make(map[Artifact]struct{}))
|
||||
func (ic *irCache) EncodeAll(w io.Writer, a Artifact) error {
|
||||
return ic.encodeAll(w, a, make(map[Artifact]struct{}))
|
||||
}
|
||||
|
||||
// ErrRemainingIR is returned for a [IRReadFunc] that failed to call
|
||||
@@ -409,6 +432,12 @@ func (e InvalidKindError) Error() string {
|
||||
// register is not safe for concurrent use. register must not be called after
|
||||
// the first instance of [Cache] has been opened.
|
||||
func register(k Kind, f IRReadFunc) {
|
||||
openMu.Lock()
|
||||
defer openMu.Unlock()
|
||||
|
||||
if opened {
|
||||
panic("attempting to register after open")
|
||||
}
|
||||
if _, ok := irArtifact[k]; ok {
|
||||
panic("attempting to register " + strconv.Itoa(int(k)) + " twice")
|
||||
}
|
||||
|
||||
@@ -85,7 +85,7 @@ func TestIRRoundtrip(t *testing.T) {
|
||||
testCasesCache := make([]cacheTestCase, len(testCases))
|
||||
for i, tc := range testCases {
|
||||
want := tc.a
|
||||
testCasesCache[i] = cacheTestCase{tc.name, nil,
|
||||
testCasesCache[i] = cacheTestCase{tc.name, 0, nil,
|
||||
func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
r, w := io.Pipe()
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
"testing/fstest"
|
||||
"unique"
|
||||
"unsafe"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/internal/pkg"
|
||||
@@ -32,21 +31,15 @@ func TestHTTPGet(t *testing.T) {
|
||||
}))
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"direct", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
var r pkg.RContext
|
||||
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
|
||||
reflect.NewAt(
|
||||
rCacheVal.Type(),
|
||||
unsafe.Pointer(rCacheVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(c))
|
||||
|
||||
{"direct", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
r := newRContext(t, c)
|
||||
f := pkg.NewHTTPGet(
|
||||
&client,
|
||||
"file:///testdata",
|
||||
testdataChecksum.Value(),
|
||||
)
|
||||
var got []byte
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -65,7 +58,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
wantErrMismatch := &pkg.ChecksumMismatchError{
|
||||
Got: testdataChecksum.Value(),
|
||||
}
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -76,7 +69,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
}
|
||||
|
||||
// check fallback validation
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if err = rc.Close(); !reflect.DeepEqual(err, wantErrMismatch) {
|
||||
t.Fatalf("Close: error = %#v, want %#v", err, wantErrMismatch)
|
||||
@@ -89,18 +82,13 @@ func TestHTTPGet(t *testing.T) {
|
||||
pkg.Checksum{},
|
||||
)
|
||||
wantErrNotFound := pkg.ResponseStatusError(http.StatusNotFound)
|
||||
if _, err := f.Cure(&r); !reflect.DeepEqual(err, wantErrNotFound) {
|
||||
if _, err := f.Cure(r); !reflect.DeepEqual(err, wantErrNotFound) {
|
||||
t.Fatalf("Cure: error = %#v, want %#v", err, wantErrNotFound)
|
||||
}
|
||||
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
|
||||
|
||||
{"cure", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
var r pkg.RContext
|
||||
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
|
||||
reflect.NewAt(
|
||||
rCacheVal.Type(),
|
||||
unsafe.Pointer(rCacheVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(c))
|
||||
{"cure", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
r := newRContext(t, c)
|
||||
|
||||
f := pkg.NewHTTPGet(
|
||||
&client,
|
||||
@@ -120,7 +108,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
}
|
||||
|
||||
var got []byte
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
@@ -136,7 +124,7 @@ func TestHTTPGet(t *testing.T) {
|
||||
"file:///testdata",
|
||||
testdataChecksum.Value(),
|
||||
)
|
||||
if rc, err := f.Cure(&r); err != nil {
|
||||
if rc, err := f.Cure(r); err != nil {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
} else if got, err = io.ReadAll(rc); err != nil {
|
||||
t.Fatalf("ReadAll: error = %v", err)
|
||||
|
||||
@@ -13,13 +13,12 @@ import (
|
||||
"hash"
|
||||
"io"
|
||||
"io/fs"
|
||||
"iter"
|
||||
"maps"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
@@ -72,8 +71,70 @@ func MustDecode(s string) (checksum Checksum) {
|
||||
return
|
||||
}
|
||||
|
||||
var (
|
||||
// extension is a string uniquely identifying a set of custom [Artifact]
|
||||
// implementations registered by calling [Register].
|
||||
extension string
|
||||
|
||||
// openMu synchronises access to global state for initialisation.
|
||||
openMu sync.Mutex
|
||||
// opened is false if [Open] was never called.
|
||||
opened bool
|
||||
)
|
||||
|
||||
// Extension returns a string uniquely identifying the currently registered set
|
||||
// of custom [Artifact], or the zero value if none was registered.
|
||||
func Extension() string { return extension }
|
||||
|
||||
// ValidExtension returns whether s is valid for use in a call to SetExtension.
|
||||
func ValidExtension(s string) bool {
|
||||
if l := len(s); l == 0 || l > 128 {
|
||||
return false
|
||||
}
|
||||
for _, v := range s {
|
||||
if v < 'a' || v > 'z' {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// ErrInvalidExtension is returned for a variant identification string for which
|
||||
// [ValidExtension] returns false.
|
||||
var ErrInvalidExtension = errors.New("invalid extension variant identification string")
|
||||
|
||||
// SetExtension sets the extension variant identification string. SetExtension
|
||||
// must be called before [Open] if custom [Artifact] implementations had been
|
||||
// recorded by calling [Register].
|
||||
//
|
||||
// The variant identification string must be between 1 and 128 bytes long and
|
||||
// consists of only bytes between 'a' and 'z'.
|
||||
//
|
||||
// SetExtension is not safe for concurrent use. SetExtension is called at most
|
||||
// once and must not be called after the first instance of Cache has been opened.
|
||||
func SetExtension(s string) {
|
||||
openMu.Lock()
|
||||
defer openMu.Unlock()
|
||||
|
||||
if opened {
|
||||
panic("attempting to set extension after open")
|
||||
}
|
||||
if extension != "" {
|
||||
panic("attempting to set extension twice")
|
||||
}
|
||||
if !ValidExtension(s) {
|
||||
panic(ErrInvalidExtension)
|
||||
}
|
||||
extension = s
|
||||
statusHeader = makeStatusHeader(s)
|
||||
}
|
||||
|
||||
// common holds elements and receives methods shared between different contexts.
|
||||
type common struct {
|
||||
// Context specific to this [Artifact]. The toplevel context in [Cache] must
|
||||
// not be exposed directly.
|
||||
ctx context.Context
|
||||
|
||||
// Address of underlying [Cache], should be zeroed or made unusable after
|
||||
// Cure returns and must not be exposed directly.
|
||||
cache *Cache
|
||||
@@ -100,19 +161,27 @@ type TContext struct {
|
||||
common
|
||||
}
|
||||
|
||||
// statusHeader is the header written to all status files in dirStatus.
|
||||
var statusHeader = func() string {
|
||||
// makeStatusHeader creates the header written to every status file. This should
|
||||
// not be called directly, its result is stored in statusHeader and will not
|
||||
// change after the first [Cache] is opened.
|
||||
func makeStatusHeader(extension string) string {
|
||||
s := programName
|
||||
if v := info.Version(); v != info.FallbackVersion {
|
||||
s += " " + v
|
||||
}
|
||||
if extension != "" {
|
||||
s += " with " + extension + " extensions"
|
||||
}
|
||||
s += " (" + runtime.GOARCH + ")"
|
||||
if name, err := os.Hostname(); err == nil {
|
||||
s += " on " + name
|
||||
}
|
||||
s += "\n\n"
|
||||
return s
|
||||
}()
|
||||
}
|
||||
|
||||
// statusHeader is the header written to all status files in dirStatus.
|
||||
var statusHeader = makeStatusHeader("")
|
||||
|
||||
// prepareStatus initialises the status file once.
|
||||
func (t *TContext) prepareStatus() error {
|
||||
@@ -185,11 +254,15 @@ func (t *TContext) destroy(errP *error) {
|
||||
}
|
||||
|
||||
// Unwrap returns the underlying [context.Context].
|
||||
func (c *common) Unwrap() context.Context { return c.cache.ctx }
|
||||
func (c *common) Unwrap() context.Context { return c.ctx }
|
||||
|
||||
// GetMessage returns [message.Msg] held by the underlying [Cache].
|
||||
func (c *common) GetMessage() message.Msg { return c.cache.msg }
|
||||
|
||||
// GetJobs returns the preferred number of jobs to run, when applicable. Its
|
||||
// value must not affect cure outcome.
|
||||
func (c *common) GetJobs() int { return c.cache.jobs }
|
||||
|
||||
// GetWorkDir returns a pathname to a directory which [Artifact] is expected to
|
||||
// write its output to. This is not the final resting place of the [Artifact]
|
||||
// and this pathname should not be directly referred to in the final contents.
|
||||
@@ -209,11 +282,11 @@ func (t *TContext) GetTempDir() *check.Absolute { return t.temp }
|
||||
// [ChecksumMismatchError], or the underlying implementation may block on Close.
|
||||
func (c *common) Open(a Artifact) (r io.ReadCloser, err error) {
|
||||
if f, ok := a.(FileArtifact); ok {
|
||||
return c.cache.openFile(f)
|
||||
return c.cache.openFile(c.ctx, f)
|
||||
}
|
||||
|
||||
var pathname *check.Absolute
|
||||
if pathname, _, err = c.cache.Cure(a); err != nil {
|
||||
if pathname, _, err = c.cache.cure(a, true); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -332,23 +405,6 @@ type FloodArtifact interface {
|
||||
Artifact
|
||||
}
|
||||
|
||||
// Flood returns an iterator over the dependency tree of an [Artifact].
|
||||
func Flood(a Artifact) iter.Seq[Artifact] {
|
||||
return func(yield func(Artifact) bool) {
|
||||
for _, d := range a.Dependencies() {
|
||||
if !yield(d) {
|
||||
return
|
||||
}
|
||||
|
||||
for d0 := range Flood(d) {
|
||||
if !yield(d0) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TrivialArtifact refers to an [Artifact] that cures without requiring that
|
||||
// any other [Artifact] is cured before it. Its dependency tree is ignored after
|
||||
// computing its identifier.
|
||||
@@ -391,6 +447,9 @@ type KnownChecksum interface {
|
||||
}
|
||||
|
||||
// FileArtifact refers to an [Artifact] backed by a single file.
|
||||
//
|
||||
// FileArtifact does not support fine-grained cancellation. Its context is
|
||||
// inherited from the first [TrivialArtifact] or [FloodArtifact] that opens it.
|
||||
type FileArtifact interface {
|
||||
// Cure returns [io.ReadCloser] of the full contents of [FileArtifact]. If
|
||||
// [FileArtifact] implements [KnownChecksum], Cure is responsible for
|
||||
@@ -435,6 +494,9 @@ const (
|
||||
// KindFile is the kind of [Artifact] returned by [NewFile].
|
||||
KindFile
|
||||
|
||||
// _kindEnd is the total number of kinds and does not denote a kind.
|
||||
_kindEnd
|
||||
|
||||
// KindCustomOffset is the first [Kind] value reserved for implementations
|
||||
// not from this package.
|
||||
KindCustomOffset = 1 << 31
|
||||
@@ -449,6 +511,9 @@ const (
|
||||
// fileLock is the file name appended to Cache.base for guaranteeing
|
||||
// exclusive access to the cache directory.
|
||||
fileLock = "lock"
|
||||
// fileVariant is the file name appended to Cache.base holding the variant
|
||||
// identification string set by a prior call to [SetExtension].
|
||||
fileVariant = "variant"
|
||||
|
||||
// dirIdentifier is the directory name appended to Cache.base for storing
|
||||
// artifacts named after their [ID].
|
||||
@@ -507,6 +572,77 @@ type pendingArtifactDep struct {
|
||||
*sync.WaitGroup
|
||||
}
|
||||
|
||||
const (
|
||||
// CValidateKnown arranges for [KnownChecksum] outcomes to be validated to
|
||||
// match its intended checksum.
|
||||
//
|
||||
// A correct implementation of [KnownChecksum] does not successfully cure
|
||||
// with output not matching its intended checksum. When an implementation
|
||||
// fails to perform this validation correctly, the on-disk format enters
|
||||
// an inconsistent state (correctable by [Cache.Scrub]).
|
||||
//
|
||||
// This flag causes [Cache.Cure] to always compute the checksum, and reject
|
||||
// a cure if it does not match the intended checksum.
|
||||
//
|
||||
// This behaviour significantly reduces performance and is not recommended
|
||||
// outside of testing a custom [Artifact] implementation.
|
||||
CValidateKnown = 1 << iota
|
||||
|
||||
// CSchedIdle arranges for the [ext.SCHED_IDLE] scheduling priority to be
|
||||
// set for [KindExec] and [KindExecNet] containers.
|
||||
CSchedIdle
|
||||
|
||||
// CAssumeChecksum enables the use of [KnownChecksum] for duplicate function
|
||||
// call suppression via the on-disk cache.
|
||||
//
|
||||
// This may cause incorrect cure outcome if an impossible checksum is
|
||||
// specified that matches an output already present in the on-disk cache.
|
||||
// This may be avoided by purposefully specifying a statistically
|
||||
// unattainable checksum, like the zero value.
|
||||
//
|
||||
// While this optimisation might seem appealing, it is almost never
|
||||
// applicable in real world use. Almost every time this path was taken, it
|
||||
// was caused by an incorrect checksum accidentally left behind while
|
||||
// bumping a package. Only enable this if you are really sure you need it.
|
||||
CAssumeChecksum
|
||||
|
||||
// CHostAbstract disables restriction of sandboxed processes from connecting
|
||||
// to an abstract UNIX socket created by a host process.
|
||||
//
|
||||
// This is considered less secure in some systems, but does not introduce
|
||||
// impurity due to [KindExecNet] being [KnownChecksum]. This flag exists
|
||||
// to support kernels without Landlock LSM enabled.
|
||||
CHostAbstract
|
||||
|
||||
// CPromoteVariant allows [pkg.Open] to promote an unextended on-disk cache
|
||||
// to the current extension variant. This is a one-way operation.
|
||||
CPromoteVariant
|
||||
)
|
||||
|
||||
// toplevel holds [context.WithCancel] over caller-supplied context, where all
|
||||
// [Artifact] context are derived from.
|
||||
type toplevel struct {
|
||||
ctx context.Context
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// newToplevel returns the address of a new toplevel via ctx.
|
||||
func newToplevel(ctx context.Context) *toplevel {
|
||||
var t toplevel
|
||||
t.ctx, t.cancel = context.WithCancel(ctx)
|
||||
return &t
|
||||
}
|
||||
|
||||
// pendingCure provides synchronisation and cancellation for pending cures.
|
||||
type pendingCure struct {
|
||||
// Closed on cure completion.
|
||||
done <-chan struct{}
|
||||
// Error outcome, safe to access after done is closed.
|
||||
err error
|
||||
// Cancels the corresponding cure.
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
// Cache is a support layer that implementations of [Artifact] can use to store
|
||||
// cured [Artifact] data in a content addressed fashion.
|
||||
type Cache struct {
|
||||
@@ -514,11 +650,10 @@ type Cache struct {
|
||||
// implementation and receives an equal amount of elements after.
|
||||
cures chan struct{}
|
||||
|
||||
// [context.WithCancel] over caller-supplied context, used by [Artifact] and
|
||||
// all dependency curing goroutines.
|
||||
ctx context.Context
|
||||
// Cancels ctx.
|
||||
cancel context.CancelFunc
|
||||
// Parent context which toplevel was derived from.
|
||||
parent context.Context
|
||||
// For deriving curing context, must not be accessed directly.
|
||||
toplevel atomic.Pointer[toplevel]
|
||||
// For waiting on dependency curing goroutines.
|
||||
wg sync.WaitGroup
|
||||
// Reports new cures and passed to [Artifact].
|
||||
@@ -526,17 +661,13 @@ type Cache struct {
|
||||
|
||||
// Directory where all [Cache] related files are placed.
|
||||
base *check.Absolute
|
||||
// Immutable cure options set by [Open].
|
||||
flags int
|
||||
// Immutable job count, when applicable.
|
||||
jobs int
|
||||
|
||||
// Whether to validate [FileArtifact.Cure] for a [KnownChecksum] file. This
|
||||
// significantly reduces performance.
|
||||
strict bool
|
||||
// Maximum size of a dependency graph.
|
||||
threshold uintptr
|
||||
|
||||
// Artifact to [unique.Handle] of identifier cache.
|
||||
artifact sync.Map
|
||||
// Identifier free list, must not be accessed directly.
|
||||
identPool sync.Pool
|
||||
// Must not be exposed directly.
|
||||
irCache
|
||||
|
||||
// Synchronises access to dirChecksum.
|
||||
checksumMu sync.RWMutex
|
||||
@@ -546,9 +677,11 @@ type Cache struct {
|
||||
// Identifier to error pair for unrecoverably faulted [Artifact].
|
||||
identErr map[unique.Handle[ID]]error
|
||||
// Pending identifiers, accessed through Cure for entries not in ident.
|
||||
identPending map[unique.Handle[ID]]<-chan struct{}
|
||||
identPending map[unique.Handle[ID]]*pendingCure
|
||||
// Synchronises access to ident and corresponding filesystem entries.
|
||||
identMu sync.RWMutex
|
||||
// Synchronises entry into Abort and Cure.
|
||||
abortMu sync.RWMutex
|
||||
|
||||
// Synchronises entry into exclusive artifacts for the cure method.
|
||||
exclMu sync.Mutex
|
||||
@@ -557,51 +690,37 @@ type Cache struct {
|
||||
|
||||
// Unlocks the on-filesystem cache. Must only be called from Close.
|
||||
unlock func()
|
||||
// Synchronises calls to Close.
|
||||
closeOnce sync.Once
|
||||
// Whether [Cache] is considered closed.
|
||||
closed bool
|
||||
// Synchronises calls to Abort and Close.
|
||||
closeMu sync.Mutex
|
||||
|
||||
// Whether EnterExec has not yet returned.
|
||||
inExec atomic.Bool
|
||||
}
|
||||
|
||||
// IsStrict returns whether the [Cache] strictly verifies checksums.
|
||||
func (c *Cache) IsStrict() bool { return c.strict }
|
||||
|
||||
// SetStrict sets whether the [Cache] strictly verifies checksums, even when
|
||||
// the implementation promises to validate them internally. This significantly
|
||||
// reduces performance and is not recommended outside of testing.
|
||||
//
|
||||
// This method is not safe for concurrent use with any other method.
|
||||
func (c *Cache) SetStrict(strict bool) { c.strict = strict }
|
||||
|
||||
// SetThreshold imposes a maximum size on the dependency graph, checked on every
|
||||
// call to Cure. The zero value disables this check entirely.
|
||||
//
|
||||
// This method is not safe for concurrent use with any other method.
|
||||
func (c *Cache) SetThreshold(threshold uintptr) { c.threshold = threshold }
|
||||
|
||||
// extIdent is a [Kind] concatenated with [ID].
|
||||
type extIdent [wordSize + len(ID{})]byte
|
||||
|
||||
// getIdentBuf returns the address of an extIdent for Ident.
|
||||
func (c *Cache) getIdentBuf() *extIdent { return c.identPool.Get().(*extIdent) }
|
||||
func (ic *irCache) getIdentBuf() *extIdent { return ic.identPool.Get().(*extIdent) }
|
||||
|
||||
// putIdentBuf adds buf to identPool.
|
||||
func (c *Cache) putIdentBuf(buf *extIdent) { c.identPool.Put(buf) }
|
||||
func (ic *irCache) putIdentBuf(buf *extIdent) { ic.identPool.Put(buf) }
|
||||
|
||||
// storeIdent adds an [Artifact] to the artifact cache.
|
||||
func (c *Cache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
|
||||
func (ic *irCache) storeIdent(a Artifact, buf *extIdent) unique.Handle[ID] {
|
||||
idu := unique.Make(ID(buf[wordSize:]))
|
||||
c.artifact.Store(a, idu)
|
||||
ic.artifact.Store(a, idu)
|
||||
return idu
|
||||
}
|
||||
|
||||
// Ident returns the identifier of an [Artifact].
|
||||
func (c *Cache) Ident(a Artifact) unique.Handle[ID] {
|
||||
buf, idu := c.unsafeIdent(a, false)
|
||||
func (ic *irCache) Ident(a Artifact) unique.Handle[ID] {
|
||||
buf, idu := ic.unsafeIdent(a, false)
|
||||
if buf != nil {
|
||||
idu = c.storeIdent(a, buf)
|
||||
c.putIdentBuf(buf)
|
||||
idu = ic.storeIdent(a, buf)
|
||||
ic.putIdentBuf(buf)
|
||||
}
|
||||
return idu
|
||||
}
|
||||
@@ -609,17 +728,17 @@ func (c *Cache) Ident(a Artifact) unique.Handle[ID] {
|
||||
// unsafeIdent implements Ident but returns the underlying buffer for a newly
|
||||
// computed identifier. Callers must return this buffer to identPool. encodeKind
|
||||
// is only a hint, kind may still be encoded in the buffer.
|
||||
func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
func (ic *irCache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
buf *extIdent,
|
||||
idu unique.Handle[ID],
|
||||
) {
|
||||
if id, ok := c.artifact.Load(a); ok {
|
||||
if id, ok := ic.artifact.Load(a); ok {
|
||||
idu = id.(unique.Handle[ID])
|
||||
return
|
||||
}
|
||||
|
||||
if ki, ok := a.(KnownIdent); ok {
|
||||
buf = c.getIdentBuf()
|
||||
buf = ic.getIdentBuf()
|
||||
if encodeKind {
|
||||
binary.LittleEndian.PutUint64(buf[:], uint64(a.Kind()))
|
||||
}
|
||||
@@ -627,9 +746,9 @@ func (c *Cache) unsafeIdent(a Artifact, encodeKind bool) (
|
||||
return
|
||||
}
|
||||
|
||||
buf = c.getIdentBuf()
|
||||
buf = ic.getIdentBuf()
|
||||
h := sha512.New384()
|
||||
if err := c.Encode(h, a); err != nil {
|
||||
if err := ic.Encode(h, a); err != nil {
|
||||
// unreachable
|
||||
panic(err)
|
||||
}
|
||||
@@ -894,7 +1013,7 @@ func (c *Cache) Scrub(checks int) error {
|
||||
se.DanglingIdentifiers = append(se.DanglingIdentifiers, *want)
|
||||
seMu.Unlock()
|
||||
return false
|
||||
} else if err = Decode(got, path.Base(linkname)); err != nil {
|
||||
} else if err = Decode(got, filepath.Base(linkname)); err != nil {
|
||||
seMu.Lock()
|
||||
lnp := dir.Append(linkname)
|
||||
se.Errs[lnp.Handle()] = append(se.Errs[lnp.Handle()], err)
|
||||
@@ -998,7 +1117,11 @@ func (c *Cache) Scrub(checks int) error {
|
||||
// loadOrStoreIdent attempts to load a cached [Artifact] by its identifier or
|
||||
// wait for a pending [Artifact] to cure. If neither is possible, the current
|
||||
// identifier is stored in identPending and a non-nil channel is returned.
|
||||
//
|
||||
// Since identErr is treated as grow-only, loadOrStoreIdent must not be entered
|
||||
// without holding a read lock on abortMu.
|
||||
func (c *Cache) loadOrStoreIdent(id unique.Handle[ID]) (
|
||||
ctx context.Context,
|
||||
done chan<- struct{},
|
||||
checksum unique.Handle[Checksum],
|
||||
err error,
|
||||
@@ -1015,20 +1138,23 @@ func (c *Cache) loadOrStoreIdent(id unique.Handle[ID]) (
|
||||
return
|
||||
}
|
||||
|
||||
var notify <-chan struct{}
|
||||
if notify, ok = c.identPending[id]; ok {
|
||||
var pending *pendingCure
|
||||
if pending, ok = c.identPending[id]; ok {
|
||||
c.identMu.Unlock()
|
||||
<-notify
|
||||
<-pending.done
|
||||
c.identMu.RLock()
|
||||
if checksum, ok = c.ident[id]; !ok {
|
||||
err = c.identErr[id]
|
||||
err = pending.err
|
||||
}
|
||||
c.identMu.RUnlock()
|
||||
return
|
||||
}
|
||||
|
||||
d := make(chan struct{})
|
||||
c.identPending[id] = d
|
||||
pending = &pendingCure{done: d}
|
||||
ctx, pending.cancel = context.WithCancel(c.toplevel.Load().ctx)
|
||||
c.wg.Add(1)
|
||||
c.identPending[id] = pending
|
||||
c.identMu.Unlock()
|
||||
done = d
|
||||
return
|
||||
@@ -1044,22 +1170,63 @@ func (c *Cache) finaliseIdent(
|
||||
) {
|
||||
c.identMu.Lock()
|
||||
if err != nil {
|
||||
c.identPending[id].err = err
|
||||
c.identErr[id] = err
|
||||
} else {
|
||||
c.ident[id] = checksum
|
||||
}
|
||||
delete(c.identPending, id)
|
||||
c.identMu.Unlock()
|
||||
c.wg.Done()
|
||||
|
||||
close(done)
|
||||
}
|
||||
|
||||
// Done returns a channel that is closed when the ongoing cure of an [Artifact]
|
||||
// referred to by the specified identifier completes. Done may return nil if
|
||||
// no ongoing cure of the specified identifier exists.
|
||||
func (c *Cache) Done(id unique.Handle[ID]) <-chan struct{} {
|
||||
c.identMu.RLock()
|
||||
pending, ok := c.identPending[id]
|
||||
c.identMu.RUnlock()
|
||||
if !ok || pending == nil {
|
||||
return nil
|
||||
}
|
||||
return pending.done
|
||||
}
|
||||
|
||||
// Cancel cancels the ongoing cure of an [Artifact] referred to by the specified
|
||||
// identifier. Cancel returns whether the [context.CancelFunc] has been killed.
|
||||
// Cancel returns after the cure is complete.
|
||||
func (c *Cache) Cancel(id unique.Handle[ID]) bool {
|
||||
c.identMu.RLock()
|
||||
pending, ok := c.identPending[id]
|
||||
c.identMu.RUnlock()
|
||||
if !ok || pending == nil || pending.cancel == nil {
|
||||
return false
|
||||
}
|
||||
pending.cancel()
|
||||
<-pending.done
|
||||
|
||||
c.abortMu.Lock()
|
||||
c.identMu.Lock()
|
||||
delete(c.identErr, id)
|
||||
c.identMu.Unlock()
|
||||
c.abortMu.Unlock()
|
||||
return true
|
||||
}
|
||||
|
||||
// openFile tries to load [FileArtifact] from [Cache], and if that fails,
|
||||
// obtains it via [FileArtifact.Cure] instead. Notably, it does not cure
|
||||
// [FileArtifact] to the filesystem. If err is nil, the caller is responsible
|
||||
// for closing the resulting [io.ReadCloser].
|
||||
func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
|
||||
if kc, ok := f.(KnownChecksum); ok {
|
||||
//
|
||||
// The context must originate from loadOrStoreIdent to enable cancellation.
|
||||
func (c *Cache) openFile(
|
||||
ctx context.Context,
|
||||
f FileArtifact,
|
||||
) (r io.ReadCloser, err error) {
|
||||
if kc, ok := f.(KnownChecksum); c.flags&CAssumeChecksum != 0 && ok {
|
||||
c.checksumMu.RLock()
|
||||
r, err = os.Open(c.base.Append(
|
||||
dirChecksum,
|
||||
@@ -1089,7 +1256,7 @@ func (c *Cache) openFile(f FileArtifact) (r io.ReadCloser, err error) {
|
||||
}
|
||||
}()
|
||||
}
|
||||
return f.Cure(&RContext{common{c}})
|
||||
return f.Cure(&RContext{common{ctx, c}})
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -1230,14 +1397,6 @@ func (e InvalidArtifactError) Error() string {
|
||||
return "artifact " + Encode(e) + " cannot be cured"
|
||||
}
|
||||
|
||||
// DependencyError refers to an artifact with a dependency tree larger than the
|
||||
// threshold specified by a previous call to [Cache.SetThreshold].
|
||||
type DependencyError struct{ A Artifact }
|
||||
|
||||
func (e DependencyError) Error() string {
|
||||
return "artifact has too many dependencies"
|
||||
}
|
||||
|
||||
// Cure cures the [Artifact] and returns its pathname and [Checksum]. Direct
|
||||
// calls to Cure are not subject to the cures limit.
|
||||
func (c *Cache) Cure(a Artifact) (
|
||||
@@ -1245,24 +1404,11 @@ func (c *Cache) Cure(a Artifact) (
|
||||
checksum unique.Handle[Checksum],
|
||||
err error,
|
||||
) {
|
||||
select {
|
||||
case <-c.ctx.Done():
|
||||
err = c.ctx.Err()
|
||||
c.abortMu.RLock()
|
||||
defer c.abortMu.RUnlock()
|
||||
|
||||
if err = c.toplevel.Load().ctx.Err(); err != nil {
|
||||
return
|
||||
|
||||
default:
|
||||
}
|
||||
|
||||
if c.threshold > 0 {
|
||||
var n uintptr
|
||||
for range Flood(a) {
|
||||
if n == c.threshold {
|
||||
err = DependencyError{a}
|
||||
return
|
||||
}
|
||||
n++
|
||||
}
|
||||
c.msg.Verbosef("visited %d artifacts", n)
|
||||
}
|
||||
|
||||
return c.cure(a, true)
|
||||
@@ -1348,15 +1494,16 @@ func (c *Cache) enterCure(a Artifact, curesExempt bool) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
ctx := c.toplevel.Load().ctx
|
||||
select {
|
||||
case c.cures <- struct{}{}:
|
||||
return nil
|
||||
|
||||
case <-c.ctx.Done():
|
||||
case <-ctx.Done():
|
||||
if a.IsExclusive() {
|
||||
c.exclMu.Unlock()
|
||||
}
|
||||
return c.ctx.Err()
|
||||
return ctx.Err()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1454,7 +1601,8 @@ func (r *RContext) NewMeasuredReader(
|
||||
return r.cache.newMeasuredReader(rc, checksum)
|
||||
}
|
||||
|
||||
// cure implements Cure without checking the full dependency graph.
|
||||
// cure implements Cure without acquiring a read lock on abortMu. cure must not
|
||||
// be entered during Abort.
|
||||
func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
pathname *check.Absolute,
|
||||
checksum unique.Handle[Checksum],
|
||||
@@ -1473,8 +1621,11 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
}
|
||||
}()
|
||||
|
||||
var done chan<- struct{}
|
||||
done, checksum, err = c.loadOrStoreIdent(id)
|
||||
var (
|
||||
ctx context.Context
|
||||
done chan<- struct{}
|
||||
)
|
||||
ctx, done, checksum, err = c.loadOrStoreIdent(id)
|
||||
if done == nil {
|
||||
return
|
||||
} else {
|
||||
@@ -1488,7 +1639,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
return
|
||||
}
|
||||
buf := c.getIdentBuf()
|
||||
err = Decode((*Checksum)(buf[:]), path.Base(name))
|
||||
err = Decode((*Checksum)(buf[:]), filepath.Base(name))
|
||||
if err == nil {
|
||||
checksum = unique.Make(Checksum(buf[:]))
|
||||
}
|
||||
@@ -1522,16 +1673,18 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
checksums,
|
||||
)
|
||||
|
||||
c.checksumMu.RLock()
|
||||
checksumFi, err = os.Stat(checksumPathname.String())
|
||||
c.checksumMu.RUnlock()
|
||||
if c.flags&CAssumeChecksum != 0 {
|
||||
c.checksumMu.RLock()
|
||||
checksumFi, err = os.Stat(checksumPathname.String())
|
||||
c.checksumMu.RUnlock()
|
||||
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
if err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
return
|
||||
}
|
||||
|
||||
checksumFi, err = nil, nil
|
||||
}
|
||||
|
||||
checksumFi, err = nil, nil
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1585,9 +1738,9 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
if err = c.enterCure(a, curesExempt); err != nil {
|
||||
return
|
||||
}
|
||||
r, err = f.Cure(&RContext{common{c}})
|
||||
r, err = f.Cure(&RContext{common{ctx, c}})
|
||||
if err == nil {
|
||||
if checksumPathname == nil || c.IsStrict() {
|
||||
if checksumPathname == nil || c.flags&CValidateKnown != 0 {
|
||||
h := sha512.New384()
|
||||
hbw := c.getWriter(h)
|
||||
_, err = io.Copy(w, io.TeeReader(r, hbw))
|
||||
@@ -1604,7 +1757,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
if checksumPathname == nil {
|
||||
checksum = unique.Make(Checksum(buf[:]))
|
||||
checksums = Encode(Checksum(buf[:]))
|
||||
} else if c.IsStrict() {
|
||||
} else if c.flags&CValidateKnown != 0 {
|
||||
if got := Checksum(buf[:]); got != checksum.Value() {
|
||||
err = &ChecksumMismatchError{
|
||||
Got: got,
|
||||
@@ -1665,7 +1818,7 @@ func (c *Cache) cure(a Artifact, curesExempt bool) (
|
||||
c.base.Append(dirWork, ids),
|
||||
c.base.Append(dirTemp, ids),
|
||||
ids, nil, nil, nil,
|
||||
common{c},
|
||||
common{ctx, c},
|
||||
}
|
||||
switch ca := a.(type) {
|
||||
case TrivialArtifact:
|
||||
@@ -1816,23 +1969,65 @@ func (c *Cache) OpenStatus(a Artifact) (r io.ReadSeekCloser, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// Abort cancels all pending cures and waits for them to clean up, but does not
|
||||
// close the cache.
|
||||
func (c *Cache) Abort() {
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
|
||||
c.toplevel.Load().cancel()
|
||||
c.abortMu.Lock()
|
||||
defer c.abortMu.Unlock()
|
||||
|
||||
// holding abortMu, identPending stays empty
|
||||
c.wg.Wait()
|
||||
c.identMu.Lock()
|
||||
c.toplevel.Store(newToplevel(c.parent))
|
||||
clear(c.identErr)
|
||||
c.identMu.Unlock()
|
||||
}
|
||||
|
||||
// Close cancels all pending cures and waits for them to clean up.
|
||||
func (c *Cache) Close() {
|
||||
c.closeOnce.Do(func() {
|
||||
c.cancel()
|
||||
c.wg.Wait()
|
||||
close(c.cures)
|
||||
c.unlock()
|
||||
})
|
||||
c.closeMu.Lock()
|
||||
defer c.closeMu.Unlock()
|
||||
|
||||
if c.closed {
|
||||
return
|
||||
}
|
||||
|
||||
c.closed = true
|
||||
c.toplevel.Load().cancel()
|
||||
c.wg.Wait()
|
||||
close(c.cures)
|
||||
c.unlock()
|
||||
}
|
||||
|
||||
// UnsupportedVariantError describes an on-disk cache with an extension variant
|
||||
// identification string that differs from the value returned by [Extension].
|
||||
type UnsupportedVariantError string
|
||||
|
||||
func (e UnsupportedVariantError) Error() string {
|
||||
return "unsupported variant " + strconv.Quote(string(e))
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrWouldPromote is returned by [Open] if the [CPromoteVariant] bit is not
|
||||
// set and the on-disk cache requires variant promotion.
|
||||
ErrWouldPromote = errors.New("operation would promote unextended cache")
|
||||
)
|
||||
|
||||
// Open returns the address of a newly opened instance of [Cache].
|
||||
//
|
||||
// Concurrent cures of a [FloodArtifact] dependency graph is limited to the
|
||||
// caller-supplied value, however direct calls to [Cache.Cure] is not subject
|
||||
// to this limitation.
|
||||
//
|
||||
// A cures value of 0 or lower is equivalent to the value returned by
|
||||
// A cures or jobs value of 0 or lower is equivalent to the value returned by
|
||||
// [runtime.NumCPU].
|
||||
//
|
||||
// A successful call to Open guarantees exclusive access to the on-filesystem
|
||||
@@ -1842,10 +2037,10 @@ func (c *Cache) Close() {
|
||||
func Open(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
cures int,
|
||||
flags, cures, jobs int,
|
||||
base *check.Absolute,
|
||||
) (*Cache, error) {
|
||||
return open(ctx, msg, cures, base, true)
|
||||
return open(ctx, msg, flags, cures, jobs, base, true)
|
||||
}
|
||||
|
||||
// open implements Open but allows omitting the [lockedfile] lock when called
|
||||
@@ -1853,13 +2048,24 @@ func Open(
|
||||
func open(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
cures int,
|
||||
flags, cures, jobs int,
|
||||
base *check.Absolute,
|
||||
lock bool,
|
||||
) (*Cache, error) {
|
||||
openMu.Lock()
|
||||
defer openMu.Unlock()
|
||||
opened = true
|
||||
|
||||
if extension == "" && len(irArtifact) != int(_kindEnd) {
|
||||
panic("attempting to open cache with incomplete variant setup")
|
||||
}
|
||||
|
||||
if cures < 1 {
|
||||
cures = runtime.NumCPU()
|
||||
}
|
||||
if jobs < 1 {
|
||||
jobs = runtime.NumCPU()
|
||||
}
|
||||
|
||||
for _, name := range []string{
|
||||
dirIdentifier,
|
||||
@@ -1867,28 +2073,34 @@ func open(
|
||||
dirStatus,
|
||||
dirWork,
|
||||
} {
|
||||
if err := os.MkdirAll(base.Append(name).String(), 0700); err != nil &&
|
||||
!errors.Is(err, os.ErrExist) {
|
||||
if err := os.MkdirAll(
|
||||
base.Append(name).String(),
|
||||
0700,
|
||||
); err != nil && !errors.Is(err, os.ErrExist) {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
c := Cache{
|
||||
parent: ctx,
|
||||
|
||||
cures: make(chan struct{}, cures),
|
||||
flags: flags,
|
||||
jobs: jobs,
|
||||
|
||||
msg: msg,
|
||||
base: base,
|
||||
|
||||
identPool: sync.Pool{New: func() any { return new(extIdent) }},
|
||||
irCache: zeroIRCache(),
|
||||
|
||||
ident: make(map[unique.Handle[ID]]unique.Handle[Checksum]),
|
||||
identErr: make(map[unique.Handle[ID]]error),
|
||||
identPending: make(map[unique.Handle[ID]]<-chan struct{}),
|
||||
identPending: make(map[unique.Handle[ID]]*pendingCure),
|
||||
|
||||
brPool: sync.Pool{New: func() any { return new(bufio.Reader) }},
|
||||
bwPool: sync.Pool{New: func() any { return new(bufio.Writer) }},
|
||||
}
|
||||
c.ctx, c.cancel = context.WithCancel(ctx)
|
||||
c.toplevel.Store(newToplevel(ctx))
|
||||
|
||||
if lock || !testing.Testing() {
|
||||
if unlock, err := lockedfile.MutexAt(
|
||||
@@ -1902,6 +2114,45 @@ func open(
|
||||
c.unlock = func() {}
|
||||
}
|
||||
|
||||
variantPath := base.Append(fileVariant).String()
|
||||
if p, err := os.ReadFile(variantPath); err != nil {
|
||||
if !errors.Is(err, os.ErrNotExist) {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
// nonexistence implies newly created cache, or a cache predating
|
||||
// variant identification strings, in which case it is silently promoted
|
||||
if err = os.WriteFile(
|
||||
variantPath,
|
||||
[]byte(extension),
|
||||
0400,
|
||||
); err != nil {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
} else if s := string(p); s == "" {
|
||||
if extension != "" {
|
||||
if flags&CPromoteVariant == 0 {
|
||||
c.unlock()
|
||||
return nil, ErrWouldPromote
|
||||
}
|
||||
if err = os.WriteFile(
|
||||
variantPath,
|
||||
[]byte(extension),
|
||||
0400,
|
||||
); err != nil {
|
||||
c.unlock()
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
} else if !ValidExtension(s) {
|
||||
c.unlock()
|
||||
return nil, ErrInvalidExtension
|
||||
} else if s != extension {
|
||||
c.unlock()
|
||||
return nil, UnsupportedVariantError(s)
|
||||
}
|
||||
|
||||
return &c, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"strconv"
|
||||
"sync"
|
||||
"syscall"
|
||||
"testing"
|
||||
"unique"
|
||||
@@ -24,6 +25,8 @@ import (
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/container"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/internal/info"
|
||||
"hakurei.app/internal/landlock"
|
||||
"hakurei.app/internal/pkg"
|
||||
"hakurei.app/internal/stub"
|
||||
"hakurei.app/message"
|
||||
@@ -33,11 +36,47 @@ import (
|
||||
func unsafeOpen(
|
||||
ctx context.Context,
|
||||
msg message.Msg,
|
||||
cures int,
|
||||
flags, cures, jobs int,
|
||||
base *check.Absolute,
|
||||
lock bool,
|
||||
) (*pkg.Cache, error)
|
||||
|
||||
var (
|
||||
// extension is a string uniquely identifying a set of custom [Artifact]
|
||||
// implementations registered by calling [Register].
|
||||
//
|
||||
//go:linkname extension hakurei.app/internal/pkg.extension
|
||||
extension string
|
||||
|
||||
// opened is false if [Open] was never called.
|
||||
//
|
||||
//go:linkname opened hakurei.app/internal/pkg.opened
|
||||
opened bool
|
||||
|
||||
// irArtifact refers to artifact IR interpretation functions and must not be
|
||||
// written to directly.
|
||||
//
|
||||
//go:linkname irArtifact hakurei.app/internal/pkg.irArtifact
|
||||
irArtifact map[pkg.Kind]pkg.IRReadFunc
|
||||
)
|
||||
|
||||
// newRContext returns the address of a new [pkg.RContext] unsafely created for
|
||||
// the specified [testing.TB].
|
||||
func newRContext(tb testing.TB, c *pkg.Cache) *pkg.RContext {
|
||||
var r pkg.RContext
|
||||
rContextVal := reflect.ValueOf(&r).Elem().FieldByName("ctx")
|
||||
reflect.NewAt(
|
||||
rContextVal.Type(),
|
||||
unsafe.Pointer(rContextVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(tb.Context()))
|
||||
rCacheVal := reflect.ValueOf(&r).Elem().FieldByName("cache")
|
||||
reflect.NewAt(
|
||||
rCacheVal.Type(),
|
||||
unsafe.Pointer(rCacheVal.UnsafeAddr()),
|
||||
).Elem().Set(reflect.ValueOf(c))
|
||||
return &r
|
||||
}
|
||||
|
||||
func TestMain(m *testing.M) { container.TryArgv0(nil); os.Exit(m.Run()) }
|
||||
|
||||
// overrideIdent overrides the ID method of [Artifact].
|
||||
@@ -228,7 +267,7 @@ func TestIdent(t *testing.T) {
|
||||
var cache *pkg.Cache
|
||||
if a, err := check.NewAbs(t.TempDir()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if cache, err = pkg.Open(t.Context(), msg, 0, a); err != nil {
|
||||
} else if cache, err = pkg.Open(t.Context(), msg, 0, 0, 0, a); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Cleanup(cache.Close)
|
||||
@@ -252,6 +291,7 @@ func TestIdent(t *testing.T) {
|
||||
// on test completion.
|
||||
type cacheTestCase struct {
|
||||
name string
|
||||
flags int
|
||||
early func(t *testing.T, base *check.Absolute)
|
||||
f func(t *testing.T, base *check.Absolute, c *pkg.Cache)
|
||||
want pkg.Checksum
|
||||
@@ -288,8 +328,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
|
||||
msg := message.New(log.New(os.Stderr, "cache: ", 0))
|
||||
msg.SwapVerbose(testing.Verbose())
|
||||
|
||||
flags := tc.flags
|
||||
|
||||
if info.CanDegrade {
|
||||
if _, err := landlock.GetABI(); err != nil {
|
||||
if !errors.Is(err, syscall.ENOSYS) {
|
||||
t.Fatalf("LandlockGetABI: error = %v", err)
|
||||
}
|
||||
flags |= pkg.CHostAbstract
|
||||
t.Log("Landlock LSM is unavailable, setting CHostAbstract")
|
||||
}
|
||||
}
|
||||
|
||||
var scrubFunc func() error // scrub after hashing
|
||||
if c, err := pkg.Open(t.Context(), msg, 1<<4, base); err != nil {
|
||||
if c, err := pkg.Open(t.Context(), msg, flags, 1<<4, 0, base); err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
} else {
|
||||
t.Cleanup(c.Close)
|
||||
@@ -309,9 +361,20 @@ func checkWithCache(t *testing.T, testCases []cacheTestCase) {
|
||||
restoreTemp = true
|
||||
}
|
||||
|
||||
// destroy lock file to avoid changing cache checksums
|
||||
if err := os.Remove(base.Append("lock").String()); err != nil {
|
||||
t.Fatal(err)
|
||||
// destroy lock and variant file to avoid changing cache checksums
|
||||
for _, s := range []string{
|
||||
"lock",
|
||||
"variant",
|
||||
} {
|
||||
pathname := base.Append(s)
|
||||
if p, err := os.ReadFile(pathname.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(p) != 0 {
|
||||
t.Fatalf("file %q: %q", s, string(p))
|
||||
}
|
||||
if err := os.Remove(pathname.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// destroy non-deterministic status files
|
||||
@@ -468,9 +531,7 @@ func TestCache(t *testing.T) {
|
||||
}()
|
||||
|
||||
testCases := []cacheTestCase{
|
||||
{"file", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
|
||||
{"file", pkg.CValidateKnown | pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
identifier := (pkg.ID)(bytes.Repeat([]byte{
|
||||
0x75, 0xe6, 0x9d, 0x6d, 0xe7, 0x9f,
|
||||
}, 8))
|
||||
@@ -593,7 +654,7 @@ func TestCache(t *testing.T) {
|
||||
if c0, err := unsafeOpen(
|
||||
t.Context(),
|
||||
message.New(nil),
|
||||
0, base, false,
|
||||
0, 0, 0, base, false,
|
||||
); err != nil {
|
||||
t.Fatalf("open: error = %v", err)
|
||||
} else {
|
||||
@@ -627,7 +688,7 @@ func TestCache(t *testing.T) {
|
||||
}
|
||||
}, pkg.MustDecode("St9rlE-mGZ5gXwiv_hzQ_B8bZP-UUvSNmf4nHUZzCMOumb6hKnheZSe0dmnuc4Q2")},
|
||||
|
||||
{"directory", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
{"directory", pkg.CAssumeChecksum, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
id := pkg.MustDecode(
|
||||
"HnySzeLQvSBZuTUcvfmLEX_OmH4yJWWH788NxuLuv7kVn8_uPM6Ks4rqFWM2NZJY",
|
||||
)
|
||||
@@ -804,9 +865,7 @@ func TestCache(t *testing.T) {
|
||||
})
|
||||
}, pkg.MustDecode("WVpvsVqVKg9Nsh744x57h51AuWUoUR2nnh8Md-EYBQpk6ziyTuUn6PLtF2e0Eu_d")},
|
||||
|
||||
{"pending", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
c.SetStrict(true)
|
||||
|
||||
{"pending", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
wantErr := stub.UniqueError(0xcafe)
|
||||
n, ready := make(chan struct{}), make(chan struct{})
|
||||
go func() {
|
||||
@@ -865,18 +924,117 @@ func TestCache(t *testing.T) {
|
||||
t.Fatalf("Scrub: error = %#v, want %#v", err, wantErrScrub)
|
||||
}
|
||||
|
||||
identPendingVal := reflect.ValueOf(c).Elem().FieldByName("identPending")
|
||||
identPending := reflect.NewAt(
|
||||
identPendingVal.Type(),
|
||||
unsafe.Pointer(identPendingVal.UnsafeAddr()),
|
||||
).Elem().Interface().(map[unique.Handle[pkg.ID]]<-chan struct{})
|
||||
notify := identPending[unique.Make(pkg.ID{0xff})]
|
||||
notify := c.Done(unique.Make(pkg.ID{0xff}))
|
||||
go close(n)
|
||||
<-notify
|
||||
if notify != nil {
|
||||
<-notify
|
||||
}
|
||||
for c.Done(unique.Make(pkg.ID{0xff})) != nil {
|
||||
}
|
||||
<-wCureDone
|
||||
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
|
||||
|
||||
{"scrub", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
{"cancel abort block", pkg.CValidateKnown, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
var wg sync.WaitGroup
|
||||
defer wg.Wait()
|
||||
|
||||
var started sync.WaitGroup
|
||||
defer started.Wait()
|
||||
|
||||
blockCures := func(d byte, e stub.UniqueError, n int) {
|
||||
started.Add(n)
|
||||
for i := range n {
|
||||
wg.Go(func() {
|
||||
if _, _, err := c.Cure(overrideIdent{pkg.ID{d, byte(i)}, &stubArtifact{
|
||||
kind: pkg.KindTar,
|
||||
cure: func(t *pkg.TContext) error {
|
||||
started.Done()
|
||||
<-t.Unwrap().Done()
|
||||
return e + stub.UniqueError(i)
|
||||
},
|
||||
}}); !reflect.DeepEqual(err, e+stub.UniqueError(i)) {
|
||||
panic(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
started.Wait()
|
||||
}
|
||||
|
||||
blockCures(0xfd, 0xbad, 16)
|
||||
c.Abort()
|
||||
wg.Wait()
|
||||
|
||||
blockCures(0xfd, 0xcafe, 16)
|
||||
c.Abort()
|
||||
wg.Wait()
|
||||
|
||||
blockCures(0xff, 0xbad, 1)
|
||||
if !c.Cancel(unique.Make(pkg.ID{0xff})) {
|
||||
t.Fatal("missed cancellation")
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
blockCures(0xff, 0xcafe, 1)
|
||||
if !c.Cancel(unique.Make(pkg.ID{0xff})) {
|
||||
t.Fatal("missed cancellation")
|
||||
}
|
||||
wg.Wait()
|
||||
|
||||
for c.Cancel(unique.Make(pkg.ID{0xff})) {
|
||||
}
|
||||
|
||||
c.Close()
|
||||
c.Abort()
|
||||
}, pkg.MustDecode("E4vEZKhCcL2gPZ2Tt59FS3lDng-d_2SKa2i5G_RbDfwGn6EemptFaGLPUDiOa94C")},
|
||||
|
||||
{"no assume checksum", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
makeGarbage := func(work *check.Absolute, wantErr error) error {
|
||||
if err := os.Mkdir(work.String(), 0700); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := os.WriteFile(work.Append(
|
||||
"check",
|
||||
).String(), nil, 0400); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return wantErr
|
||||
}
|
||||
|
||||
wantChecksum := pkg.MustDecode("Aubi5EG4_Y8DhL9bQ3Q4HFBhLRF7X5gt9D3CNCQfT-TeBtlRXc7Zi_JYZEMoCC7M")
|
||||
|
||||
cureMany(t, c, []cureStep{
|
||||
{"create", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 0}, &stubArtifact{
|
||||
kind: pkg.KindTar,
|
||||
cure: func(t *pkg.TContext) error {
|
||||
return makeGarbage(t.GetWorkDir(), nil)
|
||||
},
|
||||
}}}, base.Append(
|
||||
"identifier",
|
||||
pkg.Encode(pkg.ID{0xff, 0}),
|
||||
), wantChecksum, nil},
|
||||
|
||||
{"reject", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xfe, 1}, &stubArtifact{
|
||||
kind: pkg.KindTar,
|
||||
cure: func(t *pkg.TContext) error {
|
||||
return makeGarbage(t.GetWorkDir(), stub.UniqueError(0xbad))
|
||||
},
|
||||
}}}, nil, pkg.Checksum{}, stub.UniqueError(0xbad)},
|
||||
|
||||
{"match", overrideChecksum{wantChecksum, overrideIdent{pkg.ID{0xff, 1}, &stubArtifact{
|
||||
kind: pkg.KindTar,
|
||||
cure: func(t *pkg.TContext) error {
|
||||
return makeGarbage(t.GetWorkDir(), nil)
|
||||
},
|
||||
}}}, base.Append(
|
||||
"identifier",
|
||||
pkg.Encode(pkg.ID{0xff, 1}),
|
||||
), wantChecksum, nil},
|
||||
})
|
||||
}, pkg.MustDecode("OC290t23aimNo2Rp2pPwan5GI2KRLRdOwYxXQMD9jw0QROgHnNXWodoWdV0hwu2w")},
|
||||
|
||||
{"scrub", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
cureMany(t, c, []cureStep{
|
||||
{"bad measured file", newStubFile(
|
||||
pkg.KindHTTPGet,
|
||||
@@ -973,6 +1131,10 @@ func TestErrors(t *testing.T) {
|
||||
Want: pkg.IRKindIdent,
|
||||
Ancillary: 0xcafe,
|
||||
}, "got invalid kind 48879 IR value (0xcafe) instead of ident"},
|
||||
|
||||
{"UnsupportedVariantError", pkg.UnsupportedVariantError(
|
||||
"rosa",
|
||||
), `unsupported variant "rosa"`},
|
||||
}
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
@@ -1181,8 +1343,10 @@ func (a earlyFailureF) Cure(*pkg.FContext) error {
|
||||
}
|
||||
|
||||
func TestDependencyCureErrorEarly(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"early", nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
|
||||
{"early", 0, nil, func(t *testing.T, _ *check.Absolute, c *pkg.Cache) {
|
||||
_, _, err := c.Cure(earlyFailureF(8))
|
||||
if !errors.Is(err, stub.UniqueError(0xcafe)) {
|
||||
t.Fatalf("Cure: error = %v", err)
|
||||
@@ -1191,7 +1355,7 @@ func TestDependencyCureErrorEarly(t *testing.T) {
|
||||
})
|
||||
}
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
func TestOpen(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("nonexistent", func(t *testing.T) {
|
||||
@@ -1205,7 +1369,7 @@ func TestNew(t *testing.T) {
|
||||
if _, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(nil),
|
||||
0, check.MustAbs(container.Nonexistent),
|
||||
0, 0, 0, check.MustAbs(container.Nonexistent),
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
@@ -1233,9 +1397,225 @@ func TestNew(t *testing.T) {
|
||||
if _, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(nil),
|
||||
0, tempDir.Append("cache"),
|
||||
0, 0, 0, tempDir.Append("cache"),
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Errorf("Open: error = %#v, want %#v", err, wantErr)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestExtensionRegister(t *testing.T) {
|
||||
extensionOld := extension
|
||||
openedOld := opened
|
||||
t.Cleanup(func() { extension = extensionOld; opened = openedOld })
|
||||
extension = ""
|
||||
opened = false
|
||||
|
||||
t.Run("set", func(t *testing.T) {
|
||||
t.Cleanup(func() { extension = "" })
|
||||
|
||||
const want = "rosa"
|
||||
pkg.SetExtension(want)
|
||||
if got := pkg.Extension(); got != want {
|
||||
t.Fatalf("Extension: %q, want %q", got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("twice", func(t *testing.T) {
|
||||
t.Cleanup(func() { extension = "" })
|
||||
|
||||
defer func() {
|
||||
const wantPanic = "attempting to set extension twice"
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
pkg.SetExtension("rosa")
|
||||
pkg.SetExtension("rosa")
|
||||
})
|
||||
|
||||
t.Run("invalid", func(t *testing.T) {
|
||||
defer func() {
|
||||
var wantPanic = pkg.ErrInvalidExtension
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %#v, want %#v", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
pkg.SetExtension(" ")
|
||||
})
|
||||
|
||||
t.Run("opened", func(t *testing.T) {
|
||||
t.Cleanup(func() { opened = false })
|
||||
|
||||
if _, err := pkg.Open(
|
||||
t.Context(),
|
||||
message.New(log.Default()),
|
||||
0, 0, 0,
|
||||
check.MustAbs(container.Nonexistent),
|
||||
); !errors.Is(err, os.ErrNotExist) {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
|
||||
t.Run("variant", func(t *testing.T) {
|
||||
defer func() {
|
||||
const wantPanic = "attempting to set extension after open"
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
pkg.SetExtension("rosa")
|
||||
})
|
||||
|
||||
t.Run("register", func(t *testing.T) {
|
||||
defer func() {
|
||||
const wantPanic = "attempting to register after open"
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
pkg.Register(pkg.KindCustomOffset, nil)
|
||||
})
|
||||
})
|
||||
|
||||
t.Run("incomplete", func(t *testing.T) {
|
||||
t.Cleanup(func() { delete(irArtifact, pkg.KindCustomOffset) })
|
||||
|
||||
defer func() {
|
||||
const wantPanic = "attempting to open cache with incomplete variant setup"
|
||||
if r := recover(); r != wantPanic {
|
||||
t.Errorf("panic: %#v, want %q", r, wantPanic)
|
||||
}
|
||||
}()
|
||||
pkg.Register(pkg.KindCustomOffset, nil)
|
||||
|
||||
t.Cleanup(func() { opened = false })
|
||||
_, _ = pkg.Open(nil, nil, 0, 0, 0, nil)
|
||||
panic("unreachable")
|
||||
})
|
||||
|
||||
t.Run("create", func(t *testing.T) {
|
||||
t.Cleanup(func() { extension = "" })
|
||||
const want = "rosa"
|
||||
pkg.SetExtension(want)
|
||||
|
||||
base := check.MustAbs(t.TempDir())
|
||||
t.Cleanup(func() { opened = false })
|
||||
if c, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
0, 0, 0,
|
||||
base,
|
||||
); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
if got, err := os.ReadFile(base.Append("variant").String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(got) != want {
|
||||
t.Fatalf("variant: %q", string(got))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("access", func(t *testing.T) {
|
||||
base := check.MustAbs(t.TempDir())
|
||||
t.Cleanup(func() { opened = false })
|
||||
|
||||
if err := os.WriteFile(base.Append("variant").String(), nil, 0); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
wantErr := &os.PathError{
|
||||
Op: "open",
|
||||
Path: base.Append("variant").String(),
|
||||
Err: syscall.EACCES,
|
||||
}
|
||||
if _, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
0, 0, 0,
|
||||
base,
|
||||
); !reflect.DeepEqual(err, wantErr) {
|
||||
t.Fatalf("Open: error = %v, want %v", err, wantErr)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("promote", func(t *testing.T) {
|
||||
t.Cleanup(func() { extension = "" })
|
||||
const want = "rosa"
|
||||
pkg.SetExtension(want)
|
||||
|
||||
base := check.MustAbs(t.TempDir())
|
||||
t.Cleanup(func() { opened = false })
|
||||
|
||||
variantPath := base.Append("variant")
|
||||
if err := os.WriteFile(variantPath.String(), nil, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
0, 0, 0,
|
||||
base,
|
||||
); !reflect.DeepEqual(err, pkg.ErrWouldPromote) {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
|
||||
if p, err := os.ReadFile(variantPath.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if len(p) != 0 {
|
||||
t.Fatalf("variant: %q", string(p))
|
||||
}
|
||||
|
||||
if c, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
pkg.CPromoteVariant, 0, 0,
|
||||
base,
|
||||
); err != nil {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
} else {
|
||||
c.Close()
|
||||
}
|
||||
|
||||
if p, err := os.ReadFile(variantPath.String()); err != nil {
|
||||
t.Fatal(err)
|
||||
} else if string(p) != want {
|
||||
t.Fatalf("variant: %q, want %q", string(p), want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("open invalid", func(t *testing.T) {
|
||||
base := check.MustAbs(t.TempDir())
|
||||
t.Cleanup(func() { opened = false })
|
||||
|
||||
variantPath := base.Append("variant")
|
||||
if err := os.WriteFile(variantPath.String(), make([]byte, 129), 0400); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
0, 0, 0,
|
||||
base,
|
||||
); !reflect.DeepEqual(err, pkg.ErrInvalidExtension) {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("unsupported", func(t *testing.T) {
|
||||
base := check.MustAbs(t.TempDir())
|
||||
t.Cleanup(func() { opened = false })
|
||||
|
||||
variantPath := base.Append("variant")
|
||||
if err := os.WriteFile(variantPath.String(), []byte("rosa"), 0400); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if _, err := pkg.Open(
|
||||
t.Context(), nil,
|
||||
0, 0, 0,
|
||||
base,
|
||||
); !reflect.DeepEqual(err, pkg.UnsupportedVariantError("rosa")) {
|
||||
t.Fatalf("Open: error = %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -10,7 +10,7 @@ import (
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -43,8 +43,7 @@ var _ fmt.Stringer = new(tarArtifactNamed)
|
||||
func (a *tarArtifactNamed) String() string { return a.name + "-unpack" }
|
||||
|
||||
// NewTar returns a new [Artifact] backed by the supplied [Artifact] and
|
||||
// compression method. The source [Artifact] must be compatible with
|
||||
// [TContext.Open].
|
||||
// compression method. The source [Artifact] must be a [FileArtifact].
|
||||
func NewTar(a Artifact, compression uint32) Artifact {
|
||||
ta := tarArtifact{a, compression}
|
||||
if s, ok := a.(fmt.Stringer); ok {
|
||||
@@ -169,7 +168,7 @@ func (a *tarArtifact) Cure(t *TContext) (err error) {
|
||||
}
|
||||
|
||||
if typeflag >= '0' && typeflag <= '9' && typeflag != tar.TypeDir {
|
||||
if err = root.MkdirAll(path.Dir(header.Name), 0700); err != nil {
|
||||
if err = root.MkdirAll(filepath.Dir(header.Name), 0700); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestTar(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
checkWithCache(t, []cacheTestCase{
|
||||
{"http", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
{"http", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
checkTarHTTP(t, base, c, fstest.MapFS{
|
||||
".": {Mode: fs.ModeDir | 0700},
|
||||
|
||||
@@ -42,7 +42,7 @@ func TestTar(t *testing.T) {
|
||||
))
|
||||
}, pkg.MustDecode("NQTlc466JmSVLIyWklm_u8_g95jEEb98PxJU-kjwxLpfdjwMWJq0G8ze9R4Vo1Vu")},
|
||||
|
||||
{"http expand", nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
{"http expand", 0, nil, func(t *testing.T, base *check.Absolute, c *pkg.Cache) {
|
||||
checkTarHTTP(t, base, c, fstest.MapFS{
|
||||
".": {Mode: fs.ModeDir | 0700},
|
||||
|
||||
|
||||
60
internal/pkg/testdata/main.go
vendored
60
internal/pkg/testdata/main.go
vendored
@@ -7,12 +7,13 @@ import (
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
"path"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"runtime"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"hakurei.app/check"
|
||||
"hakurei.app/fhs"
|
||||
"hakurei.app/vfs"
|
||||
)
|
||||
@@ -21,6 +22,10 @@ func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetPrefix("testtool: ")
|
||||
|
||||
environ := slices.DeleteFunc(slices.Clone(os.Environ()), func(s string) bool {
|
||||
return s == "CURE_JOBS="+strconv.Itoa(runtime.NumCPU())
|
||||
})
|
||||
|
||||
var hostNet, layers, promote bool
|
||||
if len(os.Args) == 2 && os.Args[0] == "testtool" {
|
||||
switch os.Args[1] {
|
||||
@@ -48,15 +53,15 @@ func main() {
|
||||
|
||||
var overlayRoot bool
|
||||
wantEnv := []string{"HAKUREI_TEST=1"}
|
||||
if len(os.Environ()) == 2 {
|
||||
if len(environ) == 2 {
|
||||
overlayRoot = true
|
||||
if !layers && !promote {
|
||||
log.SetPrefix("testtool(overlay root): ")
|
||||
}
|
||||
wantEnv = []string{"HAKUREI_TEST=1", "HAKUREI_ROOT=1"}
|
||||
}
|
||||
if !slices.Equal(wantEnv, os.Environ()) {
|
||||
log.Fatalf("Environ: %q, want %q", os.Environ(), wantEnv)
|
||||
if !slices.Equal(wantEnv, environ) {
|
||||
log.Fatalf("Environ: %q, want %q", environ, wantEnv)
|
||||
}
|
||||
|
||||
var overlayWork bool
|
||||
@@ -68,7 +73,7 @@ func main() {
|
||||
if got, err := os.Executable(); err != nil {
|
||||
log.Fatalf("Executable: error = %v", err)
|
||||
} else {
|
||||
iftPath = path.Join(path.Dir(path.Dir(got)), "ift")
|
||||
iftPath = filepath.Join(filepath.Dir(filepath.Dir(got)), "ift")
|
||||
|
||||
if got != wantExec {
|
||||
switch got {
|
||||
@@ -153,43 +158,24 @@ func main() {
|
||||
m.Source != "overlay" || m.FsType != "overlay" {
|
||||
log.Fatal("unexpected root mount entry")
|
||||
}
|
||||
var lowerdir string
|
||||
var lowerdir []string
|
||||
for _, o := range strings.Split(m.FsOptstr, ",") {
|
||||
const lowerdirKey = "lowerdir="
|
||||
const lowerdirKey = "lowerdir+="
|
||||
if strings.HasPrefix(o, lowerdirKey) {
|
||||
lowerdir = o[len(lowerdirKey):]
|
||||
lowerdir = append(lowerdir, o[len(lowerdirKey):])
|
||||
}
|
||||
}
|
||||
if !layers {
|
||||
if path.Base(lowerdir) != checksumEmptyDir {
|
||||
if len(lowerdir) != 1 || filepath.Base(lowerdir[0]) != checksumEmptyDir {
|
||||
log.Fatal("unexpected artifact checksum")
|
||||
}
|
||||
} else {
|
||||
ident = "p1t_drXr34i-jZNuxDMLaMOdL6tZvQqhavNafGynGqxOZoXAUTSn7kqNh3Ovv3DT"
|
||||
|
||||
lowerdirsEscaped := strings.Split(lowerdir, ":")
|
||||
lowerdirs := lowerdirsEscaped[:0]
|
||||
// ignore the option separator since it does not appear in ident
|
||||
for i, e := range lowerdirsEscaped {
|
||||
if len(e) > 0 &&
|
||||
e[len(e)-1] == check.SpecialOverlayEscape[0] &&
|
||||
(len(e) == 1 || e[len(e)-2] != check.SpecialOverlayEscape[0]) {
|
||||
// ignore escaped pathname separator since it does not
|
||||
// appear in ident
|
||||
|
||||
e = e[:len(e)-1]
|
||||
if len(lowerdirsEscaped) != i {
|
||||
lowerdirsEscaped[i+1] = e + lowerdirsEscaped[i+1]
|
||||
continue
|
||||
}
|
||||
}
|
||||
lowerdirs = append(lowerdirs, e)
|
||||
}
|
||||
|
||||
if len(lowerdirs) != 2 ||
|
||||
path.Base(lowerdirs[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
|
||||
path.Base(lowerdirs[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
|
||||
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdirs, ", "))
|
||||
if len(lowerdir) != 2 ||
|
||||
filepath.Base(lowerdir[0]) != "MGWmEfjut2QE2xPJwTsmUzpff4BN_FEnQ7T0j7gvUCCiugJQNwqt9m151fm9D1yU" ||
|
||||
filepath.Base(lowerdir[1]) != "nY_CUdiaUM1OL4cPr5TS92FCJ3rCRV7Hm5oVTzAvMXwC03_QnTRfQ5PPs7mOU9fK" {
|
||||
log.Fatalf("unexpected lowerdirs %s", strings.Join(lowerdir, ", "))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -202,12 +188,12 @@ func main() {
|
||||
}
|
||||
|
||||
next()
|
||||
if path.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
|
||||
if filepath.Base(m.Root) != "OLBgp1GsljhM2TJ-sbHjaiH9txEUvgdDTAzHv2P24donTt6_529l-9Ua0vFImLlb" {
|
||||
log.Fatal("unexpected file artifact checksum")
|
||||
}
|
||||
|
||||
next()
|
||||
if path.Base(m.Root) != checksumEmptyDir {
|
||||
if filepath.Base(m.Root) != checksumEmptyDir {
|
||||
log.Fatal("unexpected artifact checksum")
|
||||
}
|
||||
}
|
||||
@@ -226,13 +212,13 @@ func main() {
|
||||
log.Fatal("unexpected work mount entry")
|
||||
}
|
||||
} else {
|
||||
if path.Base(m.Root) != ident || m.Target != "/work" {
|
||||
if filepath.Base(m.Root) != ident || m.Target != "/work" {
|
||||
log.Fatal("unexpected work mount entry")
|
||||
}
|
||||
}
|
||||
|
||||
next()
|
||||
if path.Base(m.Root) != ident || m.Target != "/tmp" {
|
||||
if filepath.Base(m.Root) != ident || m.Target != "/tmp" {
|
||||
log.Fatal("unexpected temp mount entry")
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user