Two gaps. The bundle's contents are per system even though its mechanism is not, so there are now three: substrate-arch.lock, substrate-alpine.lock and substrate-android.lock. All three are embedded and a host reads only the one it was built for. Arch and Alpine remain placeholders -- the closure for a one-node mesh is still research 011/012's open question, and inventing it here would be worse than an honest placeholder. Android's is not a placeholder. It says a partial host cannot raise a mesh and why: every step of a bootstrap is a package, a container, or an action against one, and those are exactly the shapes it refuses. So a partial host can JOIN a mesh and cannot BE the first node. That belongs where somebody looking for the android bundle will find it. Also separated two things that were being conflated: "this system has no bundle" and "this system was never built". Loading a bundle for debian is not ErrEmpty, and the test asserts they differ. 0062 -- a host may be episodic. There is no way to keep a process running on an ordinary Android device: init needs root, a foreground service can be killed for memory. The answer is not to fight that. It is that being killed IS disconnection, which ADR 0036 already made an ordinary situation -- and everything the design does for a laptop that closes is what an episodic host needs, at a shorter period. An authoritative local store, reconcile on start, last-heard-from reported without an alarm. So the gap closes by requiring less rather than building something. No keep- alive, no Android daemon, no fighting the platform's process management. Two consequences recorded rather than glossed. Last-heard-from is a much weaker signal on an episodic host, so a healthy phone reads as a dead server unless the reader knows which kind it is looking at. And a declaration may take a long time to land, which makes 0058's separation of outstanding from failed load-bearing rather than tidy. Left open deliberately: how an episodic host is actually started, and -- first -- what an Android node is for. Building the start mechanism before deciding that would be building it for nobody.
83 lines
3.8 KiB
Go
83 lines
3.8 KiB
Go
package system
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/novox/mesh-host/internal/declaration"
|
|
)
|
|
|
|
// android is a partial host, and being partial is the point.
|
|
//
|
|
// It implements `file`, `directory` and `action` — the shapes that need only a filesystem and a
|
|
// way to run something — and refuses the other three. That is not a broken host: a declaration
|
|
// naming a shape this host does not implement is refused whole, the same treatment an unknown
|
|
// type gets, and the profile tells the control plane which shapes exist so it never sends one
|
|
// it cannot do (novox/hq ADR 0060).
|
|
//
|
|
// What it cannot do, and why:
|
|
//
|
|
// - **package** — there is no package manager an ordinary app may drive. Installing software
|
|
// on Android means the framework installing an APK, which is not something a process asks
|
|
// for on its own behalf.
|
|
// - **service** — Android's init reads .rc files from the system partition, which needs root
|
|
// and an unlocked bootloader. On a normal device nothing can register with it.
|
|
// - **container** — no container runtime, and no kernel access to give one.
|
|
//
|
|
// **This host is EPISODIC** (novox/hq ADR 0062). Everywhere else an init runs the launcher at
|
|
// boot and the launcher supervises the host. Android grants neither: nothing to register with
|
|
// without root, and nothing worth supervising, because a supervisor would be killed alongside
|
|
// what it supervises.
|
|
//
|
|
// So it runs when the platform allows and is killed when the platform wants the memory — and
|
|
// that is **disconnection**, which ADR 0036 already made an ordinary situation rather than an
|
|
// exception. It needs no keep-alive and no new mechanism: the store is already authoritative
|
|
// while disconnected, reconcile already happens on start, and the mesh already reports *last
|
|
// heard from* rather than alarming on silence.
|
|
//
|
|
// It also **cannot be the first node** — every step of raising a substrate is a shape it
|
|
// refuses — and its bundle says so rather than being an empty placeholder.
|
|
type android struct{}
|
|
|
|
func (android) Name() string { return "android" }
|
|
func (android) Shapes() []declaration.Type { return portableShapes() }
|
|
|
|
func (android) Confirm(ctx context.Context, run Runner) error {
|
|
// Ask the property service, which exists on every Android and nowhere else. A file path
|
|
// check would pass inside a chroot; this asks something only Android answers.
|
|
if _, err := run(ctx, "getprop", "ro.build.version.sdk"); err != nil {
|
|
return fmt.Errorf(
|
|
"this is the android host and the property service does not answer here. Either "+
|
|
"this is not Android, or it is a container without it: %w", err)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// The four below are unreachable through the ordinary path: Check refuses a declaration naming
|
|
// these shapes before anything is applied. They are here so that "unreachable" fails loudly if
|
|
// it ever stops being true, rather than a nil applier being called.
|
|
|
|
func (a android) PackageInstalled(context.Context, Runner, string) (bool, error) {
|
|
return false, fmt.Errorf("%w: package (there is no package manager an app may drive)", ErrUnsupported)
|
|
}
|
|
|
|
func (a android) InstallPackage(context.Context, Runner, string) error {
|
|
return fmt.Errorf("%w: package", ErrUnsupported)
|
|
}
|
|
|
|
func (a android) ServiceState(context.Context, Runner, string) (string, error) {
|
|
return "", fmt.Errorf("%w: service (init is not reachable without root)", ErrUnsupported)
|
|
}
|
|
|
|
func (a android) SetServiceState(context.Context, Runner, string, string) error {
|
|
return fmt.Errorf("%w: service", ErrUnsupported)
|
|
}
|
|
|
|
func (a android) ServiceBoot(context.Context, Runner, string) (string, error) {
|
|
return "", fmt.Errorf("%w: service", ErrUnsupported)
|
|
}
|
|
|
|
func (a android) SetServiceBoot(context.Context, Runner, string, string) error {
|
|
return fmt.Errorf("%w: service", ErrUnsupported)
|
|
}
|