mirror of
https://github.com/kiddin9/op-packages.git
synced 2026-11-14 20:48:17 +08:00
175 lines
5.8 KiB
Go
175 lines
5.8 KiB
Go
// Package packaging checks the files that ship to the device but are not Go.
|
|
//
|
|
// The init script is a contract the Go side depends on and cannot verify from
|
|
// inside itself: it decides what gets started, how it gets stopped, and what
|
|
// happens when it dies. Nothing in `go test ./internal/...` would notice it
|
|
// drifting, so it is asserted here.
|
|
package packaging
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// repoRoot is three levels up from core/tests/packaging.
|
|
func repoRoot(t *testing.T) string {
|
|
t.Helper()
|
|
root, err := filepath.Abs(filepath.Join("..", "..", ".."))
|
|
if err != nil {
|
|
t.Fatalf("resolve repository root: %v", err)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(root, "root", "etc", "init.d")); err != nil {
|
|
t.Fatalf("repository root %q has no device tree: %v", root, err)
|
|
}
|
|
return root
|
|
}
|
|
|
|
func initScript(t *testing.T) string {
|
|
t.Helper()
|
|
path := filepath.Join(repoRoot(t), "root", "etc", "init.d", "smart_srun")
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatalf("read the init script: %v", err)
|
|
}
|
|
return string(data)
|
|
}
|
|
|
|
// commands is the script with its comments removed.
|
|
//
|
|
// The checks below are about what the script does, not what it says about
|
|
// itself. Several of them exist because of a specific thing the Python version
|
|
// did, and the comment explaining that history names it -- which would fail a
|
|
// scan of the raw text. Splitting on an unquoted `#` is enough here because
|
|
// this script has none inside a string; if one ever appears, this helper is
|
|
// where to notice.
|
|
func commands(script string) string {
|
|
var kept []string
|
|
for line := range strings.SplitSeq(script, "\n") {
|
|
if index := strings.Index(line, "#"); index >= 0 {
|
|
line = line[:index]
|
|
}
|
|
if strings.TrimSpace(line) != "" {
|
|
kept = append(kept, line)
|
|
}
|
|
}
|
|
return strings.Join(kept, "\n")
|
|
}
|
|
|
|
// T25 -- procd starts the Go binary, in the foreground, as a supervised
|
|
// instance.
|
|
//
|
|
// Anything that forked, wrote its own pidfile or ran a wrapper would be a
|
|
// process procd could not supervise, respawn or stop, which is most of what
|
|
// procd is for.
|
|
func TestProcdStartsTheGoDaemonDirectly(t *testing.T) {
|
|
raw := initScript(t)
|
|
if !strings.HasPrefix(raw, "#!/bin/sh /etc/rc.common\n") {
|
|
t.Error("the init script does not start with OpenWrt's rc.common " +
|
|
"interpreter line, so rc.common's START and the procd helpers are " +
|
|
"not in scope")
|
|
}
|
|
|
|
script := commands(raw)
|
|
for _, required := range []string{
|
|
"USE_PROCD=1",
|
|
"/usr/bin/srunnet",
|
|
"procd_set_param command",
|
|
`"$PROG" daemon`,
|
|
} {
|
|
if !strings.Contains(script, required) {
|
|
t.Errorf("the init script does not contain %q", required)
|
|
}
|
|
}
|
|
if !strings.Contains(script, "procd_open_instance") ||
|
|
!strings.Contains(script, "procd_close_instance") {
|
|
t.Error("the service is not declared as a procd instance")
|
|
}
|
|
}
|
|
|
|
// T25 -- the daemon respawns without a budget that can be exhausted.
|
|
//
|
|
// The Python version used the default finite budget and the manual actions that
|
|
// restarted the service burned through it; after that the daemon stayed dead
|
|
// until the router was rebooted. `respawn <threshold> <timeout> 0` is procd's
|
|
// unlimited form.
|
|
func TestTheDaemonRespawnsWithoutAFiniteBudget(t *testing.T) {
|
|
script := commands(initScript(t))
|
|
|
|
var respawn string
|
|
for line := range strings.SplitSeq(script, "\n") {
|
|
if strings.Contains(line, "procd_set_param respawn") {
|
|
respawn = strings.TrimSpace(line)
|
|
}
|
|
}
|
|
if respawn == "" {
|
|
t.Fatal("the service does not ask procd to respawn it at all")
|
|
}
|
|
fields := strings.Fields(respawn)
|
|
if len(fields) != 5 {
|
|
t.Fatalf("respawn = %q, want threshold, timeout and retries", respawn)
|
|
}
|
|
if fields[4] != "0" {
|
|
t.Errorf("respawn retries = %q, want 0 (unlimited); a finite budget is "+
|
|
"exhausted by ordinary restarts and then the daemon stays dead",
|
|
fields[4])
|
|
}
|
|
}
|
|
|
|
// T25 -- stopping this service stops this service, and does not hunt for
|
|
// processes that look like it.
|
|
//
|
|
// The Python version had to scan /proc because the daemon, the CLI and the
|
|
// update worker were all the same script; a blunt stop killed installs halfway
|
|
// through and left the update status stuck. The Go update worker runs under its
|
|
// own procd service with its own copy of the binary, so procd's own instance
|
|
// tracking is exact -- and a process hunt reintroduced here would silently
|
|
// undo that.
|
|
func TestStoppingDoesNotHuntForProcesses(t *testing.T) {
|
|
script := commands(initScript(t))
|
|
|
|
for _, forbidden := range []string{
|
|
"/proc/[0-9]",
|
|
"kill -TERM",
|
|
"kill -KILL",
|
|
"killall",
|
|
"pgrep",
|
|
"pkill",
|
|
} {
|
|
if strings.Contains(script, forbidden) {
|
|
t.Errorf("the init script contains %q; procd knows which process it "+
|
|
"started, and a hunt catches the update worker too", forbidden)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The daemon is given long enough to shut down cleanly.
|
|
//
|
|
// It cancels its work, writes its last snapshot and releases its lock on
|
|
// SIGTERM, and allows ten seconds for a worker that ignores cancellation.
|
|
// procd's default five seconds would SIGKILL a shutdown that was about to
|
|
// finish, which is how a state file ends up saying "running" for a process that
|
|
// is gone.
|
|
func TestTheDaemonIsGivenTimeToShutDown(t *testing.T) {
|
|
script := commands(initScript(t))
|
|
if !strings.Contains(script, "procd_set_param term_timeout") {
|
|
t.Error("no term_timeout; procd's default is shorter than the daemon's " +
|
|
"own shutdown grace")
|
|
}
|
|
}
|
|
|
|
// The device package does not invoke Python from its service definition.
|
|
//
|
|
// The legacy Python runtime is retained in Git history only. The source audit
|
|
// checks the tree; this assertion checks what procd actually starts.
|
|
func TestTheServiceDefinitionInvokesNoPython(t *testing.T) {
|
|
script := commands(initScript(t))
|
|
for _, marker := range []string{"python", "client.py", "smart_srun/"} {
|
|
if strings.Contains(script, marker) {
|
|
t.Errorf("the init script mentions %q; the service procd starts is "+
|
|
"the Go binary", marker)
|
|
}
|
|
}
|
|
}
|