diff --git a/Dockerfile b/Dockerfile index 6cdc164..1de6575 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,12 +1,14 @@ -from golang:1.21.5-alpine3.19 as build +from golang:1.21.6-alpine3.19 as build workdir /src -copy go.mod go.sum . -run go mod download copy . . -run go test ./... -run go build -o /go/bin/init -trimpath . + +run \ + --mount=type=cache,id=gomod,target=/go/pkg/mod \ + --mount=type=cache,id=gobuild,target=/root/.cache/go-build \ + go test ./... \ +&& go build -o /go/bin/init -trimpath . # ------------------------------------------------------------------------ from alpine:3.19.0 as initrd diff --git a/auth.go b/auth.go index 9890ae3..da64f55 100644 --- a/auth.go +++ b/auth.go @@ -3,8 +3,8 @@ package main import ( "bytes" "errors" - "log" + "github.com/rs/zerolog/log" "golang.org/x/crypto/ssh" config "novit.tech/direktil/pkg/bootstrapconfig" @@ -23,7 +23,7 @@ func localAuth() bool { } if config.CheckPassword(auth.Password, sec) { - log.Printf("login with auth %q", auth.Name) + log.Info().Msgf("login with auth %q", auth.Name) return true } } @@ -41,12 +41,12 @@ func sshCheckPubkey(conn ssh.ConnMetadata, key ssh.PublicKey) (*ssh.Permissions, allowedKey, _, _, _, err := ssh.ParseAuthorizedKey([]byte(auth.SSHKey)) if err != nil { - log.Printf("SSH pubkey for %q invalid: %v", auth.Name, auth.SSHKey) + log.Warn().Err(err).Str("user", auth.Name).Str("key", auth.SSHKey).Msg("SSH public key is invalid") return nil, err } if bytes.Equal(allowedKey.Marshal(), keyBytes) { - log.Print("ssh: accepting public key for ", auth.Name) + log.Info().Str("user", auth.Name).Msg("ssh: accepting public key") return &ssh.Permissions{ Extensions: map[string]string{ "pubkey-fp": ssh.FingerprintSHA256(key), diff --git a/boot-v1.go b/boot-v1.go index 9fc09c0..8009667 100644 --- a/boot-v1.go +++ b/boot-v1.go @@ -1,7 +1,6 @@ package main import ( - "log" "os" "path/filepath" "strconv" @@ -9,14 +8,17 @@ import ( "syscall" "time" + "github.com/rs/zerolog/log" yaml "gopkg.in/yaml.v2" + + "novit.tech/direktil/pkg/config/apply" "novit.tech/direktil/pkg/sysfs" ) var loopOffset = 0 func bootV1() { - log.Print("-- boot v1 --") + log.Info().Msg("-- boot v1 --") // find and mount /boot bootMatch := param("boot", "") @@ -30,21 +32,21 @@ func bootV1() { if i > 30 { fatal("boot partition not found after 30s") } - log.Print("boot partition not found, retrying") + log.Info().Msg("boot partition not found, retrying") time.Sleep(1 * time.Second) continue } devFile := filepath.Join("/dev", devNames[0]) - log.Print("boot partition found: ", devFile) + log.Info().Str("dev", devFile).Msg("boot partition found") mount(devFile, "/boot", bootFS, bootMountFlags, "") bootMounted = true break } } else { - log.Print("Assuming /boot is already populated.") + log.Info().Msg("Assuming /boot is already populated.") } // load config @@ -72,10 +74,10 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { fatal("no layers configured!") } - log.Printf("wanted layers: %q", cfg.Layers) - layersInMemory := paramBool("layers-in-mem", false) + log.Info().Strs("layers", cfg.Layers).Bool("in-memory", layersInMemory).Msg("mounting layers") + const layersInMemDir = "/layers-in-mem" if layersInMemory { mkdir(layersInMemDir, 0700) @@ -84,6 +86,8 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { lowers := make([]string, len(cfg.Layers)) for i, layer := range cfg.Layers { + log := log.With().Str("layer", layer).Logger() + path := layerPath(layer) info, err := os.Stat(path) @@ -91,10 +95,10 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { fatal(err) } - log.Printf("layer %s found (%d bytes)", layer, info.Size()) + log.Info().Int64("size", info.Size()).Msg("layer found") if layersInMemory { - log.Print(" copying to memory...") + log.Info().Msg("copying to memory") targetPath := filepath.Join(layersInMemDir, layer) cp(path, targetPath) path = targetPath @@ -126,7 +130,7 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { if bootMounted { if layersInMemory { if err := syscall.Unmount("/boot", 0); err != nil { - log.Print("WARNING: failed to unmount /boot: ", err) + log.Warn().Err(err).Msg("failed to unmount /boot") time.Sleep(2 * time.Second) } @@ -136,60 +140,47 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { } // - write configuration - log.Print("writing /config.yaml") + log.Info().Msg("writing /config.yaml") if err := os.WriteFile("/system/config.yaml", cfgBytes, 0600); err != nil { fatal("failed: ", err) } // - write files - for _, fileDef := range cfg.Files { - log.Print("writing ", fileDef.Path) - - filePath := filepath.Join("/system", fileDef.Path) - - if err := os.MkdirAll(filepath.Dir(filePath), 0755); err != nil { - log.Printf("failed to create dir %s: %v", filepath.Dir(fileDef.Path), err) - } - - mode := fileDef.Mode - if mode == 0 { - mode = 0644 - } - - err = os.WriteFile(filePath, []byte(fileDef.Content), mode) - if err != nil { - fatalf("failed to write %s: %v", fileDef.Path, err) - } - } + apply.Files(cfg.Files, "/system") // - groups for _, group := range cfg.Groups { - log.Print("creating group ", group.Name) + logEvt := log.Info().Str("group", group.Name) opts := make([]string, 0) opts = append(opts /* chroot */, "/system", "groupadd", "-r") if group.Gid != 0 { opts = append(opts, "-g", strconv.Itoa(group.Gid)) + logEvt.Int("gid", group.Gid) } opts = append(opts, group.Name) + logEvt.Msg("creating group") run("chroot", opts...) } // - user for _, user := range cfg.Users { - log.Print("creating user ", user.Name) + logEvt := log.Info().Str("user", user.Name) opts := make([]string, 0) opts = append(opts /* chroot */, "/system", "useradd", "-r") if user.Gid != 0 { opts = append(opts, "-g", strconv.Itoa(user.Gid)) + logEvt.Int("gid", user.Gid) } if user.Uid != 0 { opts = append(opts, "-u", strconv.Itoa(user.Uid)) + logEvt.Int("uid", user.Uid) } opts = append(opts, user.Name) + logEvt.Msg("creating user") run("chroot", opts...) } @@ -197,11 +188,8 @@ func applyConfig(cfgPath string, bootMounted bool) (cfg *configV1) { } func finalizeBoot() { - // clean zombies - cleanZombies() - // switch root - log.Print("switching root") + log.Info().Msg("switching root") err := syscall.Exec("/sbin/switch_root", []string{"switch_root", "-c", "/dev/console", "/system", "/sbin/init"}, os.Environ()) fatal("switch_root failed: ", err) diff --git a/boot-v2.go b/boot-v2.go index 508fa7a..af5d944 100644 --- a/boot-v2.go +++ b/boot-v2.go @@ -1,20 +1,20 @@ package main import ( - "log" "os" "os/exec" + "github.com/rs/zerolog/log" "gopkg.in/yaml.v3" config "novit.tech/direktil/pkg/bootstrapconfig" ) func bootV2() { - log.Print("-- boot v2 --") + log.Info().Msg("-- boot v2 --") kernelVersion := unameRelease() - log.Print("Linux version ", kernelVersion) + log.Info().Str("version", kernelVersion).Msg("Linux") cfg := &config.Config{} @@ -32,15 +32,19 @@ func bootV2() { } } - log.Print("config loaded") - log.Printf("\n\nanti-phishing code: %q\n", cfg.AntiPhishingCode) + log.Info().Msg("config loaded") + + if cfg.AntiPhishingCode != "" { + log.Info().Str("anti-phishing-code", cfg.AntiPhishingCode).Send() + } auths = cfg.Auths // mount kernel modules if cfg.Modules == "" { - log.Print("NOT mounting modules (nothing specified)") + log.Warn().Msg("NOT mounting modules (\"modules:\" not specified)") } else { + log.Info().Str("from", cfg.Modules).Msg("mounting modules") mountSquahfs(cfg.Modules, "/modules") modulesSourcePath := "/modules/lib/modules/" + kernelVersion @@ -55,19 +59,23 @@ func bootV2() { } // load basic modules - run("modprobe", "unix") + for _, module := range []string{"unix"} { + log.Info().Str("module", module).Msg("loading module") + run("modprobe", module) + } // devices init + log.Info().Msg("starting udevd") err := exec.Command("udevd").Start() if err != nil { fatal("failed to start udevd: ", err) } - log.Print("udevadm triggers") + log.Info().Msg("udevadm triggers") run("udevadm", "trigger", "-c", "add", "-t", "devices") run("udevadm", "trigger", "-c", "add", "-t", "subsystems") - log.Print("udevadm settle") + log.Info().Msg("udevadm settle") run("udevadm", "settle") // networks diff --git a/bootstrap.go b/bootstrap.go index 069a314..7a3a57d 100644 --- a/bootstrap.go +++ b/bootstrap.go @@ -4,12 +4,13 @@ import ( "bytes" "fmt" "io" - "log" "net/http" "os" "path/filepath" "strings" + "github.com/rs/zerolog/log" + config "novit.tech/direktil/pkg/bootstrapconfig" ) @@ -27,14 +28,14 @@ func bootstrap(cfg *config.Config) { sysCfgPath := filepath.Join(baseDir, "config.yaml") if _, err := os.Stat(sysCfgPath); os.IsNotExist(err) { - log.Printf("bootstrap %q does not exist", bootVersion) + log.Warn().Msgf("bootstrap %q does not exist", bootVersion) seed := cfg.Bootstrap.Seed if seed == "" { fatalf("boostrap seed not defined, admin required") } - log.Printf("seeding bootstrap from %s", seed) + log.Info().Str("from", seed).Msgf("seeding bootstrap") err = os.MkdirAll(baseDir, 0700) if err != nil { @@ -72,7 +73,7 @@ func bootstrap(cfg *config.Config) { fatalf("seeding failed: %v", err) } - log.Print("unpacking bootstrap file") + log.Info().Msg("unpacking bootstrap file") run("tar", "xvf", bootstrapFile, "-C", baseDir) } @@ -89,7 +90,7 @@ func bootstrap(cfg *config.Config) { // mounts are v2+ for _, mount := range sysCfg.Mounts { - log.Print("mount ", mount.Dev, " to system's ", mount.Path) + log.Info().Str("source", mount.Dev).Str("target", mount.Path).Msg("mount") path := filepath.Join("/system", mount.Path) @@ -108,15 +109,16 @@ func bootstrap(cfg *config.Config) { // setup root user if ph := sysCfg.RootUser.PasswordHash; ph != "" { - log.Print("setting root's password") + log.Info().Msg("setting root's password") setUserPass("root", ph) } if ak := sysCfg.RootUser.AuthorizedKeys; len(ak) != 0 { - log.Print("setting root's authorized keys") + log.Info().Msg("setting root's authorized keys") setAuthorizedKeys(ak) } // update-ca-certificates + log.Info().Msg("updating CA certifices") run("chroot", "/system", "update-ca-certificates") } diff --git a/go.mod b/go.mod index 12b9ede..6bee514 100644 --- a/go.mod +++ b/go.mod @@ -1,16 +1,17 @@ -module novit.nc/direktil/initrd +module novit.tech/direktil/initrd require ( github.com/freddierice/go-losetup/v2 v2.0.1 github.com/kr/pty v1.1.8 github.com/pkg/term v1.1.0 - golang.org/x/crypto v0.16.0 - golang.org/x/sys v0.15.0 - golang.org/x/term v0.15.0 + github.com/rs/zerolog v1.31.0 + golang.org/x/crypto v0.18.0 + golang.org/x/sys v0.16.0 + golang.org/x/term v0.16.0 golang.zx2c4.com/wireguard/wgctrl v0.0.0-20230429144221-925a1e7659e6 gopkg.in/yaml.v2 v2.4.0 gopkg.in/yaml.v3 v3.0.1 - novit.tech/direktil/pkg v0.0.0-20231217121409-827fa62f58aa + novit.tech/direktil/pkg v0.0.0-20240120153454-29595df7b8ff ) require ( @@ -18,11 +19,13 @@ require ( github.com/creack/pty v1.1.21 // indirect github.com/google/go-cmp v0.6.0 // indirect github.com/josharian/native v1.1.0 // indirect + github.com/mattn/go-colorable v0.1.13 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect github.com/mdlayher/genetlink v1.3.2 // indirect github.com/mdlayher/netlink v1.7.2 // indirect github.com/mdlayher/socket v0.5.0 // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect + golang.org/x/net v0.20.0 // indirect + golang.org/x/sync v0.6.0 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect ) diff --git a/go.sum b/go.sum index ea13b12..b63c0f4 100644 --- a/go.sum +++ b/go.sum @@ -1,5 +1,6 @@ github.com/cavaliergopher/cpio v1.0.1 h1:KQFSeKmZhv0cr+kawA3a0xTQCU4QxXF1vhU7P7av2KM= github.com/cavaliergopher/cpio v1.0.1/go.mod h1:pBdaqQjnvXxdS/6CvNDwIANIFSP0xRKI16PX4xejRQc= +github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.18 h1:n56/Zwd5o6whRC5PMGretI4IdRLlmBXYNjScPaBgsbY= github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= @@ -7,6 +8,7 @@ github.com/creack/pty v1.1.21 h1:1/QdRyBaHHJP61QkWMXlOIBfsgdDeeKfK8SYVUWJKf0= github.com/creack/pty v1.1.21/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr4O4= github.com/freddierice/go-losetup/v2 v2.0.1 h1:wPDx/Elu9nDV8y/CvIbEDz5Xi5Zo80y4h7MKbi3XaAI= github.com/freddierice/go-losetup/v2 v2.0.1/go.mod h1:TEyBrvlOelsPEhfWD5rutNXDmUszBXuFnwT1kIQF4J8= +github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= @@ -20,6 +22,13 @@ github.com/kr/pty v1.1.8 h1:AkaSdXYQOWeaO3neb8EM634ahkXXe3jYbVh/F9lq+GI= github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= +github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA= +github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg= +github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= +github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mdlayher/genetlink v1.3.1 h1:roBiPnual+eqtRkKX2Jb8UQN5ZPWnhDCGj/wR6Jlz2w= github.com/mdlayher/genetlink v1.3.1/go.mod h1:uaIPxkWmGk753VVIzDtROxQ8+T+dkHqOI0vB1NA9S/Q= github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw= @@ -34,8 +43,12 @@ github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI github.com/mdlayher/socket v0.5.0/go.mod h1:WkcBFfvyG8QENs5+hfQPl1X6Jpd2yeLIYgrGFmJiJxI= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws= github.com/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721/go.mod h1:Ickgr2WtCLZ2MDGd4Gr0geeCH5HybhRJbonOgQpvSxc= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/term v1.1.0 h1:xIAAdCMh3QIAy+5FrE8Ad8XoDhEU4ufwbaSozViP9kk= github.com/pkg/term v1.1.0/go.mod h1:E25nymQcrSllhX42Ok8MRm1+hyBdHY0dCeiKZ9jpNGw= +github.com/rs/xid v1.5.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg= +github.com/rs/zerolog v1.31.0 h1:FcTR3NnLWW+NnTwwhFWiJSZr4ECLpqCm6QsEnyvbV4A= +github.com/rs/zerolog v1.31.0/go.mod h1:/7mN4D5sKwJLZQ2b/znpjC3/GQWY/xaDXUM0kKWRHss= github.com/ulikunitz/xz v0.5.6/go.mod h1:2bypXElzHzzJZwzH67Y6wb67pO62Rzfn7BSiF4ABRW8= github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= @@ -46,6 +59,8 @@ golang.org/x/crypto v0.5.0 h1:U/0M97KRkSFvyD/3FSmdP5W5swImpNgle/EHFhOsQPE= golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU= golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= +golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= +golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -58,12 +73,16 @@ golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= +golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= +golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.1.0 h1:wsuoTGHzEhffawBOhz5CYhcrV4IdKZbEyZjBMuTp12o= golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= +golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= +golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20200909081042-eff7692f9009/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -71,12 +90,17 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.4.0 h1:Zr2JFtRQNX3BCZ8YtxRE9hNJYC8J6I1MVbMg6owUp18= golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.12.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc= golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= +golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.4.0 h1:O7UWfv5+A2qiuulQk30kVinPoMtoIPeVaKLEgLpVkvg= @@ -85,6 +109,8 @@ golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= +golang.org/x/term v0.16.0 h1:m+B6fahuftsE9qjo0VWp2FW0mB3MTJvR0BaMQrq0pmE= +golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= @@ -120,3 +146,5 @@ novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e h1:eQFbzcuB4wOSrnOhkc novit.tech/direktil/pkg v0.0.0-20230201224712-5e39572dc50e/go.mod h1:2Mir5x1eT/e295WeFGzzXa4siunKX4z+rmNPfVsXS0k= novit.tech/direktil/pkg v0.0.0-20231217121409-827fa62f58aa h1:eBk9nQTxIJU5cT8aJVjfRWiUd4sv8YV0kXALbSFOKdI= novit.tech/direktil/pkg v0.0.0-20231217121409-827fa62f58aa/go.mod h1:AYEEjNi7ljJG+V4F4LzxWntfbSs+KnNPO3kqvcEzIU4= +novit.tech/direktil/pkg v0.0.0-20240120153454-29595df7b8ff h1:GctOfnciAzBRoEOxFulavphO3vSVj1jR6DZVlyGueJU= +novit.tech/direktil/pkg v0.0.0-20240120153454-29595df7b8ff/go.mod h1:zjezU6tELE880oYHs/WAauGBupKIEQQ7KqWTB69RW10= diff --git a/lvm.go b/lvm.go index c32da7b..f058f73 100644 --- a/lvm.go +++ b/lvm.go @@ -3,15 +3,17 @@ package main import ( "bytes" "io/fs" - "log" "os" "os/exec" "path/filepath" "sort" "strconv" - "novit.nc/direktil/initrd/lvm" + "github.com/rs/zerolog/log" + config "novit.tech/direktil/pkg/bootstrapconfig" + + "novit.tech/direktil/initrd/lvm" ) func sortedKeys[T any](m map[string]T) (keys []string) { @@ -25,7 +27,7 @@ func sortedKeys[T any](m map[string]T) (keys []string) { func setupLVM(cfg *config.Config) { if len(cfg.LVM) == 0 { - log.Print("no LVM VG configured.") + log.Info().Msg("no LVM VG configured.") return } @@ -75,15 +77,17 @@ func setupVG(vg config.LvmVG) { } } + log := log.With().Str("vg", vg.VG).Logger() + if devNeeded <= 0 { - log.Print("LVM VG ", vg.VG, " has all its devices") + log.Info().Msg("LVM VG has all its devices") return } if vgExists { - log.Printf("LVM VG %s misses %d devices", vg.VG, devNeeded) + log.Info().Msgf("LVM VG misses %d devices", devNeeded) } else { - log.Printf("LVM VG %s does not exists, creating", vg.VG) + log.Info().Msg("LVM VG does not exists, creating") } devNames := make([]string, 0) @@ -108,20 +112,16 @@ func setupVG(vg config.LvmVG) { m := regexpSelectN(vg.PVs.N, vg.PVs.Regexps, devNames) if len(m) == 0 { - log.Printf("no devices match the regexps %v", vg.PVs.Regexps) - for _, d := range devNames { - log.Print("- ", d) - } - + log.Error().Strs("regexps", vg.PVs.Regexps).Msg("no device match the regexps") fatalf("failed to setup VG %s", vg.VG) } if vgExists { - log.Print("- extending vg to ", m) + log.Info().Strs("devices", m).Msg("LVM VG: extending") run("vgextend", append([]string{vg.VG}, m...)...) devNeeded -= len(m) } else { - log.Print("- creating vg with devices ", m) + log.Info().Strs("devices", m).Msg("LVM VG: creating") run("vgcreate", append([]string{vg.VG}, m...)...) devNeeded -= len(m) } @@ -142,17 +142,17 @@ func setupLVs(vg config.LvmVG, createdDevs map[string]string) { defaults := vg.Defaults - for _, lv := range vg.LVs { - lvKey := vg.VG + "/" + lv.Name + for idx, lv := range vg.LVs { + log := log.With().Str("vg", vg.VG).Str("lv", lv.Name).Logger() if contains(lvs, func(v lvm.LV) bool { return v.VGName == vg.VG && v.Name == lv.Name }) { - log.Printf("LV %s exists", lvKey) + log.Info().Msg("LV exists") continue } - log.Printf("creating LV %s", lvKey) + log.Info().Msg("LV does not exist") if lv.Raid == nil { lv.Raid = defaults.Raid @@ -161,7 +161,7 @@ func setupLVs(vg config.LvmVG, createdDevs map[string]string) { args := make([]string, 0) if lv.Name == "" { - fatalf("LV has no name") + fatalf("LV[%d] has no name", idx) } args = append(args, vg.VG, "--name", lv.Name) @@ -184,7 +184,7 @@ func setupLVs(vg config.LvmVG, createdDevs map[string]string) { } } - log.Print("lvcreate args: ", args) + log.Info().Strs("args", args).Msg("LV: creating") run("lvcreate", args...) dev := "/dev/" + vg.VG + "/" + lv.Name @@ -286,12 +286,12 @@ func setupCrypt(devSpecs []config.CryptDev, createdDevs map[string]string) { } if !eq { - log.Print("passwords don't match") + log.Error().Msg("passwords don't match") goto retry } } - log.Print("formatting encrypted device ", dev) + log.Info().Str("dev", dev).Msg("formatting encrypted device") cmd := exec.Command("cryptsetup", "luksFormat", dev, "--key-file=-") cmd.Stdin = bytes.NewBuffer(password) cmd.Stdout = stdout @@ -312,7 +312,7 @@ func setupCrypt(devSpecs []config.CryptDev, createdDevs map[string]string) { } } - log.Print("openning encrypted device ", name, " from ", dev) + log.Info().Str("name", name).Str("dev", dev).Msg("openning encrypted device") cmd := exec.Command("cryptsetup", "open", dev, name, "--key-file=-") cmd.Stdin = bytes.NewBuffer(password) cmd.Stdout = stdout @@ -364,7 +364,7 @@ func devInitialized(dev string) bool { func setupFS(dev, fs string) { if devInitialized(dev) { - log.Print("device ", dev, " already formatted") + log.Info().Str("dev", dev).Msg("device already formatted") return } @@ -372,7 +372,7 @@ func setupFS(dev, fs string) { fs = "ext4" } - log.Print("formatting ", dev, " (", fs, ")") + log.Info().Str("dev", dev).Str("fs", fs).Msg("formatting device") args := make([]string, 0) switch fs { diff --git a/main.go b/main.go index 45464f9..fbc0e40 100644 --- a/main.go +++ b/main.go @@ -3,7 +3,6 @@ package main import ( "fmt" "io" - "log" "os" "os/exec" "path/filepath" @@ -12,10 +11,12 @@ import ( "time" "github.com/pkg/term/termios" + "github.com/rs/zerolog" + "github.com/rs/zerolog/log" "golang.org/x/term" - "novit.nc/direktil/initrd/colorio" - "novit.nc/direktil/initrd/shio" + "novit.tech/direktil/initrd/colorio" + "novit.tech/direktil/initrd/shio" ) const ( @@ -41,11 +42,13 @@ func newPipe() (io.ReadCloser, io.WriteCloser) { } func main() { + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + switch baseName := filepath.Base(os.Args[0]); baseName { case "init": runInit() default: - log.Fatal("unknown sub-command: ", baseName) + log.Fatal().Msgf("unknown sub-command: %q", baseName) } } @@ -57,18 +60,19 @@ func runInit() { runtime.LockOSThread() - if pid := os.Getpid(); pid != 1 { - log.Fatal("init must be PID 1, not ", pid) - } - // move log to shio go io.Copy(os.Stdout, stdout.NewReader()) - log.SetOutput(stderr) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: stderr}) + + // check the PID is 1 + if pid := os.Getpid(); pid != 1 { + log.Fatal().Int("pid", pid).Msg("init must be PID 1") + } // copy os.Stdin to my stdin pipe go io.Copy(stdinPipe, os.Stdin) - log.Print("Welcome to ", VERSION) + log.Info().Msg("Welcome to " + VERSION) // essential mounts mount("none", "/proc", "proc", 0, "") @@ -78,7 +82,7 @@ func runInit() { // get the "boot version" bootVersion = param("version", "current") - log.Printf("booting system %q", bootVersion) + log.Info().Msgf("booting system %q", bootVersion) os.Setenv("PATH", "/usr/bin:/bin:/usr/sbin:/sbin") @@ -107,19 +111,20 @@ func layerPath(name string) string { } func fatal(v ...interface{}) { - log.Print("*** FATAL ***") - log.Print(v...) + log.Error().Msg("*** FATAL ***") + log.Error().Msg(fmt.Sprint(v...)) die() } func fatalf(pattern string, v ...interface{}) { - log.Print("*** FATAL ***") - log.Printf(pattern, v...) + log.Error().Msg("*** FATAL ***") + log.Error().Msgf(pattern, v...) die() } func die() { - log.SetOutput(os.Stderr) + log.Logger = log.Output(zerolog.ConsoleWriter{Out: os.Stderr}) + stdout.Close() stdin.Close() stdinPipe.Close() @@ -138,13 +143,13 @@ mainLoop: deadline := time.Now().Add(time.Minute) os.Stdin.SetReadDeadline(deadline) - termios.Tcflush(os.Stdin.Fd(), termios.TCIFLUSH) term.MakeRaw(int(os.Stdin.Fd())) + termios.Tcflush(os.Stdin.Fd(), termios.TCIFLUSH) b := make([]byte, 1) _, err := os.Stdin.Read(b) if err != nil { - log.Print("failed to read from stdin: ", err) + log.Error().Err(err).Msg("failed to read from stdin") time.Sleep(5 * time.Second) syscall.Reboot(syscall.LINUX_REBOOT_CMD_RESTART) } @@ -184,10 +189,10 @@ mainLoop: } continue mainLoop } - log.Print("failed to find a shell!") + log.Error().Msg("failed to find a shell!") default: - log.Printf("unknown choice: %q", string(b)) + log.Error().Msgf("unknown choice: %q", string(b)) } } } @@ -212,7 +217,7 @@ func mount(source, target, fstype string, flags uintptr, data string) { if err := syscall.Mount(source, target, fstype, flags, data); err != nil { fatalf("mount %q %q -t %q -o %q failed: %v", source, target, fstype, data, err) } - log.Printf("mounted %q", target) + log.Info().Str("target", target).Msg("mounted") } func cp(srcPath, dstPath string) { diff --git a/modd.conf b/modd.conf index 10d82c1..b0e96bb 100644 --- a/modd.conf +++ b/modd.conf @@ -3,6 +3,7 @@ modd.conf {} go.??? **/*.go { prep: go test ./... prep: mkdir -p dist + prep: go build -o dist/init . prep: go build -o dist/ ./tools/... } diff --git a/network.go b/network.go index 08f43a6..428a8f8 100644 --- a/network.go +++ b/network.go @@ -1,17 +1,18 @@ package main import ( - "log" "net" "os/exec" "strings" + "github.com/rs/zerolog/log" + config "novit.tech/direktil/pkg/bootstrapconfig" ) func setupNetworks(cfg *config.Config) { if len(cfg.Networks) == 0 { - log.Print("no networks configured.") + log.Info().Msg("no networks configured.") return } @@ -29,7 +30,8 @@ func setupNetworks(cfg *config.Config) { assigned := map[string]bool{} for _, network := range cfg.Networks { - log.Print("setting up network ", network.Name) + log := log.With().Str("network", network.Name).Logger() + log.Info().Msg("setting up network") // compute available names if len(assigned) != 0 { @@ -60,10 +62,11 @@ func setupNetworks(cfg *config.Config) { assigned[m] = true } - log.Print("- ", envvar) envvars = append(envvars, envvar.String()) } + log.Info().Strs("env", envvars).Msg("running script") + cmd := exec.Command("/bin/sh", "-c", network.Script) cmd.Env = envvars cmd.Stdout = stdout diff --git a/regexp.go b/regexp.go index 9b633a3..bd7ebe7 100644 --- a/regexp.go +++ b/regexp.go @@ -1,8 +1,9 @@ package main import ( - "log" "regexp" + + "github.com/rs/zerolog/log" ) func regexpSelectN(n int, regexps []string, names []string) (matches []string) { @@ -16,7 +17,7 @@ func regexpSelectN(n int, regexps []string, names []string) (matches []string) { for _, reStr := range regexps { re, err := regexp.Compile(reStr) if err != nil { - log.Printf("warning: invalid regexp ignored: %q: %v", reStr, err) + log.Warn().Err(err).Str("regexp", reStr).Msg("invalid regexp, ignored") continue } res = append(res, re) diff --git a/shio/shio.go b/shio/shio.go index d742a85..b9259f6 100644 --- a/shio/shio.go +++ b/shio/shio.go @@ -5,7 +5,7 @@ import ( "io" "sync" - "novit.nc/direktil/initrd/colorio" + "novit.tech/direktil/initrd/colorio" ) type ShIO struct { diff --git a/ssh.go b/ssh.go index 46f212b..f715c4f 100644 --- a/ssh.go +++ b/ssh.go @@ -4,7 +4,6 @@ import ( "encoding/binary" "fmt" "io" - "log" "net" "os" "os/exec" @@ -13,6 +12,7 @@ import ( "unsafe" "github.com/kr/pty" + "github.com/rs/zerolog/log" "golang.org/x/crypto/ssh" config "novit.tech/direktil/pkg/bootstrapconfig" @@ -26,20 +26,24 @@ func startSSH(cfg *config.Config) { hostKeyLoaded := false for _, format := range []string{"rsa", "dsa", "ecdsa", "ed25519"} { + log := log.With().Str("format", format).Logger() + pkBytes, err := os.ReadFile("/id_" + format) if err != nil { - log.Printf("ssh : failed to load %s host key: %v", format, err) + log.Error().Err(err).Msg("ssh: failed to load host key") continue } pk, err := ssh.ParsePrivateKey(pkBytes) if err != nil { - log.Printf("ssh: failed to parse %s host key: %v", format, err) + log.Error().Err(err).Msg("ssh: failed to parse host key") continue } sshConfig.AddHostKey(pk) hostKeyLoaded = true + + log.Info().Msg("ssh: loaded host key") } if !hostKeyLoaded { @@ -52,13 +56,13 @@ func startSSH(cfg *config.Config) { fatalf("ssh: failed to listen on %s: %v", sshBind, err) } - log.Print("SSH server listening on ", sshBind) + log.Info().Str("bind-address", sshBind).Msg("SSH server listening") go func() { for { conn, err := listener.Accept() if err != nil { - log.Print("ssh: accept conn failed: ", err) + log.Info().Err(err).Msg("ssh: accept conn failed") continue } @@ -71,12 +75,12 @@ func sshHandleConn(conn net.Conn, sshConfig *ssh.ServerConfig) { sshConn, chans, reqs, err := ssh.NewServerConn(conn, sshConfig) if err != nil { - log.Print("ssh: handshake failed: ", err) + log.Error().Err(err).Msg("ssh: handshake failed") return } remoteAddr := sshConn.User() + "@" + sshConn.RemoteAddr().String() - log.Print("ssh: new connection from ", remoteAddr) + log.Info().Str("remote", remoteAddr).Msg("ssh: new connection") go sshHandleReqs(reqs) go sshHandleChannels(remoteAddr, chans) @@ -89,7 +93,7 @@ func sshHandleReqs(reqs <-chan *ssh.Request) { req.Reply(true, nil) default: - log.Printf("ssh: discarding req: %+v", req) + log.Info().Str("type", req.Type).Msg("ssh: discarding request") req.Reply(false, nil) } } @@ -104,7 +108,7 @@ func sshHandleChannels(remoteAddr string, chans <-chan ssh.NewChannel) { channel, requests, err := newChannel.Accept() if err != nil { - log.Print("ssh: failed to accept channel: ", err) + log.Error().Err(err).Msg("ssh: failed to accept channel") continue } @@ -211,7 +215,7 @@ func sshHandleChannel(remoteAddr string, channel ssh.Channel, requests <-chan *s var err error ptyF, ttyF, err = pty.Open() if err != nil { - log.Print("PTY err: ", err) + log.Error().Err(err).Msg("ssh: PTY open failed") req.Reply(false, nil) continue } diff --git a/tools/cpiocat/main.go b/tools/cpiocat/main.go index af33f45..40f2282 100644 --- a/tools/cpiocat/main.go +++ b/tools/cpiocat/main.go @@ -2,7 +2,7 @@ package main import ( "flag" - "log" + "fmt" "os" "novit.tech/direktil/pkg/cpiocat" @@ -13,6 +13,7 @@ func main() { err := cpiocat.Append(os.Stdout, os.Stdin, flag.Args()) if err != nil { - log.Fatal(err) + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) } } diff --git a/tools/testconf/main.go b/tools/testconf/main.go index 5b49000..500d661 100644 --- a/tools/testconf/main.go +++ b/tools/testconf/main.go @@ -3,7 +3,7 @@ package main import ( "bytes" "flag" - "log" + "fmt" "os" "gopkg.in/yaml.v3" @@ -14,12 +14,10 @@ func main() { flag.Parse() for _, arg := range flag.Args() { - log.Print("testing ", arg) + fmt.Println("testing", arg) cfgBytes, err := os.ReadFile(arg) - if err != nil { - log.Fatal(err) - } + fail(err) cfg := config.Config{} @@ -27,8 +25,13 @@ func main() { dec.KnownFields(true) err = dec.Decode(&cfg) - if err != nil { - log.Fatal(err) - } + fail(err) + } +} + +func fail(err error) { + if err != nil { + fmt.Fprintln(os.Stderr, err.Error()) + os.Exit(1) } } diff --git a/vpn.go b/vpn.go index 3e6a121..408d9a3 100644 --- a/vpn.go +++ b/vpn.go @@ -1,11 +1,11 @@ package main import ( - "log" "net" "os" "path/filepath" + "github.com/rs/zerolog/log" "golang.zx2c4.com/wireguard/wgctrl" "golang.zx2c4.com/wireguard/wgctrl/wgtypes" @@ -13,11 +13,18 @@ import ( ) func setupVPN(vpn config.VPNDef, localGenDir string) { - log.Printf("setting up VPN %s", vpn.Name) + log := log.With().Str("vpn", vpn.Name).Logger() + + log.Info().Msg("VPN: setting up") vpnDir := filepath.Join(localGenDir, vpn.Name) os.MkdirAll(vpnDir, 0750) + logMsg := log.Info() + if vpn.ListenPort != nil { + logMsg.Int("ListenPort", *vpn.ListenPort) + } + // public/private key keyFile := filepath.Join(vpnDir, "key") keyBytes, err := os.ReadFile(keyFile) @@ -39,7 +46,7 @@ func setupVPN(vpn config.VPNDef, localGenDir string) { fatalf("bad VPN key: %v", err) } - log.Printf("VPN %s public key is %s", vpn.Name, key.PublicKey().String()) + logMsg.Stringer("PublicKey", key.PublicKey()) // pre-shared key pskeyFile := filepath.Join(vpnDir, "pskey") @@ -62,7 +69,10 @@ func setupVPN(vpn config.VPNDef, localGenDir string) { fatalf("bad VPN pre-shared key: %v", err) } - log.Printf("VPN %s pre-shared key is %s", vpn.Name, key.String()) + { + keyStr := key.String() + logMsg.Str("PresharedKey", keyStr[0:4]+"..."+keyStr[len(keyStr)-4:]) + } // setup interface cfg := wgtypes.Config{ @@ -110,12 +120,14 @@ func setupVPN(vpn config.VPNDef, localGenDir string) { } defer wg.Close() + log.Info().Strs("ips", vpn.IPs).Msg("VPN: creating interface") run("ip", "link", "add", vpn.Name, "type", "wireguard") for _, ip := range vpn.IPs { run("ip", "addr", "add", ip, "dev", vpn.Name) } + logMsg.Msg("VPN: configuring interface") err = wg.ConfigureDevice(vpn.Name, cfg) if err != nil { fatalf("failed to setup VPN %s: %v", vpn.Name, err) diff --git a/zombies.go b/zombies.go deleted file mode 100644 index 33aa35c..0000000 --- a/zombies.go +++ /dev/null @@ -1,26 +0,0 @@ -package main - -import ( - "log" - "syscall" -) - -func cleanZombies() { - return // FIXME noop... udhcpc is a daemon staying alive so we never finish - - var wstatus syscall.WaitStatus - - for { - pid, err := syscall.Wait4(-1, &wstatus, 0, nil) - switch err { - case nil: - log.Printf("collected PID %v", pid) - - case syscall.ECHILD: - return - - default: - log.Printf("unknown error: %v", err) - } - } -}