diff --git a/README.md b/README.md index bc2edb2..9aae03b 100644 --- a/README.md +++ b/README.md @@ -8,15 +8,64 @@ Go implementation of the Wetware distributed computing system. go install github.com/wetware/go/cmd/ww ``` +## Quick Start + +Run WASM binaries directly with `ww run`: + +```bash +# Run a local WASM file +ww run ./myapp.wasm + +# Run from IPFS +ww run /ipfs/QmHash/myapp.wasm + +# Run from $PATH +ww run myapp + +# Run with debug info +ww run --wasm-debug ./myapp.wasm +``` + ## Commands -- `ww run ` - Execute binaries in isolated cells with IPFS support +- `ww run ` - Execute WASM binaries with libp2p networking +- `ww shell` - Interactive LISP shell with IPFS and P2P capabilities - `ww export ` - Add files/directories to IPFS - `ww import ` - Download content from IPFS - `ww idgen` - Generate Ed25519 private keys ## Architecture -Wetware provides capability-based security through isolated execution environments (cells) with controlled access to IPFS and other distributed services. Each cell runs in a jailed process with file descriptor-based capability passing. +Wetware provides capability-based security through WASM-based execution environments with controlled access to IPFS and other distributed services. Each WASM module runs with its `poll()` export served on libp2p streams at `/ww/0.1.0/{proc-id}`. + +### WASM Process Model + +- **Binary Resolution**: Supports local files, $PATH binaries, and IPFS paths +- **WASM Runtime**: Uses wazero for secure WASM execution +- **libp2p Integration**: Serves WASM `poll()` export on network streams +- **IPFS Support**: Direct access to IPFS for distributed content + +## Examples + +### Hello World WASM + +Build and run a simple WASM module: + +```bash +# Install tinygo +go install tinygo.org/x/tinygo@latest + +# Build example +cd examples/hello +tinygo build -target wasi -o hello.wasm main.go + +# Run with ww +ww run hello.wasm +``` + +See [examples/hello/README.md](examples/hello/README.md) for more details. + +## Documentation -See [spec/cell.md](spec/cell.md) for the cell API specification. +- [Cell API Specification](spec/cell.md) - Complete API reference +- [Shell Documentation](cmd/ww/shell/README.md) - Interactive shell guide diff --git a/cmd/internal/flags/flags.go b/cmd/internal/flags/flags.go index 0d5b73b..ce2303f 100644 --- a/cmd/internal/flags/flags.go +++ b/cmd/internal/flags/flags.go @@ -33,12 +33,6 @@ func CapabilityFlags() []cli.Flag { Usage: "grant process execution capability", EnvVars: []string{"WW_WITH_EXEC"}, }, - &cli.BoolFlag{ - Name: "with-mdns", - Category: "CAPABILITIES", - Usage: "grant mDNS peer discovery capability", - EnvVars: []string{"WW_WITH_MDNS"}, - }, &cli.BoolFlag{ Name: "with-p2p", Category: "CAPABILITIES", @@ -58,22 +52,6 @@ func P2PFlags() []cli.Flag { Usage: "connect to cluster through specified peers", EnvVars: []string{"WW_JOIN"}, }, - &cli.StringFlag{ - Name: "discover", - Category: "P2P", - Aliases: []string{"d"}, - Usage: "automatic peer discovery settings", - Value: "/mdns", - EnvVars: []string{"WW_DISCOVER"}, - }, - &cli.StringFlag{ - Name: "namespace", - Category: "P2P", - Aliases: []string{"ns"}, - Usage: "cluster namespace (must match dial host)", - Value: "ww", - EnvVars: []string{"WW_NAMESPACE"}, - }, &cli.BoolFlag{ Name: "dial", Category: "P2P", diff --git a/cmd/ww/args/args.go b/cmd/ww/args/args.go deleted file mode 100644 index 318c186..0000000 --- a/cmd/ww/args/args.go +++ /dev/null @@ -1,29 +0,0 @@ -package args - -const separator = "--" - -const GuestArgs = "guestArgs" - -// split host and guest arguments and remove the separator. -// The function will always return initialized slices. -func SplitArgs(args []string) ([]string, []string) { - if len(args) == 0 { - return []string{}, []string{} - } - - separatorIndex := -1 - for i, arg := range args { - if arg == separator { - separatorIndex = i - break - } - } - - if separatorIndex == -1 { - return args, []string{} - } - - hostArgs := args[:separatorIndex] - guestArgs := args[separatorIndex+1:] - return hostArgs, guestArgs -} diff --git a/cmd/ww/cat/cat.go b/cmd/ww/cat/cat.go new file mode 100644 index 0000000..8bd9b4c --- /dev/null +++ b/cmd/ww/cat/cat.go @@ -0,0 +1,184 @@ +package cat + +import ( + "context" + "errors" + "fmt" + "io" + "log/slog" + "os" + "syscall" + + "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" + "github.com/libp2p/go-libp2p/core/protocol" + "github.com/urfave/cli/v2" + "github.com/wetware/go/cmd/internal/flags" + "github.com/wetware/go/util" +) + +var env util.IPFSEnv + +func Command() *cli.Command { + return &cli.Command{ + Name: "cat", + ArgsUsage: " ", + Usage: "Connect to a peer and execute a procedure over a stream", + Description: `Connect to a specified peer and execute a procedure over a custom protocol stream. +The command will: +1. Initialize IPFS environment for stream forwarding +2. Use IPFS to establish connection to the specified peer +3. Forward the stream using the /ww/0.1.0/ protocol +4. Bind the stream to stdin/stdout for communication + +Examples: + ww cat QmPeer123 /echo + ww cat 12D3KooW... /myproc`, + Flags: append([]cli.Flag{ + &cli.StringFlag{ + Name: "ipfs", + EnvVars: []string{"WW_IPFS"}, + Value: "/ip4/127.0.0.1/tcp/5001/http", + Usage: "IPFS API endpoint", + }, + }, append(flags.CapabilityFlags(), flags.P2PFlags()...)...), + + Before: func(c *cli.Context) error { + return env.Boot(c.String("ipfs")) + }, + After: func(c *cli.Context) error { + return env.Close() + }, + + Action: Main, + } +} + +func Main(c *cli.Context) error { + ctx, cancel := context.WithTimeout(c.Context, c.Duration("timeout")) + defer cancel() + + if c.NArg() != 2 { + return cli.Exit("cat requires exactly two arguments: ", 1) + } + + peerIDStr := c.Args().Get(0) + procName := c.Args().Get(1) + + // Parse peer ID + peerID, err := peer.Decode(peerIDStr) + if err != nil { + return fmt.Errorf("invalid peer ID %s: %w", peerIDStr, err) + } + + // Construct protocol ID + protocolID := protocol.ID("/ww/0.1.0/" + procName) + + // Create libp2p host in client mode + h, err := util.NewClient() + if err != nil { + return fmt.Errorf("failed to create host: %w", err) + } + defer h.Close() + + dht, err := env.NewDHT(ctx, h) + if err != nil { + return fmt.Errorf("failed to create DHT client: %w", err) + } + defer dht.Close() + + // Set up DHT readiness monitoring BEFORE bootstrapping + slog.DebugContext(ctx, "setting up DHT readiness monitoring") + readyChan := make(chan error, 1) + go func() { + readyChan <- util.WaitForDHTReady(ctx, dht) + }() + + // Bootstrap the DHT to populate routing table with IPFS peers + slog.DebugContext(ctx, "bootstrapping DHT") + if err := dht.Bootstrap(ctx); err != nil { + return fmt.Errorf("failed to bootstrap DHT: %w", err) + } + + // Wait for DHT to be ready + slog.DebugContext(ctx, "waiting for DHT routing table to populate") + if err := <-readyChan; err != nil { + slog.WarnContext(ctx, "DHT may not be fully ready", "error", err) + } + + // Use DHT for peer discovery + slog.DebugContext(ctx, "searching for peer via DHT", "peer", peerID.String()[:12]) + + // Try to find the peer using DHT + peerInfo, err := dht.FindPeer(ctx, peerID) + if err != nil { + return fmt.Errorf("failed to find peer %s via DHT: %w", peerID, err) + } + + slog.DebugContext(ctx, "target peer found via DHT", "peer", peerInfo.ID.String()[:12]) + if err := h.Connect(ctx, peerInfo); err != nil { + return fmt.Errorf("failed to connect to peer %s: %w", peerInfo.ID, err) + } + + // Open stream to peer + stream, err := h.NewStream(ctx, peerID, protocolID) + if err != nil { + return fmt.Errorf("failed to open stream to peer %s: %w", peerID, err) + } + defer stream.Close() + + // Display connection banner + fmt.Printf("⚗️ Wetware Stream Connected\n") + fmt.Printf(" Peer: %s...\n", peerID.String()[:12]) + fmt.Printf(" Endpoint: %s\n", protocolID) + fmt.Printf(" Ctrl+C to exit\n\n") + + // Bind stream to stdin/stdout + return bindStreamToStdio(ctx, stream) +} + +func bindStreamToStdio(ctx context.Context, stream network.Stream) error { + // Copy data between stream and stdin/stdout + readDone := make(chan error, 1) + writeDone := make(chan error, 1) + + // Copy from stream to stdout + go func() { + _, err := io.Copy(os.Stdout, stream) + readDone <- err + }() + + // Copy from stdin to stream + go func() { + _, err := io.Copy(stream, os.Stdin) + writeDone <- err + }() + + // Wait for stdin to close (Ctrl+D) + err := <-writeDone + if err != nil && err != io.EOF { + // Check if it's a broken pipe error (expected when stdin closes) + if !errors.Is(err, syscall.EPIPE) { + return err + } + } + + // Close the write end to signal EOF to remote peer + // This will trigger the echo server to respond and then close + stream.CloseWrite() + + // Wait for the remote peer to finish processing and send response + // The echo server should now process the input and send back the echoed text + select { + case <-ctx.Done(): + return ctx.Err() + case err := <-readDone: + // Check if this is a graceful closure (EOF) or an error + if err == nil || errors.Is(err, io.EOF) { + // Graceful closure - this is expected + return nil + } + // Any other error should be reported + return err + } +} diff --git a/cmd/ww/main.go b/cmd/ww/main.go index d70f3b0..9f6b94d 100644 --- a/cmd/ww/main.go +++ b/cmd/ww/main.go @@ -10,12 +10,11 @@ import ( "github.com/lmittmann/tint" "github.com/urfave/cli/v2" - "github.com/wetware/go/cmd/ww/args" + "github.com/wetware/go/cmd/ww/cat" "github.com/wetware/go/cmd/ww/export" "github.com/wetware/go/cmd/ww/idgen" importcmd "github.com/wetware/go/cmd/ww/import" "github.com/wetware/go/cmd/ww/run" - "github.com/wetware/go/cmd/ww/shell" ) func main() { @@ -47,18 +46,15 @@ func main() { }, }, Commands: []*cli.Command{ + cat.Command(), idgen.Command(), run.Command(), - shell.Command(), export.Command(), importcmd.Command(), }, } - hostArgs, guestArgs := args.SplitArgs(os.Args) - ctx = context.WithValue(ctx, args.GuestArgs, guestArgs) - - err := app.RunContext(ctx, hostArgs) + err := app.RunContext(ctx, os.Args) if err != nil { slog.ErrorContext(ctx, err.Error()) os.Exit(1) diff --git a/cmd/ww/run/README.md b/cmd/ww/run/README.md deleted file mode 100644 index b6399f6..0000000 --- a/cmd/ww/run/README.md +++ /dev/null @@ -1,135 +0,0 @@ -# ww run - -Execute binaries in a jailed subprocess with file descriptor passing support. - -## Usage - -```bash -ww run [flags] [args...] -``` - -## File Descriptor Passing - -Use `--with-fd` to pass file descriptors to the child process: - -```bash -# Single FD -ww run --with-fd db=3 ./app - -# Multiple FDs -ww run --with-fd db=3 --with-fd cache=4 ./app - -# Different ordering (same result) -ww run --with-fd cache=4 --with-fd db=3 ./app -``` - -### FD Layout - -The child process receives file descriptors in this order: - -- **FD 3**: RPC socket for host communication (always present) -- **FD 4**: First `--with-fd` mapping -- **FD 5**: Second `--with-fd` mapping -- **FD 6**: Third `--with-fd` mapping -- And so on... - -### Environment Variables - -The child receives environment variables mapping names to FD numbers: - -```bash -WW_FD_DB=4 # "db" available at FD 4 -WW_FD_CACHE=5 # "cache" available at FD 5 -``` - -### Child Process Usage - -```go -package main - -import ( - "fmt" - "os" - - "github.com/wetware/go/util" -) - -func main() { - // Get all available file descriptors - fdMap := util.GetFDMap() - - // Check for specific FD - if dbFD, exists := fdMap["db"]; exists { - dbFile := os.NewFile(uintptr(dbFD), "database") - defer dbFile.Close() - // Use dbFile for database operations - fmt.Printf("Using database at FD %d\n", dbFD) - } else { - fmt.Fprintf(os.Stderr, "Database FD not provided\n") - os.Exit(1) - } - - // Or iterate through all available FDs - for name, fd := range fdMap { - fmt.Printf("Available: %s -> FD %d\n", name, fd) - } -} -``` - -## Flags - -- `--with-fd name=fdnum`: Map parent FD to child with semantic name -- `--env key=value`: Set environment variable for child process -- `--ipfs addr`: IPFS API endpoint (default: `/dns4/localhost/tcp/5001/http`) - -## Implementation - -The `--with-fd` system: - -1. **Parses** `--with-fd` flags into name/fd pairs -2. **Validates** names (no duplicates) and FD numbers (non-negative) -3. **Duplicates** source FDs using `syscall.Dup()` to avoid conflicts -4. **Assigns** target FDs sequentially starting at 4 -5. **Generates** environment variables for child process -6. **Passes** FDs via `cmd.ExtraFiles` -7. **Cleans up** resources when done - -## Examples - -### Basic Usage -```bash -# Run local binary -ww run ./myapp - -# Run IPFS binary -ww run /ipfs/QmHash.../myapp - -# With environment variables -ww run --env DEBUG=1 --env LOG_LEVEL=info ./myapp -``` - -### File Descriptor Examples -```bash -# Pass database socket -ww run --with-fd db=3 ./database-app - -# Pass multiple resources -ww run --with-fd db=3 --with-fd cache=4 --with-fd logs=5 ./full-app - -# Combine with environment -ww run --with-fd db=3 --env DB_TIMEOUT=30s ./db-app -``` - -## Testing - -```bash -# Run tests -go test ./cmd/ww/run/... - -# Build -go build ./cmd/ww/... -``` - -## Demo - -See `examples/fd-demo/` for a working example of file descriptor usage. diff --git a/cmd/ww/run/env.go b/cmd/ww/run/env.go index 0c828eb..74f2674 100644 --- a/cmd/ww/run/env.go +++ b/cmd/ww/run/env.go @@ -3,20 +3,15 @@ package run import ( "context" "fmt" - "log/slog" "os" "path/filepath" "runtime" "strings" - "time" + "github.com/ipfs/boxo/files" "github.com/ipfs/boxo/path" - iface "github.com/ipfs/kubo/core/coreiface" - "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p-kad-dht/dual" "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/peerstore" - "github.com/libp2p/go-libp2p/p2p/discovery/mdns" "github.com/wetware/go/util" "go.uber.org/multierr" ) @@ -35,11 +30,9 @@ func ExpandHome(path string) (string, error) { type EnvConfig struct { IPFS string Port int - NS string - MDNS bool } -func (cfg EnvConfig) New() (env Env, err error) { +func (cfg EnvConfig) New(ctx context.Context) (env Env, err error) { env.Dir, err = os.MkdirTemp("", "cell-*") if err != nil { err = fmt.Errorf("failed to create temp directory: %w", err) @@ -55,24 +48,19 @@ func (cfg EnvConfig) New() (env Env, err error) { // Initialize libp2p host //// - env.Host, err = HostConfig{ - IPFS: env.IPFS, - Port: cfg.Port, - }.New() + env.Host, err = util.NewServer(cfg.Port) if err != nil { err = fmt.Errorf("failed to create libp2p host: %w", err) return } - // Initialize mDNS discovery service if enabled - if cfg.MDNS { - env.MDNS = mdns.NewMdnsService(env.Host, env.NS, &MDNSPeerHandler{ - Peerstore: env.Host.Peerstore(), - TTL: peerstore.AddressTTL, - }) - env.MDNS.Start() - slog.Info("mDNS discovery service started") + // Create and bootstrap DHT client + env.DHT, err = env.NewDHT(ctx, env.Host) + if err != nil { + err = fmt.Errorf("failed to create DHT client: %w", err) + return } + return } @@ -81,16 +69,16 @@ type Env struct { Host host.Host NS string Dir string // Temporary directory for cell execution - MDNS mdns.Service + DHT *dual.DHT } func (env *Env) Close() error { var errors []error - // Close mDNS service - if env.MDNS != nil { - if err := env.MDNS.Close(); err != nil { - errors = append(errors, fmt.Errorf("failed to close mDNS service: %w", err)) + // Close DHT client + if env.DHT != nil { + if err := env.DHT.Close(); err != nil { + errors = append(errors, fmt.Errorf("failed to close DHT client: %w", err)) } } @@ -165,60 +153,23 @@ func (env *Env) Arch() string { return runtime.GOARCH } -type HostConfig struct { - NS string - IPFS iface.CoreAPI - Options []libp2p.Option - Port int -} - -type HostWithDiscovery struct { - host.Host - MDNS mdns.Service -} - -// Start starts the mDNS discovery service -func (h *HostWithDiscovery) Start() { - if h.MDNS != nil { - h.MDNS.Start() +// LoadIPFSFile resolves an IPFS path to WASM bytecode +func (env *Env) LoadIPFSFile(ctx context.Context, p path.Path) (files.File, error) { + if env.IPFS == nil { + return nil, fmt.Errorf("IPFS environment not initialized") } -} -// Close stops the mDNS discovery service and closes the host -func (h *HostWithDiscovery) Close() error { - if h.MDNS != nil { - h.MDNS.Close() + // Get the file from IPFS + node, err := env.IPFS.Unixfs().Get(ctx, p) + if err != nil { + return nil, fmt.Errorf("failed to get from IPFS: %w", err) } - return h.Host.Close() -} - -func (cfg HostConfig) New() (host.Host, error) { - return libp2p.New(cfg.CombinedOptions()...) -} - -func (cfg HostConfig) CombinedOptions() []libp2p.Option { - return append(cfg.DefaultOptions(), cfg.Options...) -} -func (c HostConfig) DefaultOptions() []libp2p.Option { - return []libp2p.Option{ - libp2p.ListenAddrStrings(fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", c.Port)), + // Read the file content + file, ok := node.(files.File) + if !ok { + return nil, fmt.Errorf("IPFS path does not point to a file") } -} - -// mdnsNotifee implements the mdns.Notifee interface -type MDNSPeerHandler struct { - peerstore.Peerstore - TTL time.Duration -} -func (m MDNSPeerHandler) HandlePeerFound(pi peer.AddrInfo) { - if m.TTL < 0 { - m.TTL = peerstore.AddressTTL - } - slog.Info("mDNS discovered peer", - "peer_id", pi.ID, - "addrs", pi.Addrs, - "ttl", m.TTL) - m.Peerstore.AddAddrs(pi.ID, pi.Addrs, peerstore.AddressTTL) + return file, nil } diff --git a/cmd/ww/run/fd.go b/cmd/ww/run/fd.go deleted file mode 100644 index 5b47364..0000000 --- a/cmd/ww/run/fd.go +++ /dev/null @@ -1,129 +0,0 @@ -package run - -import ( - "fmt" - "os" - "strconv" - "strings" - "syscall" -) - -// FDManager handles file descriptor mapping for child processes -type FDManager struct { - mappings []fdMapping -} - -type fdMapping struct { - name string - sourceFD int - targetFD int -} - -// ParseFDFlag parses a --with-fd flag value in "name=fdnum" format -func ParseFDFlag(value string) (string, int, error) { - parts := strings.SplitN(value, "=", 2) - if len(parts) != 2 { - return "", 0, fmt.Errorf("invalid format: expected 'name=fdnum', got '%s'", value) - } - - name := parts[0] - if name == "" { - return "", 0, fmt.Errorf("name cannot be empty") - } - - fdnum, err := strconv.Atoi(parts[1]) - if err != nil { - return "", 0, fmt.Errorf("invalid fd number '%s': %v", parts[1], err) - } - - if fdnum < 0 { - return "", 0, fmt.Errorf("fd number must be non-negative, got %d", fdnum) - } - - return name, fdnum, nil -} - -// NewFDManager creates a new FD manager from a list of --with-fd flag values -func NewFDManager(fdFlags []string) (*FDManager, error) { - fm := &FDManager{} - - // Track used names to prevent duplicates - usedNames := make(map[string]bool) - - for i, flag := range fdFlags { - name, sourceFD, err := ParseFDFlag(flag) - if err != nil { - return nil, fmt.Errorf("flag %d '%s': %w", i+1, flag, err) - } - - if usedNames[name] { - return nil, fmt.Errorf("duplicate name '%s' in --with-fd flags", name) - } - - // Target FD starts at 3 and increments sequentially - targetFD := 3 + i - - fm.mappings = append(fm.mappings, fdMapping{ - name: name, - sourceFD: sourceFD, - targetFD: targetFD, - }) - - usedNames[name] = true - } - - return fm, nil -} - -// PrepareFDs duplicates source file descriptors and returns them for ExtraFiles -func (fm *FDManager) PrepareFDs() ([]*os.File, error) { - var files []*os.File - - for _, mapping := range fm.mappings { - // Duplicate the source FD to avoid conflicts - newFD, err := syscall.Dup(mapping.sourceFD) - if err != nil { - return nil, fmt.Errorf("failed to duplicate fd %d for '%s': %w", mapping.sourceFD, mapping.name, err) - } - - // Create os.File from the duplicated FD - file := os.NewFile(uintptr(newFD), mapping.name) - if file == nil { - syscall.Close(newFD) - return nil, fmt.Errorf("failed to create os.File for fd %d", newFD) - } - - files = append(files, file) - } - - return files, nil -} - -// GenerateEnvVars creates environment variables for the child process -func (fm *FDManager) GenerateEnvVars() []string { - var envVars []string - - for _, mapping := range fm.mappings { - envVar := fmt.Sprintf("WW_FD_%s=%d", strings.ToUpper(mapping.name), mapping.targetFD) - envVars = append(envVars, envVar) - } - - return envVars -} - -// Close cleans up all managed file descriptors -func (fm *FDManager) Close() error { - var errors []string - - for _, mapping := range fm.mappings { - if err := syscall.Close(mapping.sourceFD); err != nil { - errors = append(errors, fmt.Sprintf("failed to close fd %d: %v", mapping.sourceFD, err)) - } - } - - if len(errors) > 0 { - return fmt.Errorf("errors closing FDs: %s", strings.Join(errors, "; ")) - } - - return nil -} diff --git a/cmd/ww/run/fd_test.go b/cmd/ww/run/fd_test.go deleted file mode 100644 index be754b9..0000000 --- a/cmd/ww/run/fd_test.go +++ /dev/null @@ -1,125 +0,0 @@ -package run - -import ( - "fmt" - "os" - "reflect" - "testing" -) - -func TestParseFDFlag(t *testing.T) { - tests := []struct { - input string - wantName string - wantFD int - wantErr bool - }{ - {"db=3", "db", 3, false}, - {"cache=4", "cache", 4, false}, - {"input=0", "input", 0, false}, - {"invalid", "", 0, true}, - {"=3", "", 0, true}, - {"db=", "", 0, true}, - {"db=-1", "", 0, true}, - {"db=abc", "", 0, true}, - } - - for _, tt := range tests { - t.Run(tt.input, func(t *testing.T) { - name, fd, err := ParseFDFlag(tt.input) - - if tt.wantErr { - if err == nil { - t.Errorf("ParseFDFlag() expected error, got nil") - } - return - } - - if err != nil { - t.Errorf("ParseFDFlag() unexpected error: %v", err) - return - } - - if name != tt.wantName { - t.Errorf("ParseFDFlag() name = %v, want %v", name, tt.wantName) - } - - if fd != tt.wantFD { - t.Errorf("ParseFDFlag() fd = %v, want %v", fd, tt.wantFD) - } - }) - } -} - -func TestNewFDManager(t *testing.T) { - tests := []struct { - name string - flags []string - wantErr bool - }{ - {"valid single", []string{"db=3"}, false}, - {"valid multiple", []string{"db=3", "cache=4"}, false}, - {"duplicate names", []string{"db=3", "db=4"}, true}, - {"invalid format", []string{"invalid"}, true}, - {"empty list", []string{}, false}, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - fm, err := NewFDManager(tt.flags) - - if tt.wantErr { - if err == nil { - t.Errorf("NewFDManager() expected error, got nil") - } - return - } - - if err != nil { - t.Errorf("NewFDManager() unexpected error: %v", err) - return - } - - if fm == nil { - t.Errorf("NewFDManager() returned nil manager") - } - }) - } -} - -func TestFDManager_GenerateEnvVars(t *testing.T) { - fm, err := NewFDManager([]string{"db=3", "cache=4"}) - if err != nil { - t.Fatalf("Failed to create FDManager: %v", err) - } - - envVars := fm.GenerateEnvVars() - expected := []string{"WW_FD_DB=3", "WW_FD_CACHE=4"} - - if !reflect.DeepEqual(envVars, expected) { - t.Errorf("GenerateEnvVars() = %v, want %v", envVars, expected) - } -} - -func TestFDManager_Close(t *testing.T) { - // Create a temporary file to get a valid FD - tmpFile, err := os.CreateTemp("", "fd-test") - if err != nil { - t.Fatalf("Failed to create temp file: %v", err) - } - defer os.Remove(tmpFile.Name()) - defer tmpFile.Close() - - // Get the FD number - fd := int(tmpFile.Fd()) - - fm, err := NewFDManager([]string{fmt.Sprintf("test=%d", fd)}) - if err != nil { - t.Fatalf("Failed to create FDManager: %v", err) - } - - // Close should not error - if err := fm.Close(); err != nil { - t.Errorf("Close() unexpected error: %v", err) - } -} diff --git a/cmd/ww/run/run.go b/cmd/ww/run/run.go index 6b1434b..2852c1e 100644 --- a/cmd/ww/run/run.go +++ b/cmd/ww/run/run.go @@ -2,24 +2,27 @@ package run import ( "context" + "errors" "fmt" + "io" "log/slog" "os" - os_exec "os/exec" + "os/exec" + "path/filepath" - "capnproto.org/go/capnp/v3/rpc" + "github.com/ipfs/boxo/path" + "github.com/libp2p/go-libp2p/core/event" "github.com/libp2p/go-libp2p/core/network" + "github.com/libp2p/go-libp2p/core/peer" "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/experimental/sys" "github.com/urfave/cli/v2" "github.com/wetware/go/cmd/internal/flags" - "github.com/wetware/go/cmd/ww/args" "github.com/wetware/go/system" ) var env Env -const protocol = "/ww/0.1.0" - func Command() *cli.Command { return &cli.Command{ // ww run [args...] @@ -30,11 +33,7 @@ func Command() *cli.Command { &cli.StringFlag{ Name: "ipfs", EnvVars: []string{"WW_IPFS"}, - Value: "/dns4/localhost/tcp/5001/http", - }, - &cli.StringSliceFlag{ - Name: "env", - EnvVars: []string{"WW_ENV"}, + Value: "/ip4/127.0.0.1/tcp/5001/http", }, &cli.IntFlag{ Name: "port", @@ -42,27 +41,25 @@ func Command() *cli.Command { EnvVars: []string{"WW_PORT"}, Value: 2020, }, - &cli.StringSliceFlag{ - Name: "with-fd", - Category: "FILE DESCRIPTORS", - Usage: "map existing parent fd to name (e.g., db=3). Use --with-fd multiple times for multiple fds.", - }, &cli.BoolFlag{ Name: "wasm-debug", Usage: "enable wasm debug info", EnvVars: []string{"WW_WASM_DEBUG"}, }, + &cli.BoolFlag{ + Name: "async", + Usage: "run in async mode for stream processing", + EnvVars: []string{"WW_ASYNC"}, + }, }, flags.CapabilityFlags()...), // Environment hooks. //// Before: func(c *cli.Context) (err error) { env, err = EnvConfig{ - NS: c.String("ns"), IPFS: c.String("ipfs"), Port: c.Int("port"), - MDNS: c.Bool("with-mdns") || c.Bool("with-all"), - }.New() + }.New(c.Context) return }, After: func(c *cli.Context) error { @@ -79,100 +76,173 @@ func Main(c *cli.Context) error { ctx, cancel := context.WithCancel(c.Context) defer cancel() - // Set up the RPC socket for the cell - //// - host, guest, err := system.SocketConfig[system.Terminal]{ - BootstrapClient: system.TerminalConfig{ - Exec: exec(c), - }.New(), - }.New(ctx) - if err != nil { - return err + // Get the binary path from arguments + binaryPath := c.Args().First() + if binaryPath == "" { + return fmt.Errorf("no binary specified") } - defer host.Close() - defer guest.Close() - // Check if first arg is an IPFS path and prepare name for CommandContext - name, err := env.ResolveExecPath(ctx, c.Args().First()) + // Resolve the binary path to WASM bytecode + f, err := resolveBinary(ctx, binaryPath) if err != nil { + return fmt.Errorf("failed to resolve binary %s: %w", binaryPath, err) + } + defer f.Close() + + // Create wazero runtime + runtime := wazero.NewRuntimeWithConfig(ctx, wazero.NewRuntimeConfig(). + WithDebugInfoEnabled(c.Bool("wasm-debug")). + WithCloseOnContextDone(true)) + defer runtime.Close(ctx) + + p, err := system.ProcConfig{ + Host: env.Host, + Runtime: runtime, + Src: f, + Env: c.StringSlice("env"), + Args: c.Args().Slice(), + ErrWriter: c.App.ErrWriter, + Async: c.Bool("async"), + }.New(ctx) + if err != nil && !errors.Is(err, sys.Errno(0)) { return err } + defer p.Close(ctx) - // Fetch or create the arguments for the guest process. - guestArgs, ok := c.Context.Value(args.GuestArgs).([]string) - if !ok { - guestArgs = []string{} + if !p.Config.Async { + return nil } - // Set up file descriptor management - //// - fdManager, err := NewFDManager(c.StringSlice("with-fd")) + sub, err := env.Host.EventBus().Subscribe([]any{ + new(event.EvtPeerIdentificationCompleted), + new(event.EvtPeerIdentificationFailed), + new(event.EvtAutoRelayAddrsUpdated), + new(event.EvtHostReachableAddrsChanged), + new(event.EvtLocalReachabilityChanged), + new(event.EvtPeerProtocolsUpdated), + new(event.EvtLocalProtocolsUpdated), + new(event.EvtPeerConnectednessChanged), + new(event.EvtNATDeviceTypeChanged), + new(event.EvtLocalAddressesUpdated)}) if err != nil { - return fmt.Errorf("file descriptor setup failed: %w", err) + return fmt.Errorf("failed to subscribe to event loop: %w", err) } - defer fdManager.Close() - - // Run target in jailed subprocess - //// - cmd := os_exec.CommandContext(ctx, name, guestArgs...) - cmd.Dir = env.Dir - - // Combine environment variables: base env + --env flags + FD mappings - baseEnv := c.StringSlice("env") - fdEnvVars := fdManager.GenerateEnvVars() - cmd.Env = append(baseEnv, fdEnvVars...) - - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.SysProcAttr = sysProcAttr(env.Dir) - - // Set up ExtraFiles: RPC socket first, then user FDs - extraFiles := []*os.File{guest} - userFiles, err := fdManager.PrepareFDs() - if err != nil { - return err - } - cmd.ExtraFiles = append(extraFiles, userFiles...) + defer sub.Close() + + // Log connection information for async mode + slog.InfoContext(ctx, "process started in async mode", + "peer", env.Host.ID(), + "endpoint", p.Endpoint.Name) + + env.Host.SetStreamHandler(p.Endpoint.Protocol(), func(s network.Stream) { + defer s.CloseRead() + slog.InfoContext(ctx, "stream connected", + "peer", s.Conn().RemotePeer(), + "stream-id", s.ID(), + "endpoint", p.Endpoint.Name) + if err := p.Poll(ctx, s, nil); err != nil { + slog.ErrorContext(ctx, "failed to poll process", + "id", p.ID(), + "stream", s.ID(), + "reason", err) + } + }) + defer env.Host.RemoveStreamHandler(p.Endpoint.Protocol()) - if err := cmd.Start(); err != nil { - return err + for { + select { + case <-ctx.Done(): + return ctx.Err() + case v := <-sub.Out(): + switch ev := v.(type) { + case event.EvtNATDeviceTypeChanged: + slog.InfoContext(ctx, "NAT device type changed", + "device type", ev.NatDeviceType) + case event.EvtLocalReachabilityChanged: + slog.InfoContext(ctx, "local reachability changed", + "reachability", ev.Reachability) + case event.EvtHostReachableAddrsChanged: + slog.DebugContext(ctx, "host reachable addresses changed", + "reachable", ev.Reachable, + "unreachable", ev.Unreachable, + "unknown", ev.Unknown) + case event.EvtLocalAddressesUpdated: + r, err := ev.SignedPeerRecord.Record() + if err != nil { + return err + } + signer, err := peer.IDFromPublicKey(ev.SignedPeerRecord.PublicKey) + if err != nil { + return err + } + slog.DebugContext(ctx, "local addresses updated", + "current", ev.Current, + "diffs", ev.Diffs, + "peer", r.(*peer.PeerRecord).PeerID, + "addrs", r.(*peer.PeerRecord).Addrs, + "seq", r.(*peer.PeerRecord).Seq, + "signer", signer) + case event.EvtPeerIdentificationCompleted: + slog.DebugContext(ctx, "peer identification completed", + "peer", ev.Peer, + "agent-version", ev.AgentVersion, + "protocol-version", ev.ProtocolVersion, + "protocols", ev.Protocols) + case event.EvtPeerIdentificationFailed: + slog.WarnContext(ctx, "peer identification failed", + "peer", ev.Peer, + "reason", ev.Reason) + case event.EvtAutoRelayAddrsUpdated: + slog.DebugContext(ctx, "auto relay addresses updated", + "addresses", ev.RelayAddrs) + case event.EvtPeerProtocolsUpdated: + slog.DebugContext(ctx, "peer protocols updated", + "peer", ev.Peer, + "added", ev.Added, + "removed", ev.Removed) + case event.EvtLocalProtocolsUpdated: + slog.DebugContext(ctx, "local protocols updated", + "added", ev.Added, + "removed", ev.Removed) + case event.EvtPeerConnectednessChanged: + slog.DebugContext(ctx, "peer connectedness changed", + "peer", ev.Peer, + "connectedness", ev.Connectedness) + + default: + panic(v) // unhandled event + } + } } - defer cmd.Cancel() +} - // Set up libp2p protocol handler - //// - env.Host.SetStreamHandler(protocol, func(s network.Stream) { - defer s.Close() +// resolveBinary resolves a binary path to WASM bytecode +func resolveBinary(ctx context.Context, name string) (io.ReadCloser, error) { + // Parse the IPFS path + ipfsPath, err := path.NewPath(name) + if err == nil { + return env.LoadIPFSFile(ctx, ipfsPath) + } - conn := rpc.NewConn(rpc.NewPackedStreamTransport(s), &rpc.Options{ - BaseContext: func() context.Context { return ctx }, - BootstrapClient: host.Bootstrap(ctx), // cell-provided capability - }) - defer conn.Close() + // Check if it's an absolute path + if filepath.IsAbs(name) { + return os.Open(name) + } - select { - case <-ctx.Done(): - case <-conn.Done(): - } - }) - defer env.Host.RemoveStreamHandler(protocol) + // Check if it's a relative path (starts with . or /) + if len(name) > 0 && (name[0] == '.' || name[0] == '/') { + return os.Open(name) + } - return cmd.Wait() -} + // Check if it's in $PATH + if resolvedPath, err := exec.LookPath(name); err == nil { + return os.Open(resolvedPath) + } -func exec(c *cli.Context) system.Executor { - if !c.Bool("with-exec") && !c.Bool("with-all") { - slog.DebugContext(c.Context, "returning null executor client", - "--with-exec", c.Bool("with-exec"), - "--with-all", c.Bool("with-all")) - return system.Executor{} // null client + // Try as a relative path in current directory + if _, err := os.Stat(name); err == nil { + return os.Open(name) } - return system.ExecutorConfig{ - Host: env.Host, - Runtime: wazero.NewRuntimeConfig(). - WithDebugInfoEnabled(c.Bool("wasm-debug")). - WithCloseOnContextDone(true), - }.New(c.Context) + return nil, fmt.Errorf("binary not found: %s", name) } diff --git a/cmd/ww/run/run_darwin.go b/cmd/ww/run/run_darwin.go deleted file mode 100644 index 1c74cd8..0000000 --- a/cmd/ww/run/run_darwin.go +++ /dev/null @@ -1,29 +0,0 @@ -//go:build darwin - -package run - -import "syscall" - -func sysProcAttr(chroot string) *syscall.SysProcAttr { - return nil - // // Note: on macOS, SysProcAttr only provides chroot, credential drop, - // // session/PGID control and optional ptrace-based parent–death. It - // // does NOT support namespace unsharing (PID, mount, network, IPC) or - // // a pdeath signal as on Linux, so isolation here is inherently weaker - // // unless you layer on Apple’s sandbox or run the process in a VM/container. - - // return &syscall.SysProcAttr{ - // // Drop privileges to “nobody:nogroup”, so that even if - // // we're running as root, the child isn’t. - // Credential: &syscall.Credential{Uid: 65534, Gid: 65534}, - - // // Jail to the chroot directory. Note that this SHOULD - // // be combined with a mount namespace (e.g. CLONE_NEWNS). - // Chroot: chroot, - - // Setsid: true, // new session - // Setpgid: true, // new process group - // Pgid: 0, // child is its own group leader - // Noctty: true, // detach from any controlling TTY - // } -} diff --git a/cmd/ww/run/run_unix.go b/cmd/ww/run/run_unix.go deleted file mode 100644 index 91868a7..0000000 --- a/cmd/ww/run/run_unix.go +++ /dev/null @@ -1,33 +0,0 @@ -//go:build unix && !darwin - -package run - -import "syscall" - -func sysProcAttr(chroot string) *syscall.SysProcAttr { - return nil - // return &syscall.SysProcAttr{ - // // Drop privileges to “nobody:nogroup”, so that even if - // // we're running as root, the child isn’t. - // Credential: &syscall.Credential{Uid: 65534, Gid: 65534}, - - // // If the parent dies, kill the child. - // Pdeathsig: syscall.SIGKILL, - - // // Jail to the chroot directory. Note that this SHOULD - // // be combined with a mount namespace (e.g. CLONE_NEWNS). - // Chroot: chroot, - - // // Unshare into brand‑new namespaces: - // // - PID ⇒ child is pid 1 - // // - UTS ⇒ no shared hostname - // // - MOUNT ⇒ private mount table - // // - NET ⇒ no inherited network interfaces - // // - IPC ⇒ no SysV message queues/semaphores - // Cloneflags: syscall.CLONE_NEWUTS | - // syscall.CLONE_NEWPID | - // syscall.CLONE_NEWNS | - // syscall.CLONE_NEWNET | - // syscall.CLONE_NEWIPC, - // } -} diff --git a/cmd/ww/shell/README.md b/cmd/ww/shell/README.md deleted file mode 100644 index 5fa2953..0000000 --- a/cmd/ww/shell/README.md +++ /dev/null @@ -1,252 +0,0 @@ -# Wetware Shell - -Production-grade REPL shell built with the Wetware framework and Slurp LISP toolkit. The shell operates in two modes: host mode (spawns a guest process) and guest mode (runs as a cell process). - -## Features - -- **Dual Mode Operation**: Host mode spawns guest processes, guest mode runs as cell processes -- **REPL**: Built using `github.com/spy16/slurp` for LISP interpretation -- **Readline**: Uses `github.com/chzyer/readline` for terminal experience -- **Wetware Integration**: Runs within Wetware cell environment with system capabilities -- **CLI Integration**: Full urfave/cli integration with configurable options -- **Function Registry**: Extensible set of built-in functions - -## Available Functions - -### Basic Values -- `nil` - null value -- `true` / `false` - boolean values -- `version` - wetware version string - -### Arithmetic -- `(+ a b ...)` - sum of numbers -- `(* a b ...)` - product of numbers -- `(/ a b)` - division (returns float) - -### Comparison -- `(= a b)` - equality comparison -- `(> a b)` - greater than -- `(< a b)` - less than - -### Utilities -- `help` - display help message -- `println expr` - print expression with newline -- `print expr` - print expression without newline -- `(send "peer-addr-or-id" "proc-id" data)` - send data to a peer process - -### IPFS Functions (requires `--with-ipfs` or `--with-all`) -- `(ipfs :cat /ipfs/QmHash/...)` - read IPFS file content as bytes -- `(ipfs :get /ipfs/QmHash/...)` - get IPFS node as file/directory object -- IPFS Path Syntax: `/ipfs/QmHash/...` and `/ipns/domain/...` are automatically parsed - -### Execution Functions (requires `--with-exec` or `--with-all`) -- `(exec /ipfs/QmHash/bytecode :timeout 15s)` - execute bytecode from IPFS path - -## Usage - -The shell is integrated into the main `ww` command: - -```bash -# Interactive shell (host mode - spawns guest process) -ww shell - -# Execute single command -ww shell -c "(+ 1 2 3)" - -# Customize prompt and history -ww shell --prompt "my-shell> " --history-file ~/.ww_history - -# Disable banner -ww shell --no-banner - -# Environment variable configuration -WW_SHELL_PROMPT="ww> " WW_SHELL_HISTORY="/tmp/ww.tmp" ww shell -``` - -### Command Line Options - -- `-c, --command`: Execute a single command and exit -- `--history-file`: Path to readline history file (default: `/tmp/ww-shell.tmp`) -- `--prompt`: Shell prompt string (default: `"ww> "`) -- `--no-banner`: Disable welcome banner -- `--ipfs`: IPFS API endpoint (default: `/dns4/localhost/tcp/5001/http`) - -### Capability Flags - -- `--with-ipfs`: Grant IPFS capability (enables `ipfs` functions and path syntax) -- `--with-exec`: Grant process execution capability (enables `exec` function) -- `--with-console`: Grant console output capability -- `--with-all`: Grant all capabilities (equivalent to all above flags) - -### Environment Variables - -- `WW_SHELL_HISTORY`: Override history file path -- `WW_SHELL_PROMPT`: Override prompt string -- `WW_SHELL_NO_BANNER`: Disable banner (set to any value) -- `WW_IPFS`: Override IPFS API endpoint -- `WW_WITH_IPFS`: Enable IPFS capability -- `WW_WITH_EXEC`: Enable execution capability -- `WW_WITH_CONSOLE`: Enable console capability -- `WW_WITH_ALL`: Enable all capabilities - -## Example Session - -### Basic Usage -``` -Welcome to Wetware Shell! Type 'help' for available commands. -ww> help -Wetware Shell - Available commands: - help - Show this help message - version - Show wetware version - (+ a b ...) - Sum numbers - (* a b ...) - Multiply numbers - (= a b) - Compare equality - (> a b) - Greater than - (< a b) - Less than - (println expr) - Print expression with newline - (print expr) - Print expression without newline - (send "peer-addr-or-id" "proc-id" data) - Send data to a peer process - -ww> (+ 1 2 3 4) -10 -ww> (* 2 3 4) -24 -ww> (> 10 5) -true -ww> (println "Hello, Wetware!") -Hello, Wetware! -ww> -``` - -### With IPFS Capability -```bash -# Start shell with IPFS capability -ww shell --with-ipfs -``` - -``` -ww> /ipfs/QmHash/example.txt -Path: /ipfs/QmHash/example.txt -ww> (ipfs :cat /ipfs/QmHash/example.txt) -# Returns file content as bytes -ww> (ipfs :get /ipfs/QmHash/example.txt) - -ww> (ipfs :get /ipfs/QmHash/example.txt :read-string) -"Hello from IPFS!" -ww> -``` - -### With Execution Capability -```bash -# Start shell with execution capability -ww shell --with-exec -``` - -``` -ww> (exec /ipfs/QmHash/bytecode.wasm :timeout 30s) -abc123def -ww> -``` - -### With All Capabilities -```bash -# Start shell with all capabilities -ww shell --with-all -``` - -``` -ww> help -# Shows all available functions including ipfs and exec -ww> (send "12D3KooW..." "my-process" "Hello, peer!") -# Sends data to peer process -ww> -``` - -## Architecture - -The shell operates in two distinct modes with capability-based function loading: - -### Host Mode -- **Detection**: Runs when `WW_CELL` environment variable is not set -- **Process Spawning**: Uses `ww run -env WW_CELL=true ww -- shell` to spawn guest process -- **Flag Forwarding**: Passes all shell-specific flags and capability flags to the guest process -- **Stdio Forwarding**: Forwards stdin, stdout, and stderr to guest process -- **IPFS Environment**: Initializes IPFS environment before spawning guest process - -### Guest Mode (Cell Process) -- **Detection**: Runs when `WW_CELL=true` environment variable is set -- **RPC Connection**: Uses file descriptor 3 for RPC communication with host -- **Wetware Integration**: Connects to Wetware cell system for capabilities -- **Slurp Interpreter**: Provides LISP evaluation engine with IPFS path support -- **Custom REPL**: Production-grade read-eval-print loop with error handling -- **Readline Integration**: Enhanced terminal input with history and completion -- **Function Registry**: Capability-based function loading (base + session-specific) -- **IPFS Integration**: Direct IPFS API access when `--with-ipfs` is enabled -- **Execution Support**: Process execution capability when `--with-exec` is enabled - -### Capability System -- **Base Globals**: Always available (arithmetic, comparison, utilities, send) -- **IPFS Capability**: Loads `ipfs` object and enables IPFS path syntax when `--with-ipfs` is set -- **Execution Capability**: Loads `exec` function when `--with-exec` is set -- **Console Capability**: Enables console output when `--with-console` is set -- **All Capabilities**: `--with-all` enables all capabilities at once - -### Process Flow -1. `ww shell` (host) → `ww run` (process isolation) → `ww shell` (guest/cell) -2. Host mode initializes IPFS environment and delegates to `ww run` for proper file descriptor management -3. Guest mode runs as a cell process with capability-based function loading -4. Functions are loaded based on capability flags passed from host mode - -## Extending - -### Adding Base Functions -To add functions that are always available, modify the `globals` map in `globals.go`: - -```go -"my-function": slurp.Func("my-function", func(args ...core.Any) { - // Implementation - return result -}), -``` - -### Adding Capability-Based Functions -To add functions that require specific capabilities, modify the `getBaseGlobals()` function in `shell.go`: - -```go -// Add IPFS capability function -if c.Bool("with-ipfs") || c.Bool("with-all") { - gs["my-ipfs-function"] = &MyIPFSFunction{CoreAPI: env.IPFS} -} - -// Add execution capability function -if c.Bool("with-exec") || c.Bool("with-all") { - gs["my-exec-function"] = &MyExecFunction{Session: session} -} -``` - -### Adding CLI Flags -To add new CLI flags, modify the `Command()` function in `shell.go`: - -```go -&cli.StringFlag{ - Name: "my-flag", - Usage: "description of my flag", - EnvVars: []string{"WW_MY_FLAG"}, -}, -``` - -### Adding New Capabilities -To add a new capability system: - -1. Add the capability flag to `flags.go` -2. Add capability check in `getBaseGlobals()` or `NewSessionGlobals()` -3. Implement capability-specific functions -4. Update help message and documentation - -## Dependencies - -- `github.com/spy16/slurp` - LISP toolkit -- `github.com/chzyer/readline` - Terminal readline support -- `github.com/urfave/cli/v2` - CLI framework -- `capnproto.org/go/capnp/v3` - Cap'n Proto RPC -- `github.com/wetware/go` - Wetware framework diff --git a/cmd/ww/shell/executor.go b/cmd/ww/shell/executor.go deleted file mode 100644 index 1b10856..0000000 --- a/cmd/ww/shell/executor.go +++ /dev/null @@ -1,102 +0,0 @@ -package shell - -import ( - "context" - "errors" - "fmt" - "io" - "time" - - "github.com/ipfs/boxo/files" - "github.com/ipfs/boxo/path" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" - "github.com/wetware/go/system" -) - -var _ core.Invokable = (*Exec)(nil) - -type Exec struct { - Session interface { - Exec() system.Executor - } -} - -// (exec -// :timeout 15s) -func (e Exec) Invoke(args ...core.Any) (core.Any, error) { - if len(args) == 0 { - return nil, fmt.Errorf("exec requires at least one argument (bytecode or reader)") - } - - p, ok := args[0].(path.Path) - if !ok { - return nil, fmt.Errorf("exec expects a path, got %T", args[0]) - } - - // Process remaining args as key-value pairs - opts := make(map[builtin.Keyword]core.Any) - for i := 1; i < len(args); i += 2 { - key, ok := args[i].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("option key must be a keyword, got %T", args[i]) - } - - if i+1 >= len(args) { - return nil, fmt.Errorf("missing value for option %s", key) - } - - opts[key] = args[i+1] - } - ctx, cancel := e.NewContext(opts) - defer cancel() - - n, err := env.IPFS.Unixfs().Get(ctx, p) - if err != nil { - return nil, fmt.Errorf("failed to resolve node %v: %w", p, err) - } - - switch node := n.(type) { - case files.File: - bytecode, err := io.ReadAll(node) - if err != nil { - return nil, fmt.Errorf("failed to read bytecode: %w", err) - } - - procID, err := e.ExecBytes(ctx, bytecode) - if err != nil { - return nil, fmt.Errorf("failed to execute bytecode: %w", err) - } - return builtin.String(procID), nil - - case files.Directory: - return nil, errors.New("TODO: directory support") - default: - return nil, fmt.Errorf("unexpected node type: %T", node) - } -} - -func (e Exec) ExecBytes(ctx context.Context, bytecode []byte) (string, error) { - f, release := e.Session.Exec().Exec(ctx, func(p system.Executor_exec_Params) error { - return p.SetBytecode(bytecode) - }) - defer release() - - // Wait for the protocol setup to complete - result, err := f.Struct() - if err != nil { - return "", err - } - - procID, err := result.Protocol() - return procID, err -} - -func (e Exec) NewContext(opts map[builtin.Keyword]core.Any) (context.Context, context.CancelFunc) { - // TODO: add support for parsing durations like 15s, 15m, 15h, 15d - // if timeout, ok := opts["timeout"].(time.Duration); ok { - // return context.WithTimeout(context.Background(), timeout) - // } - - return context.WithTimeout(context.Background(), time.Second*15) -} diff --git a/cmd/ww/shell/globals.go b/cmd/ww/shell/globals.go deleted file mode 100644 index 76c7ecc..0000000 --- a/cmd/ww/shell/globals.go +++ /dev/null @@ -1,113 +0,0 @@ -package shell - -import ( - "errors" - "fmt" - - "github.com/libp2p/go-libp2p" - "github.com/spy16/slurp" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" - "github.com/urfave/cli/v2" -) - -const helpMessage = `Wetware Shell - Available commands: -help - Show this help message -version - Show wetware version -(+ a b ...) - Sum numbers -(* a b ...) - Multiply numbers -(= a b) - Compare equality -(> a b) - Greater than -(< a b) - Less than -(println expr) - Print expression with newline -(print expr) - Print expression without newline -(import "module") - Import a module (stubbed) - -IPFS Path Syntax: -/ipfs/QmHash/... - Direct IPFS path -/ipns/domain/... - IPNS path - -P2P Commands (use --with-p2p): -(peer :send "peer-addr" "proc-id" data) - Send data to a peer process -(peer :connect "peer-addr") - Connect to a peer -(peer :is-self "peer-id") - Check if peer ID is our own -(peer :id) - Get our own peer ID` - -var globals = map[string]core.Any{ - // Basic values - "nil": builtin.Nil{}, - "true": builtin.Bool(true), - "false": builtin.Bool(false), - "version": builtin.String("wetware-0.1.0"), - - // Basic operations - "=": slurp.Func("=", core.Eq), - "+": slurp.Func("sum", func(a ...int) int { - sum := 0 - for _, item := range a { - sum += item - } - return sum - }), - ">": slurp.Func(">", func(a, b builtin.Int64) bool { - return a > b - }), - "<": slurp.Func("<", func(a, b builtin.Int64) bool { - return a < b - }), - "*": slurp.Func("*", func(a ...int) int { - product := 1 - for _, item := range a { - product *= item - } - return product - }), - "/": slurp.Func("/", func(a, b builtin.Int64) float64 { - return float64(a) / float64(b) - }), - - // Wetware-specific functions - "help": slurp.Func("help", func() string { - return helpMessage - }), - "println": slurp.Func("println", func(args ...core.Any) { - for _, arg := range args { - fmt.Println(arg) - } - }), - "print": slurp.Func("print", func(args ...core.Any) { - for _, arg := range args { - fmt.Print(arg) - } - }), -} - -func NewGlobals(c *cli.Context) (map[string]core.Any, error) { - gs := make(map[string]core.Any, len(globals)) - for k, v := range globals { - gs[k] = v - } - - // Add IPFS support if --with-ipfs flag is set - if c.Bool("with-ipfs") || c.Bool("with-all") { - if env.IPFS == nil { - return nil, errors.New("uninitialized IPFS environment") - } - gs["ipfs"] = &IPFS{CoreAPI: env.IPFS} - } - - // Add P2P functionality if --with-p2p flag is set - if c.Bool("with-p2p") || c.Bool("with-all") { - // Create a new host for P2P functionality - host, err := libp2p.New() - if err != nil { - return nil, fmt.Errorf("failed to create libp2p host: %v", err) - } - gs["peer"] = &Peer{ - Ctx: c.Context, - Host: host, - } - } - - return gs, nil -} diff --git a/cmd/ww/shell/globals_test.go b/cmd/ww/shell/globals_test.go deleted file mode 100644 index e091183..0000000 --- a/cmd/ww/shell/globals_test.go +++ /dev/null @@ -1,155 +0,0 @@ -package shell - -import ( - "context" - "flag" - "testing" - - "github.com/spy16/slurp/builtin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -func TestGetBaseGlobals(t *testing.T) { - t.Parallel() - - // Create a mock CLI context - app := &cli.App{} - app.Flags = []cli.Flag{} - flagSet := &flag.FlagSet{} - flagSet.Bool("with-console", true, "Enable console capability") - flagSet.Bool("with-ipfs", false, "Enable IPFS capability") // Set to false for tests - flagSet.Bool("with-exec", true, "Enable exec capability") - flagSet.Bool("with-all", false, "Enable all capabilities") - c := cli.NewContext(app, flagSet, nil) - c.Context = context.Background() - baseGlobals, err := NewGlobals(c) - require.NoError(t, err) - - // Test that all expected base globals are present - expectedGlobals := []string{ - "nil", "true", "false", "version", - "=", "+", ">", "<", "*", "/", - "help", "println", "print", - } - - for _, expected := range expectedGlobals { - assert.Contains(t, baseGlobals, expected, "Base globals should contain %s", expected) - } - - // Test specific values - assert.Equal(t, builtin.Nil{}, baseGlobals["nil"]) - assert.Equal(t, builtin.Bool(true), baseGlobals["true"]) - assert.Equal(t, builtin.Bool(false), baseGlobals["false"]) - assert.Equal(t, builtin.String("wetware-0.1.0"), baseGlobals["version"]) -} - -func TestGlobalsFromGlobalsGo(t *testing.T) { - t.Parallel() - - // Test that the globals map from globals.go has expected content - expectedGlobals := []string{ - "nil", "true", "false", "version", - "=", "+", ">", "<", "*", "/", - "help", "println", "print", - } - - for _, expected := range expectedGlobals { - assert.Contains(t, globals, expected, "globals map should contain %s", expected) - } - - // Test specific values - assert.Equal(t, builtin.Nil{}, globals["nil"]) - assert.Equal(t, builtin.Bool(true), globals["true"]) - assert.Equal(t, builtin.Bool(false), globals["false"]) - assert.Equal(t, builtin.String("wetware-0.1.0"), globals["version"]) -} - -func TestArithmeticFunctions(t *testing.T) { - t.Parallel() - - // Test that arithmetic functions are present - addFunc, exists := globals["+"] - require.True(t, exists, "Addition function should be present") - require.NotNil(t, addFunc, "Addition function should not be nil") - - mulFunc, exists := globals["*"] - require.True(t, exists, "Multiplication function should be present") - require.NotNil(t, mulFunc, "Multiplication function should not be nil") - - divFunc, exists := globals["/"] - require.True(t, exists, "Division function should be present") - require.NotNil(t, divFunc, "Division function should not be nil") -} - -func TestComparisonFunctions(t *testing.T) { - t.Parallel() - - // Test that comparison functions are present - gtFunc, exists := globals[">"] - require.True(t, exists, "Greater than function should be present") - require.NotNil(t, gtFunc, "Greater than function should not be nil") - - ltFunc, exists := globals["<"] - require.True(t, exists, "Less than function should be present") - require.NotNil(t, ltFunc, "Less than function should not be nil") -} - -func TestEqualityFunction(t *testing.T) { - t.Parallel() - - // Test that equality function is present - eqFunc, exists := globals["="] - require.True(t, exists, "Equality function should be present") - require.NotNil(t, eqFunc, "Equality function should not be nil") -} - -func TestHelpFunction(t *testing.T) { - t.Parallel() - - // Test that help function is present - helpFunc, exists := globals["help"] - require.True(t, exists, "Help function should be present") - require.NotNil(t, helpFunc, "Help function should not be nil") -} - -func TestPrintFunctions(t *testing.T) { - t.Parallel() - - // Test that println function exists - printlnFunc, exists := globals["println"] - assert.True(t, exists, "Println function should exist") - assert.NotNil(t, printlnFunc, "Println function should not be nil") - - // Test that print function exists - printFunc, exists := globals["print"] - assert.True(t, exists, "Print function should exist") - assert.NotNil(t, printFunc, "Print function should not be nil") -} - -func TestGlobalsConsistency(t *testing.T) { - t.Parallel() - - // Test that getBaseGlobals returns a copy, not the original - app := &cli.App{} - app.Flags = []cli.Flag{} - flagSet := &flag.FlagSet{} - flagSet.Bool("with-ipfs", false, "Enable IPFS capability") // Set to false for tests - flagSet.Bool("with-exec", true, "Enable exec capability") - flagSet.Bool("with-console", true, "Enable console capability") - flagSet.Bool("with-all", false, "Enable all capabilities") - c := cli.NewContext(app, flagSet, nil) - c.Context = context.Background() - baseGlobals1, err := NewGlobals(c) - require.NoError(t, err) - baseGlobals2, err := NewGlobals(c) - require.NoError(t, err) - - // They should have the same content initially - assert.Equal(t, baseGlobals1, baseGlobals2) - - // Modifying one shouldn't affect the other - baseGlobals1["test"] = "modified" - assert.NotContains(t, baseGlobals2, "test") -} diff --git a/cmd/ww/shell/ipfs.go b/cmd/ww/shell/ipfs.go deleted file mode 100644 index 622ee06..0000000 --- a/cmd/ww/shell/ipfs.go +++ /dev/null @@ -1,265 +0,0 @@ -package shell - -import ( - "context" - "fmt" - "io" - "strings" - "time" - - "github.com/ipfs/boxo/files" - "github.com/ipfs/boxo/path" - iface "github.com/ipfs/kubo/core/coreiface" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" -) - -var _ core.Invokable = (*IPFS)(nil) - -type IPFS struct { - iface.CoreAPI -} - -// IPFS methods: (ipfs :cat /ipfs/Qm...) or (ipfs :get /ipfs/Qm...) -func (i IPFS) Invoke(args ...core.Any) (core.Any, error) { - if len(args) < 2 { - return nil, fmt.Errorf("ipfs requires at least 2 arguments: (ipfs :method path)") - } - - // First argument should be a keyword (:cat or :get) - method, ok := args[0].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("ipfs method must be a keyword, got %T", args[0]) - } - - src, ok := args[1].(path.Path) - if !ok { - return nil, fmt.Errorf("ipfs path must be a Path object, got %T", args[1]) - } - - // Create context with timeout - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - switch method { - case "cat": - return i.Cat(ctx, src) - case "get": - return i.Get(ctx, src) - default: - return nil, fmt.Errorf("unknown ipfs method: %s (supported: :cat, :get)", method) - } -} - -// Cat returns the content of an IPFS file as []byte -func (i IPFS) Cat(ctx context.Context, p path.Path) (core.Any, error) { - if i.CoreAPI == nil || i.CoreAPI.Unixfs() == nil { - return nil, fmt.Errorf("IPFS client not initialized") - } - - // Get the node from IPFS - node, err := i.Unixfs().Get(ctx, p) - if err != nil { - return nil, fmt.Errorf("failed to get IPFS path: %w", err) - } - - // Handle different node types - switch node := node.(type) { - case files.File: - // For files, read the content into a byte slice - content, err := io.ReadAll(node) - if err != nil { - return nil, fmt.Errorf("failed to read file content: %w", err) - } - return content, nil - case files.Directory: - return nil, fmt.Errorf("path is a directory, use :get instead of :cat") - default: - return nil, fmt.Errorf("unexpected node type: %T", node) - } -} - -// Get returns the IPFS node as a file-like object (io.Reader) that can be used with eval -func (i IPFS) Get(ctx context.Context, p path.Path) (core.Any, error) { - if i.CoreAPI == nil || i.CoreAPI.Unixfs() == nil { - return nil, fmt.Errorf("IPFS client not initialized") - } - - // Get the node from IPFS - node, err := i.Unixfs().Get(ctx, p) - if err != nil { - return nil, fmt.Errorf("failed to get IPFS path: %w", err) - } - - switch node := node.(type) { - case files.File: - return File{File: node}, nil - case files.Directory: - return Directory{Directory: node}, nil - default: - return Node{Node: node}, nil - } -} - -type Node struct { - files.Node -} - -func (n Node) String() string { - return fmt.Sprintf("", n.Type()) -} - -func (n Node) Type() string { - switch n.Node.(type) { - case files.File: - return "file" - case files.Directory: - return "directory" - default: - return "unknown" - } -} - -// Implement core.Invokable for Node -func (n Node) Invoke(args ...core.Any) (core.Any, error) { - if len(args) == 0 { - return n.String(), nil - } - - method, ok := args[0].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("node method must be a keyword, got %T", args[0]) - } - - switch method { - case "type": - return builtin.String(n.Type()), nil - case "size": - size, err := n.Size() - if err != nil { - return nil, fmt.Errorf("failed to get size: %w", err) - } - return builtin.Int64(size), nil - case "is-file": - return builtin.Bool(n.Type() == "file"), nil - case "is-directory": - return builtin.Bool(n.Type() == "directory"), nil - default: - return nil, fmt.Errorf("unknown node method: %s", method) - } -} - -type File struct { - files.File -} - -func (f File) String() string { - size, err := f.Size() - if err != nil { - return "" - } - return fmt.Sprintf("", size) -} - -// Implement core.Invokable for File -func (f File) Invoke(args ...core.Any) (core.Any, error) { - if len(args) == 0 { - return f.String(), nil - } - - method, ok := args[0].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("file method must be a keyword, got %T", args[0]) - } - - switch method { - case "read": - // Read all content - content, err := io.ReadAll(f.File) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - return content, nil - case "read-string": - // Read content as string - content, err := io.ReadAll(f.File) - if err != nil { - return nil, fmt.Errorf("failed to read file: %w", err) - } - return builtin.String(string(content)), nil - case "size": - size, err := f.Size() - if err != nil { - return nil, fmt.Errorf("failed to get size: %w", err) - } - return builtin.Int64(size), nil - case "type": - return builtin.String("file"), nil - default: - return nil, fmt.Errorf("unknown file method: %s", method) - } -} - -type Directory struct { - files.Directory -} - -func (d Directory) String() string { - // Try to get directory entries for a more informative string - entries := make([]string, 0) - it := d.Entries() - for it.Next() { - entries = append(entries, it.Name()) - } - - if len(entries) == 0 { - return "" - } - - if len(entries) <= 3 { - return fmt.Sprintf("", strings.Join(entries, ", ")) - } - - return fmt.Sprintf("", strings.Join(entries[:3], ", "), len(entries)-3) -} - -// Implement core.Invokable for Directory -func (d Directory) Invoke(args ...core.Any) (core.Any, error) { - if len(args) == 0 { - return d.String(), nil - } - - method, ok := args[0].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("directory method must be a keyword, got %T", args[0]) - } - - switch method { - case "list": - // List directory entries - entries := make([]core.Any, 0) - it := d.Entries() - for it.Next() { - entries = append(entries, builtin.String(it.Name())) - } - return builtin.NewList(entries...), nil - case "entries": - // Return directory entries as a list of strings - entries := make([]core.Any, 0) - it := d.Entries() - for it.Next() { - entries = append(entries, builtin.String(it.Name())) - } - return builtin.NewList(entries...), nil - case "size": - size, err := d.Size() - if err != nil { - return nil, fmt.Errorf("failed to get size: %w", err) - } - return builtin.Int64(size), nil - case "type": - return builtin.String("directory"), nil - default: - return nil, fmt.Errorf("unknown directory method: %s", method) - } -} diff --git a/cmd/ww/shell/ipfs_test.go b/cmd/ww/shell/ipfs_test.go deleted file mode 100644 index e402a0f..0000000 --- a/cmd/ww/shell/ipfs_test.go +++ /dev/null @@ -1,334 +0,0 @@ -package shell_test - -import ( - "testing" - - "github.com/ipfs/boxo/files" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/wetware/go/cmd/ww/shell" -) - -func TestFile_String(t *testing.T) { - t.Parallel() - - // Create a mock file with known size - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - - f := shell.File{File: file} - - // Test String method - result := f.String() - assert.Contains(t, result, "IPFS File:") - assert.Contains(t, result, "bytes") -} - -func TestFile_Invoke(t *testing.T) { - t.Parallel() - - // Create a mock file - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - f := shell.File{File: file} - - tests := []struct { - name string - args []core.Any - expected interface{} - wantErr bool - }{ - { - name: "no args - returns string representation", - args: []core.Any{}, - expected: "IPFS File:", - wantErr: false, - }, - { - name: "type method", - args: []core.Any{builtin.Keyword("type")}, - expected: builtin.String("file"), - wantErr: false, - }, - { - name: "size method", - args: []core.Any{builtin.Keyword("size")}, - expected: builtin.Int64(len(content)), - wantErr: false, - }, - { - name: "read-string method", - args: []core.Any{builtin.Keyword("read-string")}, - expected: builtin.String(content), - wantErr: false, - }, - { - name: "invalid method", - args: []core.Any{builtin.Keyword("invalid")}, - expected: nil, - wantErr: true, - }, - { - name: "non-keyword argument", - args: []core.Any{builtin.String("not-a-keyword")}, - expected: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := f.Invoke(tt.args...) - - if tt.wantErr { - assert.Error(t, err) - return - } - - require.NoError(t, err) - - if tt.expected != nil { - switch expected := tt.expected.(type) { - case string: - if expected == "IPFS File:" { - assert.Contains(t, result.(string), expected) - } else { - assert.Equal(t, expected, result) - } - case int64: - assert.Equal(t, expected, result) - } - } - }) - } -} - -func TestDirectory_String(t *testing.T) { - t.Parallel() - - // Create a mock directory - dir := files.NewMapDirectory(map[string]files.Node{ - "file1.txt": files.NewBytesFile([]byte("content1")), - "file2.txt": files.NewBytesFile([]byte("content2")), - }) - - d := shell.Directory{Directory: dir} - - // Test String method - result := d.String() - assert.Contains(t, result, "IPFS Directory:") - assert.Contains(t, result, "file1.txt") - assert.Contains(t, result, "file2.txt") -} - -func TestDirectory_Invoke(t *testing.T) { - t.Parallel() - - // Create a mock directory - dir := files.NewMapDirectory(map[string]files.Node{ - "file1.txt": files.NewBytesFile([]byte("content1")), - "file2.txt": files.NewBytesFile([]byte("content2")), - }) - - d := shell.Directory{Directory: dir} - - tests := []struct { - name string - args []core.Any - expected interface{} - wantErr bool - }{ - { - name: "no args - returns string representation", - args: []core.Any{}, - expected: "IPFS Directory:", - wantErr: false, - }, - { - name: "type method", - args: []core.Any{builtin.Keyword("type")}, - expected: builtin.String("directory"), - wantErr: false, - }, - { - name: "list method", - args: []core.Any{builtin.Keyword("list")}, - expected: nil, // Will be a list, we'll check it's not nil - wantErr: false, - }, - { - name: "entries method", - args: []core.Any{builtin.Keyword("entries")}, - expected: nil, // Will be a list, we'll check it's not nil - wantErr: false, - }, - { - name: "invalid method", - args: []core.Any{builtin.Keyword("invalid")}, - expected: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := d.Invoke(tt.args...) - - if tt.wantErr { - assert.Error(t, err) - return - } - - require.NoError(t, err) - - if tt.expected != nil { - if tt.expected == "IPFS Directory:" { - assert.Contains(t, result.(string), tt.expected) - } else { - assert.Equal(t, tt.expected, result) - } - } else { - // For list methods, just check it's not nil - assert.NotNil(t, result) - } - }) - } -} - -func TestNode_String(t *testing.T) { - t.Parallel() - - // Test with a file node - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - n := shell.Node{Node: file} - - result := n.String() - assert.Contains(t, result, "IPFS Node:") - assert.Contains(t, result, "file") -} - -func TestNode_Type(t *testing.T) { - t.Parallel() - - // Test with a file node - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - n := shell.Node{Node: file} - - assert.Equal(t, "file", n.Type()) - - // Test with a directory node - dir := files.NewMapDirectory(map[string]files.Node{}) - nDir := shell.Node{Node: dir} - assert.Equal(t, "directory", nDir.Type()) -} - -func TestNode_Invoke(t *testing.T) { - t.Parallel() - - // Create a mock file node - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - n := shell.Node{Node: file} - - tests := []struct { - name string - args []core.Any - expected interface{} - wantErr bool - }{ - { - name: "no args - returns string representation", - args: []core.Any{}, - expected: "IPFS Node:", - wantErr: false, - }, - { - name: "type method", - args: []core.Any{builtin.Keyword("type")}, - expected: builtin.String("file"), - wantErr: false, - }, - { - name: "is-file method", - args: []core.Any{builtin.Keyword("is-file")}, - expected: builtin.Bool(true), - wantErr: false, - }, - { - name: "is-directory method", - args: []core.Any{builtin.Keyword("is-directory")}, - expected: builtin.Bool(false), - wantErr: false, - }, - { - name: "size method", - args: []core.Any{builtin.Keyword("size")}, - expected: builtin.Int64(len(content)), - wantErr: false, - }, - { - name: "invalid method", - args: []core.Any{builtin.Keyword("invalid")}, - expected: nil, - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - result, err := n.Invoke(tt.args...) - - if tt.wantErr { - assert.Error(t, err) - return - } - - require.NoError(t, err) - - if tt.expected != nil { - if tt.expected == "IPFS Node:" { - assert.Contains(t, result.(string), tt.expected) - } else { - assert.Equal(t, tt.expected, result) - } - } - }) - } -} - -// TestFile_ReadContent tests that file content can be read multiple times -func TestFile_ReadContent(t *testing.T) { - t.Parallel() - - content := "Hello, World!" - file := files.NewBytesFile([]byte(content)) - f := shell.File{File: file} - - result, err := f.Invoke(builtin.Keyword("read-string")) - require.NoError(t, err) - assert.Equal(t, builtin.String(content), result) -} - -// TestDirectory_Empty tests empty directory handling -func TestDirectory_Empty(t *testing.T) { - t.Parallel() - - // Create an empty directory - dir := files.NewMapDirectory(map[string]files.Node{}) - d := shell.Directory{Directory: dir} - - // Test string representation - result := d.String() - assert.Contains(t, result, "empty") - - // Test list method - _, err := d.Invoke(builtin.Keyword("list")) - require.NoError(t, err) - // For empty directory, result might be nil or empty list - both are acceptable - // We just verify the method doesn't error -} diff --git a/cmd/ww/shell/path.go b/cmd/ww/shell/path.go deleted file mode 100644 index c1b55d8..0000000 --- a/cmd/ww/shell/path.go +++ /dev/null @@ -1,69 +0,0 @@ -package shell - -import ( - "fmt" - "io" - "strings" - "unicode" - - "github.com/ipfs/boxo/path" - iface "github.com/ipfs/kubo/core/coreiface" - "github.com/spy16/slurp/core" - "github.com/spy16/slurp/reader" -) - -// DefaultReaderFactory creates readers with IPFS path support -type DefaultReaderFactory struct { - IPFS iface.CoreAPI -} - -func (f DefaultReaderFactory) NewReader(r io.Reader) *reader.Reader { - rd := reader.New(r) - rd.SetMacro('/', false, NewPathReader(f.IPFS)) - return rd -} - -// NewPathReader is a ReaderMacro that handles IPFS and IPNS paths -func NewPathReader(ipfs iface.CoreAPI) func(*reader.Reader, rune) (core.Any, error) { - return func(rd *reader.Reader, init rune) (core.Any, error) { - beginPos := rd.Position() - - // Read the full path manually by reading runes until we hit whitespace or a delimiter - var b strings.Builder - b.WriteRune(init) // Start with the '/' character - - for { - r, err := rd.NextRune() - if err != nil { - if err == io.EOF { - break - } - return nil, &reader.Error{ - Cause: err, - Begin: beginPos, - End: beginPos, - } - } - - // Check if this rune should terminate the path - // Stop on whitespace, closing parentheses, and other delimiters - if unicode.IsSpace(r) || r == ')' || r == ']' || r == '}' { - rd.Unread(r) - break - } - - b.WriteRune(r) - } - - p, err := path.NewPath(b.String()) - if err != nil { - return nil, &reader.Error{ - Cause: fmt.Errorf("invalid IPFS/IPNS path: %s", err), - Begin: beginPos, - End: beginPos, - } - } - - return p, nil - } -} diff --git a/cmd/ww/shell/path_test.go b/cmd/ww/shell/path_test.go deleted file mode 100644 index b7d3584..0000000 --- a/cmd/ww/shell/path_test.go +++ /dev/null @@ -1,202 +0,0 @@ -package shell_test - -import ( - "strings" - "testing" - - "github.com/ipfs/boxo/path" - "github.com/spy16/slurp/reader" - "github.com/wetware/go/cmd/ww/shell" -) - -func TestIPFSPathReader(t *testing.T) { - t.Parallel() - tests := []struct { - name string - input string - expected interface{} - wantErr bool - }{ - { - name: "valid IPFS path", - input: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/example", - expected: "Path", - wantErr: false, - }, - { - name: "valid IPNS path", - input: "/ipns/example.com/file", - expected: "Path", - wantErr: false, - }, - { - name: "invalid path starting with slash", - input: "/foo/bar", - expected: nil, - wantErr: true, - }, - { - name: "single slash", - input: "/", - expected: nil, - wantErr: true, - }, - { - name: "path with spaces", - input: "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/example file", - expected: "Path", - wantErr: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - // Create a reader with our test input - rd := reader.New(strings.NewReader(tt.input)) - - // Call the IPFS path reader - result, err := shell.NewPathReader(nil)(rd, '/') - - // Check error expectations - if tt.wantErr { - if err == nil { - t.Errorf("IPFSPathReader() expected error but got none") - } - return - } - - if err != nil { - t.Errorf("IPFSPathReader() unexpected error: %v", err) - return - } - - // Check result type - if tt.expected == "Path" { - if _, ok := result.(path.Path); !ok { - t.Errorf("IPFSPathReader() expected Path type, got %T", result) - } - } - }) - } -} - -func TestIPFSPathReaderWithValidPaths(t *testing.T) { - t.Parallel() - validPaths := []string{ - "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi", - "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/example", - "/ipfs/bafybeigdyrzt5sfp7udm7hu76uh7y26nf3efuylqabf3oclgtqy55fbzdi/example/file.txt", - "/ipns/example.com", - "/ipns/example.com/file", - "/ipns/example.com/deep/path/file.txt", - } - - for _, pathStr := range validPaths { - t.Run("valid_"+pathStr, func(t *testing.T) { - rd := reader.New(strings.NewReader(pathStr)) - - result, err := shell.NewPathReader(nil)(rd, '/') - if err != nil { - t.Errorf("IPFSPathReader() failed for valid path %s: %v", pathStr, err) - return - } - - pathObj, ok := result.(path.Path) - if !ok { - t.Errorf("IPFSPathReader() returned wrong type for %s: %T", pathStr, result) - return - } - - // Verify the path string matches - if pathObj.String() != pathStr { - t.Errorf("IPFSPathReader() returned path %s, expected %s", pathObj.String(), pathStr) - } - }) - } -} - -func TestIPFSPathReaderWithInvalidPaths(t *testing.T) { - t.Parallel() - invalidPaths := []string{ - "/foo", - "/bar/baz", - "/notipfs/path", - "/notipns/domain", - "/random/stuff", - } - - for _, pathStr := range invalidPaths { - t.Run("invalid_"+pathStr, func(t *testing.T) { - rd := reader.New(strings.NewReader(pathStr)) - - result, err := shell.NewPathReader(nil)(rd, '/') - if err == nil { - t.Errorf("IPFSPathReader() should have failed for invalid path %s, got result: %v", pathStr, result) - } - - // Check that it's a reader error - if _, ok := err.(*reader.Error); !ok { - t.Errorf("IPFSPathReader() should return reader.Error for invalid path %s, got: %T", pathStr, err) - } - }) - } -} - -func TestIPFSPathReaderEdgeCases(t *testing.T) { - t.Parallel() - tests := []struct { - name string - input string - wantErr bool - }{ - { - name: "empty after slash", - input: "/", - wantErr: true, - }, - { - name: "just ipfs", - input: "/ipfs", - wantErr: true, - }, - { - name: "just ipns", - input: "/ipns", - wantErr: true, - }, - { - name: "ipfs with trailing slash", - input: "/ipfs/", - wantErr: true, - }, - { - name: "ipns with trailing slash", - input: "/ipns/", - wantErr: true, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - rd := reader.New(strings.NewReader(tt.input)) - - result, err := shell.NewPathReader(nil)(rd, '/') - if tt.wantErr { - if err == nil { - t.Errorf("IPFSPathReader() expected error for %s but got none", tt.input) - } - } else { - if err != nil { - t.Errorf("IPFSPathReader() unexpected error for %s: %v", tt.input, err) - } - } - - // If no error, result should be a Path - if err == nil { - if _, ok := result.(path.Path); !ok { - t.Errorf("IPFSPathReader() returned wrong type for %s: %T", tt.input, result) - } - } - }) - } -} diff --git a/cmd/ww/shell/peer.go b/cmd/ww/shell/peer.go deleted file mode 100644 index e9e7a38..0000000 --- a/cmd/ww/shell/peer.go +++ /dev/null @@ -1,227 +0,0 @@ -package shell - -import ( - "bytes" - "context" - "fmt" - "io" - "strings" - - "github.com/libp2p/go-libp2p/core/host" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - ma "github.com/multiformats/go-multiaddr" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" -) - -var _ core.Invokable = (*Peer)(nil) - -type Peer struct { - Ctx context.Context - Host host.Host -} - -// Peer methods: (peer :send "peer-addr" "proc-id" data) or (peer :connect "peer-addr") -func (p Peer) Invoke(args ...core.Any) (core.Any, error) { - if len(args) == 0 { - return p.String(), nil - } - - if len(args) < 1 { - return nil, fmt.Errorf("peer requires at least 1 argument: (peer :method ...)") - } - - // First argument should be a keyword (:send, :connect, :is-self, :id) - method, ok := args[0].(builtin.Keyword) - if !ok { - return nil, fmt.Errorf("peer method must be a keyword, got %T", args[0]) - } - - switch method { - case "id": - return p.ID(), nil - case "send": - if len(args) < 4 { - return nil, fmt.Errorf("peer :send requires 3 arguments: (peer :send peer-addr proc-id data)") - } - - var peerAddr string - switch v := args[1].(type) { - case string: - peerAddr = v - case builtin.String: - peerAddr = string(v) - default: - return nil, fmt.Errorf("peer address must be a string or builtin.String, got %T", args[1]) - } - - var procID string - switch v := args[2].(type) { - case string: - procID = v - case builtin.String: - procID = string(v) - default: - return nil, fmt.Errorf("process ID must be a string or builtin.String, got %T", args[2]) - } - - return p.Send(p.Ctx, peerAddr, procID, args[3]) - - case "connect": - if len(args) < 2 { - return nil, fmt.Errorf("peer :connect requires 1 argument: (peer :connect peer-addr)") - } - - var peerAddr string - switch v := args[1].(type) { - case string: - peerAddr = v - case builtin.String: - peerAddr = string(v) - default: - return nil, fmt.Errorf("peer address must be a string or builtin.String, got %T", args[1]) - } - return p.Connect(peerAddr) - - case "is-self": - if len(args) < 2 { - return nil, fmt.Errorf("peer :is-self requires 1 argument: (peer :is-self peer-id)") - } - - var peerIDStr string - switch v := args[1].(type) { - case string: - peerIDStr = v - case builtin.String: - peerIDStr = string(v) - default: - return nil, fmt.Errorf("peer ID must be a string or builtin.String, got %T", args[1]) - } - return p.IsSelf(peerIDStr) - - default: - return nil, fmt.Errorf("unknown peer method: %s (supported: :send, :connect, :is-self, :id)", method) - } -} - -func (p Peer) String() string { - if p.Host == nil { - return "" - } - return fmt.Sprintf("", p.Host.ID()) -} - -// Send sends data to a specific peer and process -func (p *Peer) Send(ctx context.Context, peerAddr, procIDStr string, data interface{}) (core.Any, error) { - // Parse peer address - peerInfo, err := p.parsePeerAddr(peerAddr) - if err != nil { - return nil, fmt.Errorf("failed to parse peer address: %w", err) - } - - // Check if we're sending to ourselves - if peerInfo.ID == p.Host.ID() { - // TODO: Implement self-routing optimization - // For now, we'll still go through the network - } - - // Connect to the peer - if err := p.Host.Connect(ctx, *peerInfo); err != nil { - return nil, fmt.Errorf("failed to connect to peer: %w", err) - } - - // Create protocol ID from process ID - protocolID := protocol.ID("/ww/0.1.0/" + procIDStr) - - // Open stream to the peer - stream, err := p.Host.NewStream(ctx, peerInfo.ID, protocolID) - if err != nil { - return nil, fmt.Errorf("failed to open stream: %w", err) - } - defer stream.Close() - - // Convert data to io.Reader based on type - var reader io.Reader - switch v := data.(type) { - case io.Reader: - reader = v - case []byte: - reader = bytes.NewReader(v) - case string: - reader = strings.NewReader(v) - default: - return nil, fmt.Errorf("unsupported data type: %T, expected io.Reader, []byte, or string", data) - } - - // Send the data atomically - _, err = io.Copy(stream, reader) - if err != nil { - return nil, fmt.Errorf("failed to send data: %w", err) - } - - return builtin.String("sent"), nil -} - -// Connect establishes a connection to a peer -func (p *Peer) Connect(peerAddr string) (core.Any, error) { - ctx := context.TODO() - - // Parse peer address - peerInfo, err := p.parsePeerAddr(peerAddr) - if err != nil { - return nil, fmt.Errorf("failed to parse peer address: %w", err) - } - - // Connect to the peer - if err := p.Host.Connect(ctx, *peerInfo); err != nil { - return nil, fmt.Errorf("failed to connect to peer: %w", err) - } - - return builtin.String("connected"), nil -} - -// IsSelf checks if the given peer ID is our own -func (p *Peer) IsSelf(peerIDStr string) (core.Any, error) { - targetPeerID, err := peer.Decode(peerIDStr) - if err != nil { - return nil, fmt.Errorf("invalid peer ID: %w", err) - } - - return builtin.Bool(targetPeerID == p.Host.ID()), nil -} - -// ID returns our own peer ID as a string -func (p *Peer) ID() core.Any { - if p.Host == nil { - return builtin.String("") - } - return builtin.String(p.Host.ID().String()) -} - -// parsePeerAddr parses a peer address (either peer ID or multiaddr) into AddrInfo -func (p *Peer) parsePeerAddr(peerAddr string) (*peer.AddrInfo, error) { - // Try to parse as peer ID first - peerID, err := peer.Decode(peerAddr) - if err == nil { - // Successfully parsed as peer ID - return &peer.AddrInfo{ - ID: peerID, - // Note: In a real implementation, you'd need peer discovery - // or provide addresses as additional parameters - }, nil - } - - // Fall back to treating as multiaddr - addr, err := ma.NewMultiaddr(peerAddr) - if err != nil { - return nil, fmt.Errorf("invalid peer address or ID: %w", err) - } - - peerInfo, err := peer.AddrInfoFromP2pAddr(addr) - if err != nil { - return nil, fmt.Errorf("failed to parse peer info from multiaddr: %w", err) - } - - return peerInfo, nil -} diff --git a/cmd/ww/shell/peer_test.go b/cmd/ww/shell/peer_test.go deleted file mode 100644 index 9062636..0000000 --- a/cmd/ww/shell/peer_test.go +++ /dev/null @@ -1,103 +0,0 @@ -package shell_test - -import ( - "context" - "testing" - - "github.com/libp2p/go-libp2p" - "github.com/spy16/slurp/builtin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/wetware/go/cmd/ww/shell" -) - -func TestPeer(t *testing.T) { - t.Parallel() - - // Create a test host - host, err := libp2p.New() - require.NoError(t, err, "failed to create test host") - defer host.Close() - - // Create a test peer - peer := &shell.Peer{ - Ctx: context.Background(), - Host: host, - } - - t.Run("String", func(t *testing.T) { - str := peer.String() - assert.NotEqual(t, "", str, "expected peer string to show host ID") - assert.NotEmpty(t, str, "expected non-empty peer string") - }) - - t.Run("ID", func(t *testing.T) { - id := peer.ID() - require.NotNil(t, id, "expected non-nil peer ID") - - idStr, ok := id.(builtin.String) - require.True(t, ok, "expected builtin.String, got %T", id) - assert.NotEmpty(t, string(idStr), "expected non-empty peer ID string") - }) - - t.Run("IsSelf", func(t *testing.T) { - // Test with our own peer ID - ourID := host.ID().String() - result, err := peer.IsSelf(ourID) - require.NoError(t, err, "unexpected error") - - isSelf, ok := result.(builtin.Bool) - require.True(t, ok, "expected builtin.Bool, got %T", result) - assert.True(t, bool(isSelf), "expected peer to recognize itself") - - // Test with a different peer ID - create another host to get a valid peer ID - otherHost, err := libp2p.New() - require.NoError(t, err, "failed to create other host") - defer otherHost.Close() - - differentID := otherHost.ID().String() - result, err = peer.IsSelf(differentID) - require.NoError(t, err, "unexpected error") - - isSelf, ok = result.(builtin.Bool) - require.True(t, ok, "expected builtin.Bool, got %T", result) - assert.False(t, bool(isSelf), "expected peer to not recognize different ID as self") - }) - - t.Run("Invoke", func(t *testing.T) { - // Test :id method - result, err := peer.Invoke(builtin.Keyword("id")) - require.NoError(t, err, "unexpected error calling :id") - assert.NotNil(t, result, "expected non-nil result from :id") - - // Test :is-self method - ourID := host.ID().String() - result, err = peer.Invoke(builtin.Keyword("is-self"), ourID) - require.NoError(t, err, "unexpected error calling :is-self") - assert.NotNil(t, result, "expected non-nil result from :is-self") - - // Test invalid method - _, err = peer.Invoke(builtin.Keyword("invalid")) - assert.Error(t, err, "expected error for invalid method") - }) - - t.Run("BuiltinStringSupport", func(t *testing.T) { - // Test that builtin.String types are properly handled in Peer methods. - // This is important for composable expressions like: - // (peer :send (peer :id) "/ww/0.1.0/crU9ZDuzKWr" "hello, wetware!") - // where (peer :id) returns a builtin.String, not a regular Go string. - ourID := host.ID().String() - builtinID := builtin.String(ourID) - - // Test :is-self with builtin.String - result, err := peer.Invoke(builtin.Keyword("is-self"), builtinID) - require.NoError(t, err, "unexpected error calling :is-self with builtin.String") - assert.NotNil(t, result, "expected non-nil result from :is-self") - - // Test :connect with builtin.String (this will fail to connect, but should not error on type) - _, err = peer.Invoke(builtin.Keyword("connect"), builtinID) - // We expect a connection error, not a type error - assert.Error(t, err, "expected connection error") - assert.NotContains(t, err.Error(), "must be a string", "should not get type error for builtin.String") - }) -} diff --git a/cmd/ww/shell/shell.capnp b/cmd/ww/shell/shell.capnp deleted file mode 100644 index 22158d7..0000000 --- a/cmd/ww/shell/shell.capnp +++ /dev/null @@ -1,29 +0,0 @@ -using Go = import "/go.capnp"; - -@0xead48f650c32a806; - -$Go.package("main"); -$Go.import("github.com/wetware/go/shell"); - - -interface EventLoop { - addActor @0 (handler :Text) -> (mailbox :Mailbox); -} - -interface Mailbox { - newBuffer @0 () -> (buffer :Buffer); -} - -interface Buffer { - write @0 (input :Data) -> (status :Status); - writeString @1 (input :Text) -> (status :Status); - read @2 (count :UInt64) -> (output :Data, status :Status); - flush @3 (); - struct Status { - union { - ok @0 :Void; - eof @1 :Void; - error @2 :Text; - } - } -} diff --git a/cmd/ww/shell/shell.capnp.go b/cmd/ww/shell/shell.capnp.go deleted file mode 100644 index 370bf4b..0000000 --- a/cmd/ww/shell/shell.capnp.go +++ /dev/null @@ -1,1824 +0,0 @@ -// Code generated by capnpc-go. DO NOT EDIT. - -package shell - -import ( - context "context" - strconv "strconv" - - capnp "capnproto.org/go/capnp/v3" - text "capnproto.org/go/capnp/v3/encoding/text" - fc "capnproto.org/go/capnp/v3/flowcontrol" - schemas "capnproto.org/go/capnp/v3/schemas" - server "capnproto.org/go/capnp/v3/server" -) - -type EventLoop capnp.Client - -// EventLoop_TypeID is the unique identifier for the type EventLoop. -const EventLoop_TypeID = 0xef0707db310901e8 - -func (c EventLoop) AddActor(ctx context.Context, params func(EventLoop_addActor_Params) error) (EventLoop_addActor_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xef0707db310901e8, - MethodID: 0, - InterfaceName: "shell.capnp:EventLoop", - MethodName: "addActor", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} - s.PlaceArgs = func(s capnp.Struct) error { return params(EventLoop_addActor_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return EventLoop_addActor_Results_Future{Future: ans.Future()}, release - -} - -func (c EventLoop) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c EventLoop) String() string { - return "EventLoop(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c EventLoop) AddRef() EventLoop { - return EventLoop(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c EventLoop) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c EventLoop) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c EventLoop) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (EventLoop) DecodeFromPtr(p capnp.Ptr) EventLoop { - return EventLoop(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c EventLoop) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c EventLoop) IsSame(other EventLoop) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c EventLoop) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c EventLoop) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A EventLoop_Server is a EventLoop with a local implementation. -type EventLoop_Server interface { - AddActor(context.Context, EventLoop_addActor) error -} - -// EventLoop_NewServer creates a new Server from an implementation of EventLoop_Server. -func EventLoop_NewServer(s EventLoop_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(EventLoop_Methods(nil, s), s, c) -} - -// EventLoop_ServerToClient creates a new Client from an implementation of EventLoop_Server. -// The caller is responsible for calling Release on the returned Client. -func EventLoop_ServerToClient(s EventLoop_Server) EventLoop { - return EventLoop(capnp.NewClient(EventLoop_NewServer(s))) -} - -// EventLoop_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func EventLoop_Methods(methods []server.Method, s EventLoop_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 1) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xef0707db310901e8, - MethodID: 0, - InterfaceName: "shell.capnp:EventLoop", - MethodName: "addActor", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.AddActor(ctx, EventLoop_addActor{call}) - }, - }) - - return methods -} - -// EventLoop_addActor holds the state for a server call to EventLoop.addActor. -// See server.Call for documentation. -type EventLoop_addActor struct { - *server.Call -} - -// Args returns the call's arguments. -func (c EventLoop_addActor) Args() EventLoop_addActor_Params { - return EventLoop_addActor_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c EventLoop_addActor) AllocResults() (EventLoop_addActor_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return EventLoop_addActor_Results(r), err -} - -// EventLoop_List is a list of EventLoop. -type EventLoop_List = capnp.CapList[EventLoop] - -// NewEventLoop creates a new list of EventLoop. -func NewEventLoop_List(s *capnp.Segment, sz int32) (EventLoop_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[EventLoop](l), err -} - -type EventLoop_addActor_Params capnp.Struct - -// EventLoop_addActor_Params_TypeID is the unique identifier for the type EventLoop_addActor_Params. -const EventLoop_addActor_Params_TypeID = 0xd91363a073d0485a - -func NewEventLoop_addActor_Params(s *capnp.Segment) (EventLoop_addActor_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return EventLoop_addActor_Params(st), err -} - -func NewRootEventLoop_addActor_Params(s *capnp.Segment) (EventLoop_addActor_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return EventLoop_addActor_Params(st), err -} - -func ReadRootEventLoop_addActor_Params(msg *capnp.Message) (EventLoop_addActor_Params, error) { - root, err := msg.Root() - return EventLoop_addActor_Params(root.Struct()), err -} - -func (s EventLoop_addActor_Params) String() string { - str, _ := text.Marshal(0xd91363a073d0485a, capnp.Struct(s)) - return str -} - -func (s EventLoop_addActor_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (EventLoop_addActor_Params) DecodeFromPtr(p capnp.Ptr) EventLoop_addActor_Params { - return EventLoop_addActor_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s EventLoop_addActor_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s EventLoop_addActor_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s EventLoop_addActor_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s EventLoop_addActor_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s EventLoop_addActor_Params) Handler() (string, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s EventLoop_addActor_Params) HasHandler() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s EventLoop_addActor_Params) HandlerBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s EventLoop_addActor_Params) SetHandler(v string) error { - return capnp.Struct(s).SetText(0, v) -} - -// EventLoop_addActor_Params_List is a list of EventLoop_addActor_Params. -type EventLoop_addActor_Params_List = capnp.StructList[EventLoop_addActor_Params] - -// NewEventLoop_addActor_Params creates a new list of EventLoop_addActor_Params. -func NewEventLoop_addActor_Params_List(s *capnp.Segment, sz int32) (EventLoop_addActor_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[EventLoop_addActor_Params](l), err -} - -// EventLoop_addActor_Params_Future is a wrapper for a EventLoop_addActor_Params promised by a client call. -type EventLoop_addActor_Params_Future struct{ *capnp.Future } - -func (f EventLoop_addActor_Params_Future) Struct() (EventLoop_addActor_Params, error) { - p, err := f.Future.Ptr() - return EventLoop_addActor_Params(p.Struct()), err -} - -type EventLoop_addActor_Results capnp.Struct - -// EventLoop_addActor_Results_TypeID is the unique identifier for the type EventLoop_addActor_Results. -const EventLoop_addActor_Results_TypeID = 0xbbafeb3e01e71170 - -func NewEventLoop_addActor_Results(s *capnp.Segment) (EventLoop_addActor_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return EventLoop_addActor_Results(st), err -} - -func NewRootEventLoop_addActor_Results(s *capnp.Segment) (EventLoop_addActor_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return EventLoop_addActor_Results(st), err -} - -func ReadRootEventLoop_addActor_Results(msg *capnp.Message) (EventLoop_addActor_Results, error) { - root, err := msg.Root() - return EventLoop_addActor_Results(root.Struct()), err -} - -func (s EventLoop_addActor_Results) String() string { - str, _ := text.Marshal(0xbbafeb3e01e71170, capnp.Struct(s)) - return str -} - -func (s EventLoop_addActor_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (EventLoop_addActor_Results) DecodeFromPtr(p capnp.Ptr) EventLoop_addActor_Results { - return EventLoop_addActor_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s EventLoop_addActor_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s EventLoop_addActor_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s EventLoop_addActor_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s EventLoop_addActor_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s EventLoop_addActor_Results) Mailbox() Mailbox { - p, _ := capnp.Struct(s).Ptr(0) - return Mailbox(p.Interface().Client()) -} - -func (s EventLoop_addActor_Results) HasMailbox() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s EventLoop_addActor_Results) SetMailbox(v Mailbox) error { - if !v.IsValid() { - return capnp.Struct(s).SetPtr(0, capnp.Ptr{}) - } - seg := s.Segment() - in := capnp.NewInterface(seg, seg.Message().CapTable().Add(capnp.Client(v))) - return capnp.Struct(s).SetPtr(0, in.ToPtr()) -} - -// EventLoop_addActor_Results_List is a list of EventLoop_addActor_Results. -type EventLoop_addActor_Results_List = capnp.StructList[EventLoop_addActor_Results] - -// NewEventLoop_addActor_Results creates a new list of EventLoop_addActor_Results. -func NewEventLoop_addActor_Results_List(s *capnp.Segment, sz int32) (EventLoop_addActor_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[EventLoop_addActor_Results](l), err -} - -// EventLoop_addActor_Results_Future is a wrapper for a EventLoop_addActor_Results promised by a client call. -type EventLoop_addActor_Results_Future struct{ *capnp.Future } - -func (f EventLoop_addActor_Results_Future) Struct() (EventLoop_addActor_Results, error) { - p, err := f.Future.Ptr() - return EventLoop_addActor_Results(p.Struct()), err -} -func (p EventLoop_addActor_Results_Future) Mailbox() Mailbox { - return Mailbox(p.Future.Field(0, nil).Client()) -} - -type Mailbox capnp.Client - -// Mailbox_TypeID is the unique identifier for the type Mailbox. -const Mailbox_TypeID = 0xbd4bc3d338b68790 - -func (c Mailbox) NewBuffer(ctx context.Context, params func(Mailbox_newBuffer_Params) error) (Mailbox_newBuffer_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xbd4bc3d338b68790, - MethodID: 0, - InterfaceName: "shell.capnp:Mailbox", - MethodName: "newBuffer", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} - s.PlaceArgs = func(s capnp.Struct) error { return params(Mailbox_newBuffer_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Mailbox_newBuffer_Results_Future{Future: ans.Future()}, release - -} - -func (c Mailbox) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c Mailbox) String() string { - return "Mailbox(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c Mailbox) AddRef() Mailbox { - return Mailbox(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c Mailbox) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c Mailbox) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c Mailbox) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (Mailbox) DecodeFromPtr(p capnp.Ptr) Mailbox { - return Mailbox(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c Mailbox) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c Mailbox) IsSame(other Mailbox) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c Mailbox) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c Mailbox) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A Mailbox_Server is a Mailbox with a local implementation. -type Mailbox_Server interface { - NewBuffer(context.Context, Mailbox_newBuffer) error -} - -// Mailbox_NewServer creates a new Server from an implementation of Mailbox_Server. -func Mailbox_NewServer(s Mailbox_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(Mailbox_Methods(nil, s), s, c) -} - -// Mailbox_ServerToClient creates a new Client from an implementation of Mailbox_Server. -// The caller is responsible for calling Release on the returned Client. -func Mailbox_ServerToClient(s Mailbox_Server) Mailbox { - return Mailbox(capnp.NewClient(Mailbox_NewServer(s))) -} - -// Mailbox_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func Mailbox_Methods(methods []server.Method, s Mailbox_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 1) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xbd4bc3d338b68790, - MethodID: 0, - InterfaceName: "shell.capnp:Mailbox", - MethodName: "newBuffer", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.NewBuffer(ctx, Mailbox_newBuffer{call}) - }, - }) - - return methods -} - -// Mailbox_newBuffer holds the state for a server call to Mailbox.newBuffer. -// See server.Call for documentation. -type Mailbox_newBuffer struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Mailbox_newBuffer) Args() Mailbox_newBuffer_Params { - return Mailbox_newBuffer_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Mailbox_newBuffer) AllocResults() (Mailbox_newBuffer_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Mailbox_newBuffer_Results(r), err -} - -// Mailbox_List is a list of Mailbox. -type Mailbox_List = capnp.CapList[Mailbox] - -// NewMailbox creates a new list of Mailbox. -func NewMailbox_List(s *capnp.Segment, sz int32) (Mailbox_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[Mailbox](l), err -} - -type Mailbox_newBuffer_Params capnp.Struct - -// Mailbox_newBuffer_Params_TypeID is the unique identifier for the type Mailbox_newBuffer_Params. -const Mailbox_newBuffer_Params_TypeID = 0xbd4ee25c19bae4a0 - -func NewMailbox_newBuffer_Params(s *capnp.Segment) (Mailbox_newBuffer_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Mailbox_newBuffer_Params(st), err -} - -func NewRootMailbox_newBuffer_Params(s *capnp.Segment) (Mailbox_newBuffer_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Mailbox_newBuffer_Params(st), err -} - -func ReadRootMailbox_newBuffer_Params(msg *capnp.Message) (Mailbox_newBuffer_Params, error) { - root, err := msg.Root() - return Mailbox_newBuffer_Params(root.Struct()), err -} - -func (s Mailbox_newBuffer_Params) String() string { - str, _ := text.Marshal(0xbd4ee25c19bae4a0, capnp.Struct(s)) - return str -} - -func (s Mailbox_newBuffer_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Mailbox_newBuffer_Params) DecodeFromPtr(p capnp.Ptr) Mailbox_newBuffer_Params { - return Mailbox_newBuffer_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Mailbox_newBuffer_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Mailbox_newBuffer_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Mailbox_newBuffer_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Mailbox_newBuffer_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} - -// Mailbox_newBuffer_Params_List is a list of Mailbox_newBuffer_Params. -type Mailbox_newBuffer_Params_List = capnp.StructList[Mailbox_newBuffer_Params] - -// NewMailbox_newBuffer_Params creates a new list of Mailbox_newBuffer_Params. -func NewMailbox_newBuffer_Params_List(s *capnp.Segment, sz int32) (Mailbox_newBuffer_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) - return capnp.StructList[Mailbox_newBuffer_Params](l), err -} - -// Mailbox_newBuffer_Params_Future is a wrapper for a Mailbox_newBuffer_Params promised by a client call. -type Mailbox_newBuffer_Params_Future struct{ *capnp.Future } - -func (f Mailbox_newBuffer_Params_Future) Struct() (Mailbox_newBuffer_Params, error) { - p, err := f.Future.Ptr() - return Mailbox_newBuffer_Params(p.Struct()), err -} - -type Mailbox_newBuffer_Results capnp.Struct - -// Mailbox_newBuffer_Results_TypeID is the unique identifier for the type Mailbox_newBuffer_Results. -const Mailbox_newBuffer_Results_TypeID = 0x846071f72bde13bb - -func NewMailbox_newBuffer_Results(s *capnp.Segment) (Mailbox_newBuffer_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Mailbox_newBuffer_Results(st), err -} - -func NewRootMailbox_newBuffer_Results(s *capnp.Segment) (Mailbox_newBuffer_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Mailbox_newBuffer_Results(st), err -} - -func ReadRootMailbox_newBuffer_Results(msg *capnp.Message) (Mailbox_newBuffer_Results, error) { - root, err := msg.Root() - return Mailbox_newBuffer_Results(root.Struct()), err -} - -func (s Mailbox_newBuffer_Results) String() string { - str, _ := text.Marshal(0x846071f72bde13bb, capnp.Struct(s)) - return str -} - -func (s Mailbox_newBuffer_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Mailbox_newBuffer_Results) DecodeFromPtr(p capnp.Ptr) Mailbox_newBuffer_Results { - return Mailbox_newBuffer_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Mailbox_newBuffer_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Mailbox_newBuffer_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Mailbox_newBuffer_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Mailbox_newBuffer_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Mailbox_newBuffer_Results) Buffer() Buffer { - p, _ := capnp.Struct(s).Ptr(0) - return Buffer(p.Interface().Client()) -} - -func (s Mailbox_newBuffer_Results) HasBuffer() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Mailbox_newBuffer_Results) SetBuffer(v Buffer) error { - if !v.IsValid() { - return capnp.Struct(s).SetPtr(0, capnp.Ptr{}) - } - seg := s.Segment() - in := capnp.NewInterface(seg, seg.Message().CapTable().Add(capnp.Client(v))) - return capnp.Struct(s).SetPtr(0, in.ToPtr()) -} - -// Mailbox_newBuffer_Results_List is a list of Mailbox_newBuffer_Results. -type Mailbox_newBuffer_Results_List = capnp.StructList[Mailbox_newBuffer_Results] - -// NewMailbox_newBuffer_Results creates a new list of Mailbox_newBuffer_Results. -func NewMailbox_newBuffer_Results_List(s *capnp.Segment, sz int32) (Mailbox_newBuffer_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Mailbox_newBuffer_Results](l), err -} - -// Mailbox_newBuffer_Results_Future is a wrapper for a Mailbox_newBuffer_Results promised by a client call. -type Mailbox_newBuffer_Results_Future struct{ *capnp.Future } - -func (f Mailbox_newBuffer_Results_Future) Struct() (Mailbox_newBuffer_Results, error) { - p, err := f.Future.Ptr() - return Mailbox_newBuffer_Results(p.Struct()), err -} -func (p Mailbox_newBuffer_Results_Future) Buffer() Buffer { - return Buffer(p.Future.Field(0, nil).Client()) -} - -type Buffer capnp.Client - -// Buffer_TypeID is the unique identifier for the type Buffer. -const Buffer_TypeID = 0xc385438ec56c4e58 - -func (c Buffer) Write(ctx context.Context, params func(Buffer_write_Params) error) (Buffer_write_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 0, - InterfaceName: "shell.capnp:Buffer", - MethodName: "write", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} - s.PlaceArgs = func(s capnp.Struct) error { return params(Buffer_write_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Buffer_write_Results_Future{Future: ans.Future()}, release - -} - -func (c Buffer) WriteString(ctx context.Context, params func(Buffer_writeString_Params) error) (Buffer_writeString_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 1, - InterfaceName: "shell.capnp:Buffer", - MethodName: "writeString", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} - s.PlaceArgs = func(s capnp.Struct) error { return params(Buffer_writeString_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Buffer_writeString_Results_Future{Future: ans.Future()}, release - -} - -func (c Buffer) Read(ctx context.Context, params func(Buffer_read_Params) error) (Buffer_read_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 2, - InterfaceName: "shell.capnp:Buffer", - MethodName: "read", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 8, PointerCount: 0} - s.PlaceArgs = func(s capnp.Struct) error { return params(Buffer_read_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Buffer_read_Results_Future{Future: ans.Future()}, release - -} - -func (c Buffer) Flush(ctx context.Context, params func(Buffer_flush_Params) error) (Buffer_flush_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 3, - InterfaceName: "shell.capnp:Buffer", - MethodName: "flush", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} - s.PlaceArgs = func(s capnp.Struct) error { return params(Buffer_flush_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Buffer_flush_Results_Future{Future: ans.Future()}, release - -} - -func (c Buffer) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c Buffer) String() string { - return "Buffer(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c Buffer) AddRef() Buffer { - return Buffer(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c Buffer) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c Buffer) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c Buffer) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (Buffer) DecodeFromPtr(p capnp.Ptr) Buffer { - return Buffer(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c Buffer) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c Buffer) IsSame(other Buffer) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c Buffer) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c Buffer) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A Buffer_Server is a Buffer with a local implementation. -type Buffer_Server interface { - Write(context.Context, Buffer_write) error - - WriteString(context.Context, Buffer_writeString) error - - Read(context.Context, Buffer_read) error - - Flush(context.Context, Buffer_flush) error -} - -// Buffer_NewServer creates a new Server from an implementation of Buffer_Server. -func Buffer_NewServer(s Buffer_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(Buffer_Methods(nil, s), s, c) -} - -// Buffer_ServerToClient creates a new Client from an implementation of Buffer_Server. -// The caller is responsible for calling Release on the returned Client. -func Buffer_ServerToClient(s Buffer_Server) Buffer { - return Buffer(capnp.NewClient(Buffer_NewServer(s))) -} - -// Buffer_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func Buffer_Methods(methods []server.Method, s Buffer_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 4) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 0, - InterfaceName: "shell.capnp:Buffer", - MethodName: "write", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Write(ctx, Buffer_write{call}) - }, - }) - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 1, - InterfaceName: "shell.capnp:Buffer", - MethodName: "writeString", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.WriteString(ctx, Buffer_writeString{call}) - }, - }) - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 2, - InterfaceName: "shell.capnp:Buffer", - MethodName: "read", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Read(ctx, Buffer_read{call}) - }, - }) - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xc385438ec56c4e58, - MethodID: 3, - InterfaceName: "shell.capnp:Buffer", - MethodName: "flush", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Flush(ctx, Buffer_flush{call}) - }, - }) - - return methods -} - -// Buffer_write holds the state for a server call to Buffer.write. -// See server.Call for documentation. -type Buffer_write struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Buffer_write) Args() Buffer_write_Params { - return Buffer_write_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Buffer_write) AllocResults() (Buffer_write_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_write_Results(r), err -} - -// Buffer_writeString holds the state for a server call to Buffer.writeString. -// See server.Call for documentation. -type Buffer_writeString struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Buffer_writeString) Args() Buffer_writeString_Params { - return Buffer_writeString_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Buffer_writeString) AllocResults() (Buffer_writeString_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_writeString_Results(r), err -} - -// Buffer_read holds the state for a server call to Buffer.read. -// See server.Call for documentation. -type Buffer_read struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Buffer_read) Args() Buffer_read_Params { - return Buffer_read_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Buffer_read) AllocResults() (Buffer_read_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 2}) - return Buffer_read_Results(r), err -} - -// Buffer_flush holds the state for a server call to Buffer.flush. -// See server.Call for documentation. -type Buffer_flush struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Buffer_flush) Args() Buffer_flush_Params { - return Buffer_flush_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Buffer_flush) AllocResults() (Buffer_flush_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Buffer_flush_Results(r), err -} - -// Buffer_List is a list of Buffer. -type Buffer_List = capnp.CapList[Buffer] - -// NewBuffer creates a new list of Buffer. -func NewBuffer_List(s *capnp.Segment, sz int32) (Buffer_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[Buffer](l), err -} - -type Buffer_Status capnp.Struct -type Buffer_Status_Which uint16 - -const ( - Buffer_Status_Which_ok Buffer_Status_Which = 0 - Buffer_Status_Which_eof Buffer_Status_Which = 1 - Buffer_Status_Which_error Buffer_Status_Which = 2 -) - -func (w Buffer_Status_Which) String() string { - const s = "okeoferror" - switch w { - case Buffer_Status_Which_ok: - return s[0:2] - case Buffer_Status_Which_eof: - return s[2:5] - case Buffer_Status_Which_error: - return s[5:10] - - } - return "Buffer_Status_Which(" + strconv.FormatUint(uint64(w), 10) + ")" -} - -// Buffer_Status_TypeID is the unique identifier for the type Buffer_Status. -const Buffer_Status_TypeID = 0xb391d0fa84c21d71 - -func NewBuffer_Status(s *capnp.Segment) (Buffer_Status, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) - return Buffer_Status(st), err -} - -func NewRootBuffer_Status(s *capnp.Segment) (Buffer_Status, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}) - return Buffer_Status(st), err -} - -func ReadRootBuffer_Status(msg *capnp.Message) (Buffer_Status, error) { - root, err := msg.Root() - return Buffer_Status(root.Struct()), err -} - -func (s Buffer_Status) String() string { - str, _ := text.Marshal(0xb391d0fa84c21d71, capnp.Struct(s)) - return str -} - -func (s Buffer_Status) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_Status) DecodeFromPtr(p capnp.Ptr) Buffer_Status { - return Buffer_Status(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_Status) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} - -func (s Buffer_Status) Which() Buffer_Status_Which { - return Buffer_Status_Which(capnp.Struct(s).Uint16(0)) -} -func (s Buffer_Status) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_Status) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_Status) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_Status) SetOk() { - capnp.Struct(s).SetUint16(0, 0) - -} - -func (s Buffer_Status) SetEof() { - capnp.Struct(s).SetUint16(0, 1) - -} - -func (s Buffer_Status) Error() (string, error) { - if capnp.Struct(s).Uint16(0) != 2 { - panic("Which() != error") - } - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s Buffer_Status) HasError() bool { - if capnp.Struct(s).Uint16(0) != 2 { - return false - } - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_Status) ErrorBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s Buffer_Status) SetError(v string) error { - capnp.Struct(s).SetUint16(0, 2) - return capnp.Struct(s).SetText(0, v) -} - -// Buffer_Status_List is a list of Buffer_Status. -type Buffer_Status_List = capnp.StructList[Buffer_Status] - -// NewBuffer_Status creates a new list of Buffer_Status. -func NewBuffer_Status_List(s *capnp.Segment, sz int32) (Buffer_Status_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 1}, sz) - return capnp.StructList[Buffer_Status](l), err -} - -// Buffer_Status_Future is a wrapper for a Buffer_Status promised by a client call. -type Buffer_Status_Future struct{ *capnp.Future } - -func (f Buffer_Status_Future) Struct() (Buffer_Status, error) { - p, err := f.Future.Ptr() - return Buffer_Status(p.Struct()), err -} - -type Buffer_write_Params capnp.Struct - -// Buffer_write_Params_TypeID is the unique identifier for the type Buffer_write_Params. -const Buffer_write_Params_TypeID = 0xac24ea5bb35da196 - -func NewBuffer_write_Params(s *capnp.Segment) (Buffer_write_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_write_Params(st), err -} - -func NewRootBuffer_write_Params(s *capnp.Segment) (Buffer_write_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_write_Params(st), err -} - -func ReadRootBuffer_write_Params(msg *capnp.Message) (Buffer_write_Params, error) { - root, err := msg.Root() - return Buffer_write_Params(root.Struct()), err -} - -func (s Buffer_write_Params) String() string { - str, _ := text.Marshal(0xac24ea5bb35da196, capnp.Struct(s)) - return str -} - -func (s Buffer_write_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_write_Params) DecodeFromPtr(p capnp.Ptr) Buffer_write_Params { - return Buffer_write_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_write_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_write_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_write_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_write_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_write_Params) Input() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return []byte(p.Data()), err -} - -func (s Buffer_write_Params) HasInput() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_write_Params) SetInput(v []byte) error { - return capnp.Struct(s).SetData(0, v) -} - -// Buffer_write_Params_List is a list of Buffer_write_Params. -type Buffer_write_Params_List = capnp.StructList[Buffer_write_Params] - -// NewBuffer_write_Params creates a new list of Buffer_write_Params. -func NewBuffer_write_Params_List(s *capnp.Segment, sz int32) (Buffer_write_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Buffer_write_Params](l), err -} - -// Buffer_write_Params_Future is a wrapper for a Buffer_write_Params promised by a client call. -type Buffer_write_Params_Future struct{ *capnp.Future } - -func (f Buffer_write_Params_Future) Struct() (Buffer_write_Params, error) { - p, err := f.Future.Ptr() - return Buffer_write_Params(p.Struct()), err -} - -type Buffer_write_Results capnp.Struct - -// Buffer_write_Results_TypeID is the unique identifier for the type Buffer_write_Results. -const Buffer_write_Results_TypeID = 0xd15c12b2b2c5d94e - -func NewBuffer_write_Results(s *capnp.Segment) (Buffer_write_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_write_Results(st), err -} - -func NewRootBuffer_write_Results(s *capnp.Segment) (Buffer_write_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_write_Results(st), err -} - -func ReadRootBuffer_write_Results(msg *capnp.Message) (Buffer_write_Results, error) { - root, err := msg.Root() - return Buffer_write_Results(root.Struct()), err -} - -func (s Buffer_write_Results) String() string { - str, _ := text.Marshal(0xd15c12b2b2c5d94e, capnp.Struct(s)) - return str -} - -func (s Buffer_write_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_write_Results) DecodeFromPtr(p capnp.Ptr) Buffer_write_Results { - return Buffer_write_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_write_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_write_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_write_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_write_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_write_Results) Status() (Buffer_Status, error) { - p, err := capnp.Struct(s).Ptr(0) - return Buffer_Status(p.Struct()), err -} - -func (s Buffer_write_Results) HasStatus() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_write_Results) SetStatus(v Buffer_Status) error { - return capnp.Struct(s).SetPtr(0, capnp.Struct(v).ToPtr()) -} - -// NewStatus sets the status field to a newly -// allocated Buffer_Status struct, preferring placement in s's segment. -func (s Buffer_write_Results) NewStatus() (Buffer_Status, error) { - ss, err := NewBuffer_Status(capnp.Struct(s).Segment()) - if err != nil { - return Buffer_Status{}, err - } - err = capnp.Struct(s).SetPtr(0, capnp.Struct(ss).ToPtr()) - return ss, err -} - -// Buffer_write_Results_List is a list of Buffer_write_Results. -type Buffer_write_Results_List = capnp.StructList[Buffer_write_Results] - -// NewBuffer_write_Results creates a new list of Buffer_write_Results. -func NewBuffer_write_Results_List(s *capnp.Segment, sz int32) (Buffer_write_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Buffer_write_Results](l), err -} - -// Buffer_write_Results_Future is a wrapper for a Buffer_write_Results promised by a client call. -type Buffer_write_Results_Future struct{ *capnp.Future } - -func (f Buffer_write_Results_Future) Struct() (Buffer_write_Results, error) { - p, err := f.Future.Ptr() - return Buffer_write_Results(p.Struct()), err -} -func (p Buffer_write_Results_Future) Status() Buffer_Status_Future { - return Buffer_Status_Future{Future: p.Future.Field(0, nil)} -} - -type Buffer_writeString_Params capnp.Struct - -// Buffer_writeString_Params_TypeID is the unique identifier for the type Buffer_writeString_Params. -const Buffer_writeString_Params_TypeID = 0xeb0e5010d1b59026 - -func NewBuffer_writeString_Params(s *capnp.Segment) (Buffer_writeString_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_writeString_Params(st), err -} - -func NewRootBuffer_writeString_Params(s *capnp.Segment) (Buffer_writeString_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_writeString_Params(st), err -} - -func ReadRootBuffer_writeString_Params(msg *capnp.Message) (Buffer_writeString_Params, error) { - root, err := msg.Root() - return Buffer_writeString_Params(root.Struct()), err -} - -func (s Buffer_writeString_Params) String() string { - str, _ := text.Marshal(0xeb0e5010d1b59026, capnp.Struct(s)) - return str -} - -func (s Buffer_writeString_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_writeString_Params) DecodeFromPtr(p capnp.Ptr) Buffer_writeString_Params { - return Buffer_writeString_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_writeString_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_writeString_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_writeString_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_writeString_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_writeString_Params) Input() (string, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s Buffer_writeString_Params) HasInput() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_writeString_Params) InputBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s Buffer_writeString_Params) SetInput(v string) error { - return capnp.Struct(s).SetText(0, v) -} - -// Buffer_writeString_Params_List is a list of Buffer_writeString_Params. -type Buffer_writeString_Params_List = capnp.StructList[Buffer_writeString_Params] - -// NewBuffer_writeString_Params creates a new list of Buffer_writeString_Params. -func NewBuffer_writeString_Params_List(s *capnp.Segment, sz int32) (Buffer_writeString_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Buffer_writeString_Params](l), err -} - -// Buffer_writeString_Params_Future is a wrapper for a Buffer_writeString_Params promised by a client call. -type Buffer_writeString_Params_Future struct{ *capnp.Future } - -func (f Buffer_writeString_Params_Future) Struct() (Buffer_writeString_Params, error) { - p, err := f.Future.Ptr() - return Buffer_writeString_Params(p.Struct()), err -} - -type Buffer_writeString_Results capnp.Struct - -// Buffer_writeString_Results_TypeID is the unique identifier for the type Buffer_writeString_Results. -const Buffer_writeString_Results_TypeID = 0xfae584cad6e82c9b - -func NewBuffer_writeString_Results(s *capnp.Segment) (Buffer_writeString_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_writeString_Results(st), err -} - -func NewRootBuffer_writeString_Results(s *capnp.Segment) (Buffer_writeString_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Buffer_writeString_Results(st), err -} - -func ReadRootBuffer_writeString_Results(msg *capnp.Message) (Buffer_writeString_Results, error) { - root, err := msg.Root() - return Buffer_writeString_Results(root.Struct()), err -} - -func (s Buffer_writeString_Results) String() string { - str, _ := text.Marshal(0xfae584cad6e82c9b, capnp.Struct(s)) - return str -} - -func (s Buffer_writeString_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_writeString_Results) DecodeFromPtr(p capnp.Ptr) Buffer_writeString_Results { - return Buffer_writeString_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_writeString_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_writeString_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_writeString_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_writeString_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_writeString_Results) Status() (Buffer_Status, error) { - p, err := capnp.Struct(s).Ptr(0) - return Buffer_Status(p.Struct()), err -} - -func (s Buffer_writeString_Results) HasStatus() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_writeString_Results) SetStatus(v Buffer_Status) error { - return capnp.Struct(s).SetPtr(0, capnp.Struct(v).ToPtr()) -} - -// NewStatus sets the status field to a newly -// allocated Buffer_Status struct, preferring placement in s's segment. -func (s Buffer_writeString_Results) NewStatus() (Buffer_Status, error) { - ss, err := NewBuffer_Status(capnp.Struct(s).Segment()) - if err != nil { - return Buffer_Status{}, err - } - err = capnp.Struct(s).SetPtr(0, capnp.Struct(ss).ToPtr()) - return ss, err -} - -// Buffer_writeString_Results_List is a list of Buffer_writeString_Results. -type Buffer_writeString_Results_List = capnp.StructList[Buffer_writeString_Results] - -// NewBuffer_writeString_Results creates a new list of Buffer_writeString_Results. -func NewBuffer_writeString_Results_List(s *capnp.Segment, sz int32) (Buffer_writeString_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Buffer_writeString_Results](l), err -} - -// Buffer_writeString_Results_Future is a wrapper for a Buffer_writeString_Results promised by a client call. -type Buffer_writeString_Results_Future struct{ *capnp.Future } - -func (f Buffer_writeString_Results_Future) Struct() (Buffer_writeString_Results, error) { - p, err := f.Future.Ptr() - return Buffer_writeString_Results(p.Struct()), err -} -func (p Buffer_writeString_Results_Future) Status() Buffer_Status_Future { - return Buffer_Status_Future{Future: p.Future.Field(0, nil)} -} - -type Buffer_read_Params capnp.Struct - -// Buffer_read_Params_TypeID is the unique identifier for the type Buffer_read_Params. -const Buffer_read_Params_TypeID = 0x988f94f380d5b032 - -func NewBuffer_read_Params(s *capnp.Segment) (Buffer_read_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}) - return Buffer_read_Params(st), err -} - -func NewRootBuffer_read_Params(s *capnp.Segment) (Buffer_read_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}) - return Buffer_read_Params(st), err -} - -func ReadRootBuffer_read_Params(msg *capnp.Message) (Buffer_read_Params, error) { - root, err := msg.Root() - return Buffer_read_Params(root.Struct()), err -} - -func (s Buffer_read_Params) String() string { - str, _ := text.Marshal(0x988f94f380d5b032, capnp.Struct(s)) - return str -} - -func (s Buffer_read_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_read_Params) DecodeFromPtr(p capnp.Ptr) Buffer_read_Params { - return Buffer_read_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_read_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_read_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_read_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_read_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_read_Params) Count() uint64 { - return capnp.Struct(s).Uint64(0) -} - -func (s Buffer_read_Params) SetCount(v uint64) { - capnp.Struct(s).SetUint64(0, v) -} - -// Buffer_read_Params_List is a list of Buffer_read_Params. -type Buffer_read_Params_List = capnp.StructList[Buffer_read_Params] - -// NewBuffer_read_Params creates a new list of Buffer_read_Params. -func NewBuffer_read_Params_List(s *capnp.Segment, sz int32) (Buffer_read_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 8, PointerCount: 0}, sz) - return capnp.StructList[Buffer_read_Params](l), err -} - -// Buffer_read_Params_Future is a wrapper for a Buffer_read_Params promised by a client call. -type Buffer_read_Params_Future struct{ *capnp.Future } - -func (f Buffer_read_Params_Future) Struct() (Buffer_read_Params, error) { - p, err := f.Future.Ptr() - return Buffer_read_Params(p.Struct()), err -} - -type Buffer_read_Results capnp.Struct - -// Buffer_read_Results_TypeID is the unique identifier for the type Buffer_read_Results. -const Buffer_read_Results_TypeID = 0xd97c0d57b5fd9e5d - -func NewBuffer_read_Results(s *capnp.Segment) (Buffer_read_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) - return Buffer_read_Results(st), err -} - -func NewRootBuffer_read_Results(s *capnp.Segment) (Buffer_read_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}) - return Buffer_read_Results(st), err -} - -func ReadRootBuffer_read_Results(msg *capnp.Message) (Buffer_read_Results, error) { - root, err := msg.Root() - return Buffer_read_Results(root.Struct()), err -} - -func (s Buffer_read_Results) String() string { - str, _ := text.Marshal(0xd97c0d57b5fd9e5d, capnp.Struct(s)) - return str -} - -func (s Buffer_read_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_read_Results) DecodeFromPtr(p capnp.Ptr) Buffer_read_Results { - return Buffer_read_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_read_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_read_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_read_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_read_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Buffer_read_Results) Output() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return []byte(p.Data()), err -} - -func (s Buffer_read_Results) HasOutput() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Buffer_read_Results) SetOutput(v []byte) error { - return capnp.Struct(s).SetData(0, v) -} - -func (s Buffer_read_Results) Status() (Buffer_Status, error) { - p, err := capnp.Struct(s).Ptr(1) - return Buffer_Status(p.Struct()), err -} - -func (s Buffer_read_Results) HasStatus() bool { - return capnp.Struct(s).HasPtr(1) -} - -func (s Buffer_read_Results) SetStatus(v Buffer_Status) error { - return capnp.Struct(s).SetPtr(1, capnp.Struct(v).ToPtr()) -} - -// NewStatus sets the status field to a newly -// allocated Buffer_Status struct, preferring placement in s's segment. -func (s Buffer_read_Results) NewStatus() (Buffer_Status, error) { - ss, err := NewBuffer_Status(capnp.Struct(s).Segment()) - if err != nil { - return Buffer_Status{}, err - } - err = capnp.Struct(s).SetPtr(1, capnp.Struct(ss).ToPtr()) - return ss, err -} - -// Buffer_read_Results_List is a list of Buffer_read_Results. -type Buffer_read_Results_List = capnp.StructList[Buffer_read_Results] - -// NewBuffer_read_Results creates a new list of Buffer_read_Results. -func NewBuffer_read_Results_List(s *capnp.Segment, sz int32) (Buffer_read_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 2}, sz) - return capnp.StructList[Buffer_read_Results](l), err -} - -// Buffer_read_Results_Future is a wrapper for a Buffer_read_Results promised by a client call. -type Buffer_read_Results_Future struct{ *capnp.Future } - -func (f Buffer_read_Results_Future) Struct() (Buffer_read_Results, error) { - p, err := f.Future.Ptr() - return Buffer_read_Results(p.Struct()), err -} -func (p Buffer_read_Results_Future) Status() Buffer_Status_Future { - return Buffer_Status_Future{Future: p.Future.Field(1, nil)} -} - -type Buffer_flush_Params capnp.Struct - -// Buffer_flush_Params_TypeID is the unique identifier for the type Buffer_flush_Params. -const Buffer_flush_Params_TypeID = 0xe6195e5a8613fc66 - -func NewBuffer_flush_Params(s *capnp.Segment) (Buffer_flush_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Buffer_flush_Params(st), err -} - -func NewRootBuffer_flush_Params(s *capnp.Segment) (Buffer_flush_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Buffer_flush_Params(st), err -} - -func ReadRootBuffer_flush_Params(msg *capnp.Message) (Buffer_flush_Params, error) { - root, err := msg.Root() - return Buffer_flush_Params(root.Struct()), err -} - -func (s Buffer_flush_Params) String() string { - str, _ := text.Marshal(0xe6195e5a8613fc66, capnp.Struct(s)) - return str -} - -func (s Buffer_flush_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_flush_Params) DecodeFromPtr(p capnp.Ptr) Buffer_flush_Params { - return Buffer_flush_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_flush_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_flush_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_flush_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_flush_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} - -// Buffer_flush_Params_List is a list of Buffer_flush_Params. -type Buffer_flush_Params_List = capnp.StructList[Buffer_flush_Params] - -// NewBuffer_flush_Params creates a new list of Buffer_flush_Params. -func NewBuffer_flush_Params_List(s *capnp.Segment, sz int32) (Buffer_flush_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) - return capnp.StructList[Buffer_flush_Params](l), err -} - -// Buffer_flush_Params_Future is a wrapper for a Buffer_flush_Params promised by a client call. -type Buffer_flush_Params_Future struct{ *capnp.Future } - -func (f Buffer_flush_Params_Future) Struct() (Buffer_flush_Params, error) { - p, err := f.Future.Ptr() - return Buffer_flush_Params(p.Struct()), err -} - -type Buffer_flush_Results capnp.Struct - -// Buffer_flush_Results_TypeID is the unique identifier for the type Buffer_flush_Results. -const Buffer_flush_Results_TypeID = 0xa71b7c75f8fd8005 - -func NewBuffer_flush_Results(s *capnp.Segment) (Buffer_flush_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Buffer_flush_Results(st), err -} - -func NewRootBuffer_flush_Results(s *capnp.Segment) (Buffer_flush_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Buffer_flush_Results(st), err -} - -func ReadRootBuffer_flush_Results(msg *capnp.Message) (Buffer_flush_Results, error) { - root, err := msg.Root() - return Buffer_flush_Results(root.Struct()), err -} - -func (s Buffer_flush_Results) String() string { - str, _ := text.Marshal(0xa71b7c75f8fd8005, capnp.Struct(s)) - return str -} - -func (s Buffer_flush_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Buffer_flush_Results) DecodeFromPtr(p capnp.Ptr) Buffer_flush_Results { - return Buffer_flush_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Buffer_flush_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Buffer_flush_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Buffer_flush_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Buffer_flush_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} - -// Buffer_flush_Results_List is a list of Buffer_flush_Results. -type Buffer_flush_Results_List = capnp.StructList[Buffer_flush_Results] - -// NewBuffer_flush_Results creates a new list of Buffer_flush_Results. -func NewBuffer_flush_Results_List(s *capnp.Segment, sz int32) (Buffer_flush_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) - return capnp.StructList[Buffer_flush_Results](l), err -} - -// Buffer_flush_Results_Future is a wrapper for a Buffer_flush_Results promised by a client call. -type Buffer_flush_Results_Future struct{ *capnp.Future } - -func (f Buffer_flush_Results_Future) Struct() (Buffer_flush_Results, error) { - p, err := f.Future.Ptr() - return Buffer_flush_Results(p.Struct()), err -} - -const schema_ead48f650c32a806 = "x\xda\x8cUQh\x1cU\x14\xbd\xe7\xbd\x99\x9d\xa6\xec" + - "\xda<'*U\xca\xa2,\xa2\xd5\x84v\xe3\x87\x06t" + - "\xd7\xea\xd2\xaam\xd8I\x04Mm\xc5I2\xdb,n" + - "w6\xb3\xb3\xa6B\xc5\x06R\xc5\x0fm\x03\"\xa2 " + - "U\x10-\x08\xb5)\x15\xb4\xe4\xc7\x92\x1f\xbf\x1a\xb5B" + - "\x10\xfc\x10\xb5\xb5\xfd\x10\xf1C\x09\xc6\x91\xf7&\xb3;" + - "\xc9&\xa4_\x09\xfb\xde\x9ew\xce\xb9\xe7\xdc\xdd\x91g" + - "ymg\xeaJ\x82\x98U\xd4\x13\xc1\x05\xf3\xa7\xfb\xfe" + - "\x1e\x7fa\x8aD\x17\x88t\x18D\xbdC<\x0b\x82i" + - "\xf3\x1c!\xc8~~\xf9\xd8_o\x9fx\x97,\x13 " + - "\xd2\x0c\"s\x92/\x12\xcc\xe3\xea\\?\xb6\xf4O\xe3" + - "\xe8\x1d\x9f\x900\x97\x8f{?\xe6\x9bAZ\xf0\xce\x87" + - "\x07\xcf=w-\xf3Yx\x12B\xbf\xc9\x99\x84\x9eV" + - "_\x1d\xdf\xf6\xf5\xd4\xe2\xa5\xe9s\x0a:x\xb6\xbf2" + - "\xf7\xd6c\xc7/R\x01\x06'2\xcf\xf2o\xcdY." + - "\x9f\xfb\x92\x9f!\x045q\x05\x8f\\?s\x81\xc4\xad" + - "M8K\xeb\x93pC\x9a\x84;\xf9\xfa\x17\x0f~w" + - "\xf1\xa9Y\x12I\x1e$>\xcd&\x9d\x13\xdf_#\x82" + - "\xf9\xb2\xf6\x919\xa9\x88\xbf\xa2\xed6O\xcb\xff\x82S" + - "\xbf|\xb5\xf5\xc0\xcf\xfd\xb3\xa1nE{Z\xdb.i" + - "7y\xb4\xc3\xbcgNj\xb7\x11\xf5\xbe\xa1\x190\xbb" + - "u\x89\xd3\xbf073s\xf3\x81\xf9\xb8\xc8[\xf4\xcd" + - "\x92\xd56]\xb2\xda\xbf\xe7R\xfd\xd4\x88\xb9\x10\xa7\xfd" + - "\xb0\xae\x0c.\xa8\x0b\x07?X:\xffL\xea\xe8\xc22" + - "\x02\x93\x17\x1c]\xd9T\xd6'\x08A\xe9_\xf3\xb5\xfd" + - "\xcfo\xfd-\xe6\xf0\x9c<\xd7\x82\xbbO\x9e\x9f\xef," + - "\xdet=\xfe\xf8\xe9\x10\xfb\xac\xc2\xbe\x8a\x8e\x9d?\x1a" + - "\xc6\x1fmZ\xe6\xf5\x19sA\x0a0/\xeb\xbbM$" + - "\xa4\x94\xf7\xef\xbf\xfa\xc37S\xbf.\xc6\xd1~\xd7\x95" + - "\xc1\x7f\xea9\xea\x0e\xeacN\xa5\xd23b\xf3Z\xb5" + - "\xd6\xb7\xcf.W\x86\xdd#=UgbW\xa3Tr" + - "\xbc\xcc\x80\x93\xae7*~\xdd\xd2\xb8F\xa4\x81H\xa4" + - "\xfa\x88\xacM\x1cV\x17CnX]\x83h9L\x80" + - " 4q\x99\xc4\x0d\xc1z<\xc7\x1e\xcd\xe4\x8a\xb6g" + - "\x1f^\x01\x98m\x01\xa6G\xdcF\xd5G\x071t\xc4" + - "Px\x0c\xa5Ti\xd4\xc72\x03\x8e\xe2E\xb4\xe6\x9d" + - "\x09\xaf\xec;\x99\xf0%Z\xef\xa9r\xb5\xd6\xf0\x91\"" + - "\x86\xd4:\x84\x07}\xdbo\xa0^\x04\xac$\xd7\x92A" + - "\xa0@\x0a\xb7\x13Yy\x0ek/C\x0a\xff\x05]\xb2" + - "C\xe2\x89\xbb\x88\xac\xc79\xac\"C\x8a-\x05]`" + - "Db\x9f|p\x0f\x87\xf54\x03w_\xa4\x84\xe1\xb8" + - "%J\xa4\x1d\xcfs=$\x89!\xb9Zf\xe1%\xa7" + - "\xea\xefu\xddZ\x8f=:\xfa\xe8\x88\xefz\x99\x81\x9c" + - "\xd36\x85]-%\xaf\x1e\x0e\xe7\x06\xd1\xea\xcb\xaa1" + - " \x1a\xaf1\xec\x1e\x91z4\xae\xc7\x1a\x83he\x08" + - "1@Lt\x18A\x14\x01\x82\x97G\x11\xd8((E" + - "{\x8b4{\xe5\x83\xf2\x8c;\x9e\xa5!\xb6\x18\x04\xfa" + - "r\xca\xd8\xba\xd5\xa9HD;\x05Q\xef\xc4x\x96\x98" + - "p\x0c\xa0\xd9\x06DA\x16C\xc3\xc4\x84e\x805\xb7" + - "\x18\xa2\xb6\x89\xc2vb\xe2!\x03\xbc\xd9/D\xabL" + - "tK\xcc;\x8d\xb4\x8aF\x1e\x81\xfa;\xe8{d\x94" + - "\xab\x87\xf2\xd8\"\xc3\x99GZ\xa5k\x0d\xc5+\x82\xd5" + - "\x0c\xdfz\xb5\xa8+}\xe8l\xa9&\xa0\xf3\x06&]" + - "L\xb7\xb5#>\xe81\xbb:Zq\xd6\x09N\xbce" + - "\x11CkS\x13\xe8^I0\xc3a\xed`\x10@\x98" + - "\xdan\xf9\xe1=\x1c\xd6\x03\x0c9\xb7\xe1\xc7\x1a\xb1\xb1" + - "\x08\xa3\xad\x95\xcb\x8d\x8b.\xac6n\xd0\xf7\xca\xd5C" + - "k\x89l\xef\xe5j\x89\x88\x1cK+\xcbZ\x19\x8e\x96" + - "1\xa2\x1f\x13!\x9e\x0c3\x1c\xb9JD\x1bLt\x99" + - "\xd8Z=\xbb\x91\xb1\xfe\x1f\x00\x00\xff\xff\xe7\xa9\x16\x92" - -func RegisterSchema(reg *schemas.Registry) { - reg.Register(&schemas.Schema{ - String: schema_ead48f650c32a806, - Nodes: []uint64{ - 0x846071f72bde13bb, - 0x988f94f380d5b032, - 0xa71b7c75f8fd8005, - 0xac24ea5bb35da196, - 0xb391d0fa84c21d71, - 0xbbafeb3e01e71170, - 0xbd4bc3d338b68790, - 0xbd4ee25c19bae4a0, - 0xc385438ec56c4e58, - 0xd15c12b2b2c5d94e, - 0xd91363a073d0485a, - 0xd97c0d57b5fd9e5d, - 0xe6195e5a8613fc66, - 0xeb0e5010d1b59026, - 0xef0707db310901e8, - 0xfae584cad6e82c9b, - }, - Compressed: true, - }) -} diff --git a/cmd/ww/shell/shell.go b/cmd/ww/shell/shell.go deleted file mode 100644 index d96faf0..0000000 --- a/cmd/ww/shell/shell.go +++ /dev/null @@ -1,407 +0,0 @@ -//go:generate capnp compile -I.. -I$GOPATH/src/capnproto.org/go/capnp/std -ogo shell.capnp - -package shell - -import ( - "context" - "fmt" - "io" - "os" - "os/exec" - "strings" - - "capnproto.org/go/capnp/v3/rpc" - "github.com/chzyer/readline" - "github.com/ipfs/boxo/path" - "github.com/spy16/slurp" - "github.com/spy16/slurp/builtin" - "github.com/spy16/slurp/core" - "github.com/urfave/cli/v2" - - "github.com/spy16/slurp/repl" - "github.com/wetware/go/cmd/internal/flags" - "github.com/wetware/go/system" - "github.com/wetware/go/util" -) - -var env util.IPFSEnv - -func Command() *cli.Command { - return &cli.Command{ - Name: "shell", - Before: func(c *cli.Context) error { - addr := c.String("ipfs") - if err := env.Boot(addr); err != nil { - return fmt.Errorf("failed to boot IPFS environment: %w", err) - } - return nil - }, - After: func(c *cli.Context) error { - return env.Close() - }, - Action: Main, - Flags: append([]cli.Flag{ - &cli.StringFlag{ - Name: "ipfs", - EnvVars: []string{"WW_IPFS"}, - Value: "/dns4/localhost/tcp/5001/http", - }, - &cli.StringFlag{ - Name: "command", - Aliases: []string{"c"}, - Usage: "execute a single command and exit", - }, - &cli.StringFlag{ - Name: "history-file", - Usage: "path to readline history file", - Value: "/tmp/ww-shell.tmp", - EnvVars: []string{"WW_SHELL_HISTORY"}, - }, - &cli.StringFlag{ - Name: "prompt", - Usage: "shell prompt string", - Value: "ww> ", - EnvVars: []string{"WW_SHELL_PROMPT"}, - }, - &cli.BoolFlag{ - Name: "no-banner", - Usage: "disable welcome banner", - EnvVars: []string{"WW_SHELL_NO_BANNER"}, - }, - }, flags.CapabilityFlags()...), - } -} - -func Main(c *cli.Context) error { - // Check if we're in guest mode (cell process) - if os.Getenv("WW_CELL") == "true" { - return runGuestMode(c) - } - - // Host mode: spawn guest process with ww run - return runHostMode(c) -} - -// runHostMode runs the shell in host mode, spawning a guest process -func runHostMode(c *cli.Context) error { - // Get the current executable path - execPath, err := os.Executable() - if err != nil { - return fmt.Errorf("failed to get executable path: %w", err) - } - - // Build the command to run the shell in guest mode - cmd := exec.CommandContext(c.Context, execPath, "run", "-env", "WW_CELL=true") - - // Pass through capability flags to the run command - if c.Bool("with-ipfs") { - cmd.Args = append(cmd.Args, "--with-ipfs") - } - if c.Bool("with-exec") { - cmd.Args = append(cmd.Args, "--with-exec") - } - if c.Bool("with-console") { - cmd.Args = append(cmd.Args, "--with-console") - } - if c.Bool("with-p2p") { - cmd.Args = append(cmd.Args, "--with-p2p") - } - if c.Bool("with-all") { - cmd.Args = append(cmd.Args, "--with-all") - } - - // Pass through mDNS capability to the run command - if c.Bool("with-mdns") { - cmd.Args = append(cmd.Args, "--with-mdns") - } - - // Add the executable and shell command - cmd.Args = append(cmd.Args, execPath, "--", "shell") - - // Pass through capability flags to the shell command as well - if c.Bool("with-ipfs") { - cmd.Args = append(cmd.Args, "--with-ipfs") - } - if c.Bool("with-exec") { - cmd.Args = append(cmd.Args, "--with-exec") - } - if c.Bool("with-console") { - cmd.Args = append(cmd.Args, "--with-console") - } - if c.Bool("with-mdns") { - cmd.Args = append(cmd.Args, "--with-mdns") - } - if c.Bool("with-p2p") { - cmd.Args = append(cmd.Args, "--with-p2p") - } - if c.Bool("with-all") { - cmd.Args = append(cmd.Args, "--with-all") - } - - // Pass through shell-specific flags - if command := c.String("command"); command != "" { - cmd.Args = append(cmd.Args, "-c", command) - } - if historyFile := c.String("history-file"); historyFile != "" { - cmd.Args = append(cmd.Args, "--history-file", historyFile) - } - if prompt := c.String("prompt"); prompt != "" { - cmd.Args = append(cmd.Args, "--prompt", prompt) - } - if c.Bool("no-banner") { - cmd.Args = append(cmd.Args, "--no-banner") - } - - // Set up stdio - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - - // Run the command - return cmd.Run() -} - -// runGuestMode runs the shell in guest mode (as a cell process) -func runGuestMode(c *cli.Context) error { - // Check if the bootstrap file descriptor exists - host := os.NewFile(system.BOOTSTRAP_FD, "host") - if host == nil { - return fmt.Errorf("failed to create bootstrap file descriptor") - } - - // Check if command flag is provided - if command := c.String("command"); command != "" { - // Execute single command - if err := executeCommand(c, command, host); err != nil { - return fmt.Errorf("repl error: %w", err) - } - return nil - } - - conn := rpc.NewConn(rpc.NewStreamTransport(host), &rpc.Options{ - BaseContext: func() context.Context { return c.Context }, - // BootstrapClient: export(), - }) - defer conn.Close() - - client := conn.Bootstrap(c.Context) - defer client.Release() - - f, release := system.Terminal(client).Login(c.Context, nil) - defer release() - - res, err := f.Struct() - if err != nil { - return fmt.Errorf("failed to resolve terminal login: %w", err) - } - - gs, err := NewGlobals(c) - if err != nil { - return err - } - - eval := slurp.New() - if err := eval.Bind(gs); err != nil { - return fmt.Errorf("failed to bind base globals: %w", err) - } - // Bind session-specific globals (interactive mode only) - if err := eval.Bind(NewSessionGlobals(c, &res)); err != nil { - return fmt.Errorf("failed to bind session globals: %w", err) - } - - rl, err := readline.NewEx(&readline.Config{ - Prompt: c.String("prompt"), - HistoryFile: c.String("history-file"), - AutoComplete: getCompleter(c), - InterruptPrompt: "^C", - EOFPrompt: "exit", - }) - if err != nil { - return fmt.Errorf("readline: %w", err) - } - defer rl.Close() - - // Configure banner - var banner string - if !c.Bool("no-banner") { - banner = "Welcome to Wetware Shell! Type 'help' for available commands." - } - - if err := repl.New(eval, - repl.WithBanner(banner), - repl.WithPrompts("ww ", " | "), - repl.WithPrinter(printer{out: os.Stdout}), - repl.WithReaderFactory(DefaultReaderFactory{IPFS: env.IPFS}), - repl.WithInput(lineReader{Driver: rl}, func(err error) error { - if err == nil || err == readline.ErrInterrupt { - return nil - } - return err - }), - ).Loop(c.Context); err != nil { - return fmt.Errorf("repl: %w", err) - } - return nil -} - -// executeCommand executes a single command line with a specific host file descriptor -func executeCommand(c *cli.Context, command string, host *os.File) error { - conn := rpc.NewConn(rpc.NewStreamTransport(host), &rpc.Options{ - BaseContext: func() context.Context { return c.Context }, - // BootstrapClient: export(), - }) - defer conn.Close() - - client := conn.Bootstrap(c.Context) - defer client.Release() - - f, release := system.Terminal(client).Login(c.Context, nil) - defer release() - - // Resolve the future to get the actual results - res, err := f.Struct() - if err != nil { - return fmt.Errorf("failed to resolve terminal login: %w", err) - } - - // Create base environment with analyzer and globals to it. - eval := slurp.New() - - // Bind base globals (common to both modes) - gs, err := NewGlobals(c) - if err != nil { - return fmt.Errorf("failed to bind base globals: %w", err) - } - if err := eval.Bind(gs); err != nil { - return fmt.Errorf("failed to bind base globals: %w", err) - } - - // Bind session-specific globals (including executor if --with-exec is set) - if err := eval.Bind(NewSessionGlobals(c, &res)); err != nil { - return fmt.Errorf("failed to bind session globals: %w", err) - } - - // Create a reader from the command string - commandReader := strings.NewReader(command) - - // Create a reader factory for IPFS path support - readerFactory := DefaultReaderFactory{IPFS: env.IPFS} - - // Read and evaluate the command directly - reader := readerFactory.NewReader(commandReader) - - // Read the expression - expr, err := reader.One() - if err != nil { - if err == io.EOF { - return nil // Empty command, nothing to do - } - return fmt.Errorf("failed to read command: %w", err) - } - - // Evaluate the expression - result, err := eval.Eval(expr) - if err != nil { - return fmt.Errorf("failed to evaluate command: %w", err) - } - - // Print the result if it's not nil - if result != nil { - printer := printer{out: os.Stdout} - return printer.Print(result) - } - - return nil -} - -// printer implements the repl.Printer interface for better output formatting -type printer struct { - out io.Writer -} - -func (p printer) Print(val interface{}) error { - switch v := val.(type) { - case nil: - _, err := fmt.Fprintf(p.out, "nil\n") - return err - case builtin.Bool: - _, err := fmt.Fprintf(p.out, "%t\n", bool(v)) - return err - case builtin.Int64: - _, err := fmt.Fprintf(p.out, "%d\n", int64(v)) - return err - case builtin.String: - _, err := fmt.Fprintf(p.out, "%s\n", string(v)) - return err - case builtin.Float64: - _, err := fmt.Fprintf(p.out, "%g\n", float64(v)) - return err - case builtin.Nil: - _, err := fmt.Fprintf(p.out, "nil\n") - return err - case path.Path: - _, err := fmt.Fprintf(p.out, "Path: %s\n", v.String()) - return err - default: - // For any other type, use Go's default formatting - _, err := fmt.Fprintf(p.out, "%v\n", v) - return err - } -} - -// lineReader implements the repl.Input interface using readline -type lineReader struct { - Driver *readline.Instance -} - -func (s lineReader) Readline() (string, error) { - line, err := s.Driver.Readline() - return line, err -} - -// Prompt implements the repl.Prompter interface -func (s lineReader) Prompt(p string) { - s.Driver.SetPrompt(p) -} - -// getCompleter returns a readline completer for wetware commands -func getCompleter(c *cli.Context) readline.AutoCompleter { - completers := []readline.PrefixCompleterInterface{ - readline.PcItem("help"), - readline.PcItem("version"), - readline.PcItem("println"), - readline.PcItem("print"), - readline.PcItem("send"), - readline.PcItem("system"), - readline.PcItem("callc"), - readline.PcItem("+"), - readline.PcItem("*"), - readline.PcItem("="), - readline.PcItem(">"), - readline.PcItem("<"), - readline.PcItem("nil"), - readline.PcItem("true"), - readline.PcItem("false"), - } - - if c.Bool("with-ipfs") || c.Bool("with-all") { - completers = append(completers, readline.PcItem("ipfs")) - } - if c.Bool("with-exec") || c.Bool("with-all") { - completers = append(completers, readline.PcItem("exec")) - } - - return readline.NewPrefixCompleter(completers...) -} - -// NewSessionGlobals returns additional globals for interactive mode (requires terminal connection) -func NewSessionGlobals(c *cli.Context, f *system.Terminal_login_Results) map[string]core.Any { - session := make(map[string]core.Any) - - // Add exec functionality if --with-exec flag is set - if c.Bool("with-exec") || c.Bool("with-all") { - session["exec"] = &Exec{Session: f} - } - - return session -} diff --git a/cmd/ww/shell/shell_test.go b/cmd/ww/shell/shell_test.go deleted file mode 100644 index 10e1a53..0000000 --- a/cmd/ww/shell/shell_test.go +++ /dev/null @@ -1,327 +0,0 @@ -package shell - -import ( - "bytes" - "context" - "flag" - "fmt" - "io" - "os" - "strings" - "testing" - - "github.com/spy16/slurp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "github.com/urfave/cli/v2" -) - -// createMockCLIContext creates a mock CLI context for testing -func createMockCLIContext() *cli.Context { - app := &cli.App{} - app.Flags = []cli.Flag{} - flagSet := &flag.FlagSet{} - flagSet.Bool("with-ipfs", false, "Enable IPFS capability") // Set to false for tests - flagSet.Bool("with-exec", true, "Enable exec capability") - flagSet.Bool("with-console", true, "Enable console capability") - flagSet.Bool("with-all", false, "Enable all capabilities") - flagSet.String("prompt", "ww> ", "Shell prompt") - flagSet.String("history-file", "/tmp/ww_history", "History file path") - ctx := cli.NewContext(app, flagSet, nil) - ctx.Context = context.Background() - return ctx -} - -// executeCommandForTesting executes a command without RPC setup for testing -func executeCommandForTesting(c *cli.Context, command string) error { - // Create base environment with analyzer and globals - eval := slurp.New() - - // Bind base globals (common to both modes) - gs, err := NewGlobals(c) - if err != nil { - return fmt.Errorf("failed to bind base globals: %w", err) - } - if err := eval.Bind(gs); err != nil { - return fmt.Errorf("failed to bind base globals: %w", err) - } - - // Create a reader from the command string - commandReader := strings.NewReader(command) - - // Create a reader factory for IPFS path support - readerFactory := DefaultReaderFactory{IPFS: nil} // No IPFS in tests - - // Read and evaluate the command directly - reader := readerFactory.NewReader(commandReader) - - // Read the expression - expr, err := reader.One() - if err != nil { - if err == io.EOF { - return nil // Empty command, nothing to do - } - return fmt.Errorf("failed to read command: %w", err) - } - - // Evaluate the expression - result, err := eval.Eval(expr) - if err != nil { - return fmt.Errorf("failed to evaluate command: %w", err) - } - - // Print the result if it's not nil - if result != nil { - printer := printer{out: os.Stdout} - return printer.Print(result) - } - - return nil -} - -func TestExecuteCommand(t *testing.T) { - t.Parallel() - - tests := []struct { - name string - command string - wantError bool - }{ - { - name: "simple arithmetic", - command: "(+ 1 2 3)", - wantError: false, - }, - { - name: "multiplication", - command: "(* 2 3 4)", - wantError: false, - }, - { - name: "comparison", - command: "(> 5 3)", - wantError: false, - }, - { - name: "equality", - command: "(= 5 5)", - wantError: false, - }, - { - name: "boolean values", - command: "true", - wantError: false, - }, - { - name: "nil value", - command: "nil", - wantError: false, - }, - { - name: "version", - command: "version", - wantError: false, - }, - { - name: "help function", - command: "(help)", - wantError: false, - }, - { - name: "println function", - command: "(println \"Hello World\")", - wantError: false, - }, - { - name: "nested expressions", - command: "(+ (* 2 3) 4)", - wantError: false, - }, - { - name: "invalid syntax", - command: "(+ 1 2", - wantError: true, - }, - { - name: "unknown function", - command: "(unknown-function 1 2)", - wantError: true, - }, - { - name: "empty command", - command: "", - wantError: false, - }, - { - name: "whitespace only", - command: " ", - wantError: false, - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := executeCommandForTesting(createMockCLIContext(), tt.command) - - if tt.wantError { - assert.Error(t, err, "Expected error for command: %s", tt.command) - } else { - assert.NoError(t, err, "Expected no error for command: %s", tt.command) - } - }) - } -} - -func TestExecuteCommandWithIPFS(t *testing.T) { - t.Parallel() - - // These tests will fail if IPFS is not available, but they test the structure - tests := []struct { - name string - command string - wantError bool - }{ - { - name: "ipfs function exists", - command: "ipfs", - wantError: true, // IPFS not available in test environment - }, - { - name: "ipfs cat with invalid path", - command: "(ipfs :cat \"/invalid/path\")", - wantError: true, // Should fail with invalid path - }, - { - name: "ipfs get with invalid path", - command: "(ipfs :get \"/invalid/path\")", - wantError: true, // Should fail with invalid path - }, - } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - err := executeCommandForTesting(createMockCLIContext(), tt.command) - - if tt.wantError { - assert.Error(t, err, "Expected error for command: %s", tt.command) - } else { - assert.NoError(t, err, "Expected no error for command: %s", tt.command) - } - }) - } -} - -func TestGetCompleter(t *testing.T) { - t.Parallel() - - completer := getCompleter(createMockCLIContext()) - assert.NotNil(t, completer, "Completer should not be nil") - - // Test that completer can be used without panicking - assert.NotPanics(t, func() { - completer.Do([]rune("help"), 0) // HACK: zero was a wild guess - }) -} - -func TestPrinter(t *testing.T) { - t.Parallel() - - // Create a test writer to avoid nil pointer panics - testWriter := &bytes.Buffer{} - printer := &printer{out: testWriter} - - // Test that printer can handle different types without panicking - testCases := []interface{}{ - nil, - "hello", - 42, - true, - false, - []byte("bytes"), - } - - for _, tc := range testCases { - assert.NotPanics(t, func() { - printer.Print(tc) - }, "Printer should handle type %T without panicking", tc) - } -} - -func TestCommandStructure(t *testing.T) { - t.Parallel() - - cmd := Command() - - // Test that command has expected properties - assert.Equal(t, "shell", cmd.Name) - assert.NotNil(t, cmd.Action) - assert.NotNil(t, cmd.Before) - assert.NotNil(t, cmd.After) - assert.NotEmpty(t, cmd.Flags) - - // Test that expected flags are present - flagNames := make([]string, len(cmd.Flags)) - for i, flag := range cmd.Flags { - flagNames[i] = flag.Names()[0] - } - - expectedFlags := []string{"ipfs", "command", "history-file", "prompt", "no-banner"} - for _, expected := range expectedFlags { - assert.Contains(t, flagNames, expected, "Command should have flag: %s", expected) - } -} - -func TestGlobalsIntegration(t *testing.T) { - t.Parallel() - - baseGlobals, err := NewGlobals(createMockCLIContext()) - require.NoError(t, err) - - // Test that all expected functions are present and callable - expectedFunctions := []string{"+", "*", ">", "<", "=", "/", "help", "println", "print"} - - for _, funcName := range expectedFunctions { - funcVal, exists := baseGlobals[funcName] - assert.True(t, exists, "Function %s should exist in base globals", funcName) - assert.NotNil(t, funcVal, "Function %s should not be nil", funcName) - } - - // Test that basic values are present - expectedValues := []string{"nil", "true", "false", "version"} - - for _, valName := range expectedValues { - val, exists := baseGlobals[valName] - assert.True(t, exists, "Value %s should exist in base globals", valName) - assert.NotNil(t, val, "Value %s should not be nil", valName) - } -} - -func TestArithmeticIntegration(t *testing.T) { - t.Parallel() - - // Test that arithmetic works in command execution - arithmeticTests := []struct { - command string - expectError bool - }{ - {"(+ 1 2)", false}, - {"(* 2 3)", false}, - {"(+ 1 2 3 4 5)", false}, - {"(* 2 3 4)", false}, - {"(> 5 3)", false}, - {"(< 3 5)", false}, - {"(= 5 5)", false}, - {"(* 10 0.5)", false}, - } - - for _, tt := range arithmeticTests { - t.Run(tt.command, func(t *testing.T) { - err := executeCommandForTesting(createMockCLIContext(), tt.command) - if tt.expectError { - assert.Error(t, err) - } else { - assert.NoError(t, err) - } - }) - } -} diff --git a/examples/echo/README.md b/examples/echo/README.md new file mode 100644 index 0000000..94ffb90 --- /dev/null +++ b/examples/echo/README.md @@ -0,0 +1,95 @@ +# Echo Example + +This example demonstrates the idiomatic wetware process pattern for supporting both synchronous and asynchronous execution modes. + +## Pattern Overview + +Wetware processes follow a unified pattern that supports both sync and async behaviors through a single `Proc` configuration: + +1. **Sync Mode** (`Async: false`): The `_start` function runs automatically, calling `main()` which processes stdin and exits +2. **Async Mode** (`Async: true`): The `_start` function is prevented from running, and the `poll()` export is called for each incoming stream + +## Implementation + +### Sync Mode +```go +func main() { + // Process stdin and exit + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error: " + err.Error() + "\n") + os.Exit(1) + } + // Return 0 to indicate successful completion +} +``` + +### Async Mode +```go +//export poll +func poll() { + // Process each incoming stream + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error in poll: " + err.Error() + "\n") + os.Exit(1) + } +} +``` + +## Runtime Behavior + +### Sync Mode (`Async: false`) +1. **Module Instantiation**: `_start` runs automatically during `ProcConfig.New()` +2. **Message Processing**: `main()` reads from stdin until EOF (one complete message) +3. **Module Closure**: Module closes after `main()` returns +4. **Usage**: One module instance per message + +### Async Mode (`Async: true`) +1. **Module Instantiation**: `_start` is prevented from running (`WithStartFunctions()`) +2. **Stream Handler**: Module is registered with a stream handler +3. **Message Processing**: Each incoming stream calls `poll()` export +4. **Module Persistence**: Module stays alive for multiple messages +5. **Usage**: One module instance for multiple messages + +## Configuration + +```go +// Sync mode +config := system.ProcConfig{ + Host: host, + Runtime: runtime, + Bytecode: bytecode, + ErrWriter: &bytes.Buffer{}, + Async: false, // Sync mode +} + +// Async mode +config := system.ProcConfig{ + Host: host, + Runtime: runtime, + Bytecode: bytecode, + ErrWriter: &bytes.Buffer{}, + Async: true, // Async mode +} +``` + +## Benefits + +- **Explicit Mode Selection**: `Async` flag makes behavior clear and predictable +- **WASM Lifecycle Compliance**: Respects the fundamental constraint that `_start` can only run once +- **Flexible Implementation**: Processes can support one or both modes +- **Clean Separation**: Sync and async logic are clearly separated +- **Unified API**: Same `ProcessMessage` method works for both modes + +## Usage + +### Building +```bash +tinygo build -o main.wasm -target=wasi -scheduler=none main.go +``` + +### Running +The process behavior is determined by the `Async` configuration flag: +- **Sync mode**: Process one message and exit +- **Async mode**: Process multiple messages via stream handler + +This pattern enables wetware processes to be both simple command-line tools and long-running stream processors, depending on the configuration and requirements. \ No newline at end of file diff --git a/examples/echo/main.go b/examples/echo/main.go index cc7a4e7..cbe8515 100644 --- a/examples/echo/main.go +++ b/examples/echo/main.go @@ -4,18 +4,33 @@ package main import ( "io" - "log/slog" "os" ) -func main() {} +// main is the entry point for synchronous mode. +// It processes one complete message from stdin and exits. +func main() { + // Echo: copy stdin to stdout using io.Copy + // io.Copy uses an internal 32KB buffer by default + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error copying stdin to stdout: " + err.Error() + "\n") + os.Exit(1) + } + defer os.Stdout.Sync() + // implicitly returns 0 to indicate successful completion +} -// Echo function that can be called from the WASM module +// poll is the async entry point for stream-based processing. +// This function is called by the wetware runtime when a new stream +// is established for this process. // //export poll func poll() { - var buf [512]byte - if n, err := io.CopyBuffer(os.Stdout, os.Stdin, buf[:]); err != nil { - slog.Error("failed to copy", "reason", err, "written", n) + // In async mode, we process each incoming stream + // This is the same logic as main() but for individual streams + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error in poll: " + err.Error() + "\n") + os.Exit(1) } + defer os.Stdout.Sync() } diff --git a/examples/echo/main.wasm b/examples/echo/main.wasm index 8cb4b51..ba70093 100644 Binary files a/examples/echo/main.wasm and b/examples/echo/main.wasm differ diff --git a/examples/export/cap/export.capnp b/examples/export/cap/export.capnp deleted file mode 100644 index 8c95ab9..0000000 --- a/examples/export/cap/export.capnp +++ /dev/null @@ -1,11 +0,0 @@ -using Go = import "/go.capnp"; - -@0xa0266946850e6061; - -$Go.package("cap"); -$Go.import("github.com/wetware/go/examples/export/cap"); - - -interface Greeter { - greet @0 (name :Text) -> (greeting :Text); -} diff --git a/examples/export/cap/export.capnp.go b/examples/export/cap/export.capnp.go deleted file mode 100644 index b3012b1..0000000 --- a/examples/export/cap/export.capnp.go +++ /dev/null @@ -1,367 +0,0 @@ -// Code generated by capnpc-go. DO NOT EDIT. - -package cap - -import ( - capnp "capnproto.org/go/capnp/v3" - text "capnproto.org/go/capnp/v3/encoding/text" - fc "capnproto.org/go/capnp/v3/flowcontrol" - schemas "capnproto.org/go/capnp/v3/schemas" - server "capnproto.org/go/capnp/v3/server" - context "context" -) - -type Greeter capnp.Client - -// Greeter_TypeID is the unique identifier for the type Greeter. -const Greeter_TypeID = 0xbe2d1febcefe3193 - -func (c Greeter) Greet(ctx context.Context, params func(Greeter_greet_Params) error) (Greeter_greet_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xbe2d1febcefe3193, - MethodID: 0, - InterfaceName: "export.capnp:Greeter", - MethodName: "greet", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} - s.PlaceArgs = func(s capnp.Struct) error { return params(Greeter_greet_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Greeter_greet_Results_Future{Future: ans.Future()}, release - -} - -func (c Greeter) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c Greeter) String() string { - return "Greeter(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c Greeter) AddRef() Greeter { - return Greeter(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c Greeter) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c Greeter) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c Greeter) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (Greeter) DecodeFromPtr(p capnp.Ptr) Greeter { - return Greeter(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c Greeter) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c Greeter) IsSame(other Greeter) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c Greeter) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c Greeter) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A Greeter_Server is a Greeter with a local implementation. -type Greeter_Server interface { - Greet(context.Context, Greeter_greet) error -} - -// Greeter_NewServer creates a new Server from an implementation of Greeter_Server. -func Greeter_NewServer(s Greeter_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(Greeter_Methods(nil, s), s, c) -} - -// Greeter_ServerToClient creates a new Client from an implementation of Greeter_Server. -// The caller is responsible for calling Release on the returned Client. -func Greeter_ServerToClient(s Greeter_Server) Greeter { - return Greeter(capnp.NewClient(Greeter_NewServer(s))) -} - -// Greeter_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func Greeter_Methods(methods []server.Method, s Greeter_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 1) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xbe2d1febcefe3193, - MethodID: 0, - InterfaceName: "export.capnp:Greeter", - MethodName: "greet", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Greet(ctx, Greeter_greet{call}) - }, - }) - - return methods -} - -// Greeter_greet holds the state for a server call to Greeter.greet. -// See server.Call for documentation. -type Greeter_greet struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Greeter_greet) Args() Greeter_greet_Params { - return Greeter_greet_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Greeter_greet) AllocResults() (Greeter_greet_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Greeter_greet_Results(r), err -} - -// Greeter_List is a list of Greeter. -type Greeter_List = capnp.CapList[Greeter] - -// NewGreeter creates a new list of Greeter. -func NewGreeter_List(s *capnp.Segment, sz int32) (Greeter_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[Greeter](l), err -} - -type Greeter_greet_Params capnp.Struct - -// Greeter_greet_Params_TypeID is the unique identifier for the type Greeter_greet_Params. -const Greeter_greet_Params_TypeID = 0xcb62612da96426ac - -func NewGreeter_greet_Params(s *capnp.Segment) (Greeter_greet_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Greeter_greet_Params(st), err -} - -func NewRootGreeter_greet_Params(s *capnp.Segment) (Greeter_greet_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Greeter_greet_Params(st), err -} - -func ReadRootGreeter_greet_Params(msg *capnp.Message) (Greeter_greet_Params, error) { - root, err := msg.Root() - return Greeter_greet_Params(root.Struct()), err -} - -func (s Greeter_greet_Params) String() string { - str, _ := text.Marshal(0xcb62612da96426ac, capnp.Struct(s)) - return str -} - -func (s Greeter_greet_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Greeter_greet_Params) DecodeFromPtr(p capnp.Ptr) Greeter_greet_Params { - return Greeter_greet_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Greeter_greet_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Greeter_greet_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Greeter_greet_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Greeter_greet_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Greeter_greet_Params) Name() (string, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s Greeter_greet_Params) HasName() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Greeter_greet_Params) NameBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s Greeter_greet_Params) SetName(v string) error { - return capnp.Struct(s).SetText(0, v) -} - -// Greeter_greet_Params_List is a list of Greeter_greet_Params. -type Greeter_greet_Params_List = capnp.StructList[Greeter_greet_Params] - -// NewGreeter_greet_Params creates a new list of Greeter_greet_Params. -func NewGreeter_greet_Params_List(s *capnp.Segment, sz int32) (Greeter_greet_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Greeter_greet_Params](l), err -} - -// Greeter_greet_Params_Future is a wrapper for a Greeter_greet_Params promised by a client call. -type Greeter_greet_Params_Future struct{ *capnp.Future } - -func (f Greeter_greet_Params_Future) Struct() (Greeter_greet_Params, error) { - p, err := f.Future.Ptr() - return Greeter_greet_Params(p.Struct()), err -} - -type Greeter_greet_Results capnp.Struct - -// Greeter_greet_Results_TypeID is the unique identifier for the type Greeter_greet_Results. -const Greeter_greet_Results_TypeID = 0xc0c1aae7ac34f9c6 - -func NewGreeter_greet_Results(s *capnp.Segment) (Greeter_greet_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Greeter_greet_Results(st), err -} - -func NewRootGreeter_greet_Results(s *capnp.Segment) (Greeter_greet_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Greeter_greet_Results(st), err -} - -func ReadRootGreeter_greet_Results(msg *capnp.Message) (Greeter_greet_Results, error) { - root, err := msg.Root() - return Greeter_greet_Results(root.Struct()), err -} - -func (s Greeter_greet_Results) String() string { - str, _ := text.Marshal(0xc0c1aae7ac34f9c6, capnp.Struct(s)) - return str -} - -func (s Greeter_greet_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Greeter_greet_Results) DecodeFromPtr(p capnp.Ptr) Greeter_greet_Results { - return Greeter_greet_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Greeter_greet_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Greeter_greet_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Greeter_greet_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Greeter_greet_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Greeter_greet_Results) Greeting() (string, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s Greeter_greet_Results) HasGreeting() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Greeter_greet_Results) GreetingBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s Greeter_greet_Results) SetGreeting(v string) error { - return capnp.Struct(s).SetText(0, v) -} - -// Greeter_greet_Results_List is a list of Greeter_greet_Results. -type Greeter_greet_Results_List = capnp.StructList[Greeter_greet_Results] - -// NewGreeter_greet_Results creates a new list of Greeter_greet_Results. -func NewGreeter_greet_Results_List(s *capnp.Segment, sz int32) (Greeter_greet_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Greeter_greet_Results](l), err -} - -// Greeter_greet_Results_Future is a wrapper for a Greeter_greet_Results promised by a client call. -type Greeter_greet_Results_Future struct{ *capnp.Future } - -func (f Greeter_greet_Results_Future) Struct() (Greeter_greet_Results, error) { - p, err := f.Future.Ptr() - return Greeter_greet_Results(p.Struct()), err -} - -const schema_a0266946850e6061 = "x\xda|\xcd\xb1J\xf3P\x18\xc6\xf1\xe79'\xf9\xfa" + - "\x0d\xad\xf5\x18\x05\x11\xa5\x08\xdaAH!\xea\xa2K;" + - "Yt\xcaq\x17<\xea\xa1\x14l\x1a\xd2\x08\xde\x80\xab" + - "\x8b7\xe0j\xe95\x88..\x82\xb7\xe1\xe0\x15\xb8x" + - "$\x85\x14]\xdc^^\xfe\xfc\x9e\xf9\xdb\x8e\x17\xd5\x86" + - "\x02B\xaf\xfa\xff\xdc]\xf4\xf5\xf6\xd1\x08\x1f\xa1j\xd2" + - "\x99\xd3\xb9\x9b\x83~\xf3\x1e`\xb0\xc6q\xb0\xc9\x0a\x10" + - "\xac\xb3\x1b\x1c\x16\x97{\xf9\xdc\x9d\xbc\x8f\x9f\x9f\xa0\x96" + - "\x08\xf8\xc5o'\xe2\x0a\xc1`\x8fm\xd0M\x9a\x17\x0f" + - "\xa19{\xfd\x19\x9cp\xa1\x08,\xdb\x08\x9d\xbdN\x87" + - "Y\xde:\xa7I\x93t\xbf\x9b\xd9\xba\xcdm\x16\x93\xda" + - "\x93>0\x03XN)\xb5\x0d\xa1\xfcJ\xa3\x97Y\x9b" + - "w\x18\x933D\x96Ha\xb4\xa6\xc1\xc6\xb1\x1d]]" + - "\xca|\xa4=\xe9\x01\x1e\x01U;\x02tUR/\x0b" + - "\xbai\xd5Oz\x00X\x85`\x15\x7f{\xb1\xc9\xcc\x80" + - "\xbf\xb8-@\xff\x97\xd4\x8b\x82\xf5\xc4\x0cl\xe9|\x07" + - "\x00\x00\xff\xff\xcaQX3" - -func RegisterSchema(reg *schemas.Registry) { - reg.Register(&schemas.Schema{ - String: schema_a0266946850e6061, - Nodes: []uint64{ - 0xbe2d1febcefe3193, - 0xc0c1aae7ac34f9c6, - 0xcb62612da96426ac, - }, - Compressed: true, - }) -} diff --git a/examples/export/cap/gen.go b/examples/export/cap/gen.go deleted file mode 100644 index 516b445..0000000 --- a/examples/export/cap/gen.go +++ /dev/null @@ -1,2 +0,0 @@ -//go:generate capnp compile -I$GOPATH/src/capnproto.org/go/capnp/std -ogo export.capnp -package cap diff --git a/examples/export/export b/examples/export/export deleted file mode 100755 index 3354181..0000000 Binary files a/examples/export/export and /dev/null differ diff --git a/examples/export/main.go b/examples/export/main.go deleted file mode 100644 index 19b915f..0000000 --- a/examples/export/main.go +++ /dev/null @@ -1,58 +0,0 @@ -package main - -import ( - "context" - "fmt" - "os" - "os/signal" - "syscall" - - "capnproto.org/go/capnp/v3" - "capnproto.org/go/capnp/v3/rpc" - "github.com/wetware/go/examples/export/cap" - "github.com/wetware/go/system" -) - -func main() { - ctx, cancel := signal.NotifyContext(context.Background(), - syscall.SIGINT, - syscall.SIGTERM) - defer cancel() - - // Open the bootstrap socket that was passed in from the host. - sock := os.NewFile(system.BOOTSTRAP_FD, "host") - defer sock.Close() - - // Wrap the socket in a Cap'n Proto connection. - conn := rpc.NewConn(rpc.NewStreamTransport(sock), &rpc.Options{ - BaseContext: func() context.Context { return ctx }, - BootstrapClient: export(), - }) - defer conn.Close() - - // Get the bootstrap client from the host. - conn.Bootstrap(ctx) - - <-ctx.Done() -} - -func export() capnp.Client { - server := cap.Greeter_NewServer(greeter{}) - return capnp.NewClient(server) -} - -type greeter struct{} - -func (greeter) Greet(_ context.Context, call cap.Greeter_greet) error { - name, err := call.Args().Name() - if err != nil { - return err - } - - res, err := call.AllocResults() - if err != nil { - return err - } - - return res.SetGreeting(fmt.Sprintf("Hello, %s! 👋", name)) -} diff --git a/examples/fd-demo/README.md b/examples/fd-demo/README.md deleted file mode 100644 index 99fd4a0..0000000 --- a/examples/fd-demo/README.md +++ /dev/null @@ -1,86 +0,0 @@ -# File Descriptor Demo - -This example demonstrates how to use the `--with-fd` flag with `ww run` to pass file descriptors to child processes. - -## Building - -```bash -go build -o fd-demo -``` - -## Usage - -### Basic Demo (no file descriptors) -```bash -ww run ./fd-demo -``` - -### With File Descriptors -```bash -# Pass a single file descriptor -ww run --with-fd demo=3 ./fd-demo - -# Pass multiple file descriptors -ww run --with-fd demo=3 --with-fd log=4 ./fd-demo - -# Pass file descriptors in different order (same result) -ww run --with-fd log=4 --with-fd demo=3 ./fd-demo -``` - -## What It Does - -The demo program: - -1. **Checks for RPC socket**: Always available at FD 3 for communication with the host -2. **Lists user FDs**: Shows all file descriptors passed via `--with-fd` -3. **Displays file info**: Attempts to stat each file descriptor to show basic information -4. **Provides guidance**: Shows helpful usage examples if no FDs are provided - -## Expected Output - -With no file descriptors: -``` -File Descriptor Demo -==================== -✓ RPC socket available at FD 3 - -No user file descriptors provided. -Try running with: ww run --with-fd demo=3 --with-fd log=4 ./fd-demo - -Demo completed. -``` - -With file descriptors: -``` -File Descriptor Demo -==================== -✓ RPC socket available at FD 3 -✓ WW_FD_DEMO=4 - └─ File: demo, Size: 1234 bytes -✓ WW_FD_LOG=5 - └─ File: log, Size: 5678 bytes - -Demo completed. -``` - -## File Descriptor Layout - -- **FD 3**: RPC socket (always present) -- **FD 4**: First `--with-fd` mapping -- **FD 5**: Second `--with-fd` mapping -- And so on... - -## Testing with Real Files - -To test with actual files: - -```bash -# Create test files -echo "Hello World" > demo.txt -echo "Log message" > log.txt - -# Run with file descriptors -ww run --with-fd demo=3 --with-fd log=4 ./fd-demo < demo.txt 3 0 { - fmt.Println("\nFile Descriptors Available:") - for name, fd := range fdMap { - fmt.Printf("✓ %s -> FD %d\n", name, fd) - - // Try to get file info directly from the FD - if file := os.NewFile(uintptr(fd), name); file != nil { - // Try to get file info - if stat, err := file.Stat(); err == nil { - fmt.Printf(" └─ File: %s, Size: %d bytes\n", stat.Name(), stat.Size()) - } else { - fmt.Printf(" └─ File: %s (stat failed: %v)\n", file.Name(), err) - } - file.Close() - } else { - fmt.Printf(" └─ Failed to open FD %d\n", fd) - } - } - } else { - fmt.Println("\nNo user file descriptors provided.") - fmt.Println("Try running with: ww run --with-fd demo=3 --with-fd log=4 ./fd-demo") - } - - fmt.Println("\nDemo completed.") -} diff --git a/examples/import/main.go b/examples/import/main.go deleted file mode 100644 index cf2330c..0000000 --- a/examples/import/main.go +++ /dev/null @@ -1,100 +0,0 @@ -package main - -import ( - "context" - "fmt" - "log/slog" - "os" - - "capnproto.org/go/capnp/v3/rpc" - "github.com/libp2p/go-libp2p" - "github.com/libp2p/go-libp2p/core/peer" - "github.com/libp2p/go-libp2p/core/protocol" - ma "github.com/multiformats/go-multiaddr" - - export_cap "github.com/wetware/go/examples/export/cap" -) - -func fail(error string) { - fmt.Fprintf(os.Stderr, "ERROR: %s\n", error) - os.Exit(1) -} - -func main() { - ctx := context.Background() - - // Validate and extract the process arguments. - if len(os.Args) != 3 { - fmt.Println(`Usage: - ./ -Example: - ./ id=12D3KooWMh9743yKf2h3ZrjAGVtVBmDqyqnghxEoyqz3wSiPfv5e /ip4/127.0.0.1/tcp/2020`) - } - - remote, remoteAddr := os.Args[1], os.Args[2] - - // Build the libp2p host and connect to the remote peer. - host, err := libp2p.New() - if err != nil { - fail(fmt.Sprintf("ERROR: Failed to create libp2p host: %v\n", err)) - } - defer host.Close() - - addr, err := ma.NewMultiaddr(remoteAddr) - if err != nil { - fail(err.Error()) - } - - id, err := peer.Decode(remote) - if err != nil { - fail(err.Error()) - } - - err = host.Connect(ctx, peer.AddrInfo{ - ID: id, - Addrs: []ma.Multiaddr{addr}, - }) - if err != nil { - fail(err.Error()) - } - - s, err := host.NewStream(ctx, id, protocol.ID("/ww/0.1.0")) - if err != nil { - fail(err.Error()) - } - defer s.Close() - - // Bootstrap the object capability over the p2p connection. - conn := rpc.NewConn(rpc.NewPackedStreamTransport(s), &rpc.Options{ - BaseContext: func() context.Context { return ctx }, - }) - - client := conn.Bootstrap(ctx) - defer client.Release() - - if err = client.Resolve(ctx); err != nil { - fail(err.Error()) - } - - // Call the object capability. - greeter := export_cap.Greeter(client) - f, release := greeter.Greet(ctx, func(params export_cap.Greeter_greet_Params) error { - return params.SetName("Import Example") - }) - defer release() - - // Wait for the greeting to complete - <-f.Done() - - res, err := f.Struct() - if err != nil { - fail(fmt.Sprintf("ERROR: greet failed: %v\n", err)) - } - - greeting, err := res.Greeting() - if err != nil { - fail(fmt.Sprintf("ERROR: greet failed: %v\n", err)) - } - - slog.Info(fmt.Sprintf("Object capability response: %s", greeting)) -} diff --git a/go.mod b/go.mod index 4f4f7a5..990a55b 100644 --- a/go.mod +++ b/go.mod @@ -3,16 +3,14 @@ module github.com/wetware/go go 1.24 require ( - capnproto.org/go/capnp/v3 v3.1.0-alpha.1 - github.com/chzyer/readline v1.5.1 github.com/ipfs/boxo v0.28.0 github.com/ipfs/kubo v0.31.0 github.com/libp2p/go-libp2p v0.43.0 + github.com/libp2p/go-libp2p-kad-dht v0.29.0 github.com/lmittmann/tint v1.0.4 github.com/lthibault/go-libp2p-inproc-transport v0.4.1 github.com/mr-tron/base58 v1.2.0 github.com/multiformats/go-multiaddr v0.16.0 - github.com/spy16/slurp v0.3.0 github.com/stretchr/testify v1.11.1 github.com/tetratelabs/wazero v1.9.0 github.com/urfave/cli/v2 v2.27.5 @@ -22,12 +20,12 @@ require ( ) require ( + github.com/Jorropo/jsync v1.0.1 // indirect github.com/alecthomas/units v0.0.0-20240927000941-0f3dac36c52b // indirect github.com/benbjohnson/clock v1.3.5 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/blang/semver/v4 v4.0.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect - github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 // indirect github.com/cpuguy83/go-md2man/v2 v2.0.6 // indirect github.com/crackcomm/go-gitignore v0.0.0-20241020182519-7843d2ba8fdf // indirect github.com/davecgh/go-spew v1.1.1 // indirect @@ -77,7 +75,6 @@ require ( github.com/libp2p/go-cidranger v1.1.0 // indirect github.com/libp2p/go-flow-metrics v0.2.0 // indirect github.com/libp2p/go-libp2p-asn-util v0.4.1 // indirect - github.com/libp2p/go-libp2p-kad-dht v0.29.0 // indirect github.com/libp2p/go-libp2p-kbucket v0.6.4 // indirect github.com/libp2p/go-libp2p-record v0.3.1 // indirect github.com/libp2p/go-libp2p-routing-helpers v0.7.4 // indirect @@ -85,7 +82,6 @@ require ( github.com/libp2p/go-netroute v0.2.2 // indirect github.com/libp2p/go-reuseport v0.4.0 // indirect github.com/libp2p/go-yamux/v5 v5.0.1 // indirect - github.com/libp2p/zeroconf/v2 v2.2.0 // indirect github.com/lthibault/util v0.0.12 // indirect github.com/marten-seemann/tcp v0.0.0-20210406111302-dfbc87cc63fd // indirect github.com/mattn/go-isatty v0.0.20 // indirect diff --git a/go.sum b/go.sum index 5d31279..4d1b0ea 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,5 @@ bazil.org/fuse v0.0.0-20200117225306-7b5117fecadc h1:utDghgcjE8u+EBjHOgYT+dJPcnDF05KqWMBcjuJy510= bazil.org/fuse v0.0.0-20200117225306-7b5117fecadc/go.mod h1:FbcW6z/2VytnFDhZfumh8Ss8zxHE6qpMP5sHTRe0EaM= -capnproto.org/go/capnp/v3 v3.1.0-alpha.1 h1:8/sMnWuatR99G0L0vmnrXj0zVP0MrlyClRqSmqGYydo= -capnproto.org/go/capnp/v3 v3.1.0-alpha.1/go.mod h1:2vT5D2dtG8sJGEoEKU17e+j7shdaYp1Myl8X03B3hmc= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.31.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= @@ -58,14 +56,8 @@ github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghf github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/logex v1.2.1 h1:XHDu3E6q+gdHgsdTPH6ImJMIp436vR6MPtH8gP05QzM= -github.com/chzyer/logex v1.2.1/go.mod h1:JLbx6lG2kDbNRFnfkgvh4eRJRPX1QCoOIWomwysCBrQ= github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/readline v1.5.1 h1:upd/6fQk4src78LMRzh5vItIt361/o4uq553V8B5sGI= -github.com/chzyer/readline v1.5.1/go.mod h1:Eh+b79XXUwfKfcPLepksvw2tcLE/Ct21YObkaSkeBlk= github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/chzyer/test v1.0.0 h1:p3BQDXSxOhOG0P9z6/hGnII4LGiEPOYBhs8asl/fC04= -github.com/chzyer/test v1.0.0/go.mod h1:2JlltgoNkt4TW/z9V/IzDdFaMTM2JPIi26O1pF38GC8= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= @@ -80,8 +72,6 @@ github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwP github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= -github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381 h1:d5EKgQfRQvO97jnISfR89AiCCCJMwMFoSxUiU0OGCRU= -github.com/colega/zeropool v0.0.0-20230505084239-6fb4a4f75381/go.mod h1:OU76gHeRo8xrzGJU3F3I1CqX1ekM8dfJw0+wPeMwnp0= github.com/coreos/go-systemd v0.0.0-20181012123002-c6f51f82210d/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.6 h1:XJtiaUW6dEEqVuZiMTn1ldk455QWwEIsMIJlo5vtkx0= @@ -400,7 +390,6 @@ github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWE github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4= -github.com/miekg/dns v1.1.43/go.mod h1:+evo5L0630/F6ca/Z9+GAqzhjGyn8/c+TBaOyfEl0V4= github.com/miekg/dns v1.1.66 h1:FeZXOS3VCVsKnEAd+wBkjMC3D2K+ww66Cq3VnCINuJE= github.com/miekg/dns v1.1.66/go.mod h1:jGFzBsSNbJw6z1HYut1RKBKHA9PBdxeHrZG8J+gC2WE= github.com/mikioh/tcp v0.0.0-20190314235350-803a9b46060c h1:bzE/A84HN25pxAuk9Eej1Kz9OUelF97nAc82bDquQI8= @@ -458,8 +447,6 @@ github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58 h1:onHthvaw9LFnH4t2D github.com/pbnjay/memory v0.0.0-20210728143218-7b4eea64cf58/go.mod h1:DXv8WO4yhMYhSNPKjeNKa5WY9YCIEBRbNzFFPJbWO6Y= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9 h1:1/WtZae0yGtPq+TI6+Tv1WTxkukpXeMlviSxvL7SRgk= github.com/petar/GoLLRB v0.0.0-20210522233825-ae3b015fd3e9/go.mod h1:x3N5drFsm2uilKKuuYo6LdyD8vZAW55sH/9w+pbo1sw= -github.com/philhofer/fwd v1.1.2 h1:bnDivRJ1EWPjUIRXV5KfORO897HTbpFAQddBdE8t7Gw= -github.com/philhofer/fwd v1.1.2/go.mod h1:qkPdfjR2SIEbspLqpe1tO4n5yICnr2DY7mqEx2tUTP0= github.com/pion/datachannel v1.5.10 h1:ly0Q26K1i6ZkGf42W7D4hQYR90pZwzFOjTq5AuCKk4o= github.com/pion/datachannel v1.5.10/go.mod h1:p/jJfC9arb29W7WrxyKbepTU20CFgyx5oLo8Rs4Py/M= github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= @@ -572,8 +559,6 @@ github.com/sourcegraph/annotate v0.0.0-20160123013949-f4cad6c6324d/go.mod h1:Udh github.com/sourcegraph/syntaxhighlight v0.0.0-20170531221838-bd320f5d308e/go.mod h1:HuIsMU8RRBOtsCgI77wP899iHVBQpCmg4ErYMZB+2IA= github.com/spaolacci/murmur3 v1.1.0 h1:7c1g84S4BPRrfL5Xrdp6fOJ206sU9y293DDHaoy0bLI= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spy16/slurp v0.3.0 h1:L8HcbTdyq/nmI/9Zqd6sNUUouSbTIzMBrS+SBzEFzfg= -github.com/spy16/slurp v0.3.0/go.mod h1:S8B4KMkF6husIZ3jSwsXPPCEM8LwM5Q5QTuLRZ6ujXc= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= @@ -595,10 +580,6 @@ github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45 github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA= github.com/tetratelabs/wazero v1.9.0 h1:IcZ56OuxrtaEz8UYNRHBrUa9bYeX9oVY93KspZZBf/I= github.com/tetratelabs/wazero v1.9.0/go.mod h1:TSbcXCfFP0L2FGkRPxHphadXPjo1T6W+CseNNY7EkjM= -github.com/tinylib/msgp v1.1.9 h1:SHf3yoO2sGA0veCJeCBYLHuttAVFHGm2RHgNodW7wQU= -github.com/tinylib/msgp v1.1.9/go.mod h1:BCXGB54lDD8qUEPmiG0cQQUANC4IUQyB2ItS2UDlO/k= -github.com/tj/assert v0.0.3 h1:Df/BlaZ20mq6kuai7f5z2TvPFiwC3xaWJSDQNiIS3Rk= -github.com/tj/assert v0.0.3/go.mod h1:Ne6X72Q+TB1AteidzQncjw9PabbMp4PBMZ1k+vd1Pvk= github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb h1:Ywfo8sUltxogBpFuMOFRrrSifO788kAFxmvVw31PtQQ= github.com/ucarion/urlpath v0.0.0-20200424170820-7ccc79b76bbb/go.mod h1:ikPs9bRWicNw3S7XpJ8sK/smGwU9WcSVU3dy9qahYBM= github.com/urfave/cli v1.22.10/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= @@ -759,7 +740,6 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= @@ -809,11 +789,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200602225109-6fdc65e7d980/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210426080607-c94f62235c83/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220310020820-b874c991c1a5/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.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -836,7 +812,6 @@ golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= diff --git a/spec/cell.md b/spec/cell.md deleted file mode 100644 index a52d0ee..0000000 --- a/spec/cell.md +++ /dev/null @@ -1,151 +0,0 @@ -# Wetware Cell API Specification - -## Motivation - -The Wetware Cell API defines the interface between a host process (running `ww run`) and a cell process (child process managed by `ww run`) that runs in a controlled execution environment with attenuated capabilities. - -This document is for users who want to write their own cells. Simply adhere to this specification, and then you can `ww run `, and your application will run in a secure, decentralized environment with access to capabilities like IPFS. - -## Process Interface - -### Standard Input/Output -- **stdin (fd 0)**: Direct passthrough from host's stdin -- **stdout (fd 1)**: Direct passthrough to host's stdout -- **stderr (fd 2)**: Direct passthrough to host's stderr - -### Command Line Arguments -- **argv[0]**: Executable path -- **argv[1..n]**: Arguments passed from host to cell -- Arguments are passed through unchanged from host's `exec.Command` - -### Environment Variables -- **WW_ENV**: Comma-separated list of environment variables to pass to cell -- Additional environment variables can be specified via `--env` flag -- Cell inherits host's environment filtered by these specifications - -## File Descriptors - -### Fixed File Descriptors -- **fd 3**: One end of a Unix domain socket pair for Cap'n Proto RPC communication with host - -### User-Configurable File Descriptors -Additional file descriptors can be passed from host to cell using the `--with-fd` flag: - -```bash -ww run --with-fd name=fdnum [--with-fd name2=fdnum2 ...] -``` - -**Format**: `--with-fd name=fdnum` where: -- `name`: Logical name for the file descriptor (e.g., "db", "cache", "input") -- `fdnum`: Source file descriptor number in the host process - -**Target Assignment**: File descriptors are automatically assigned to the cell in **predictable positional order** starting at fd 3: -- First `--with-fd` flag → fd 3 (first argument) -- Second `--with-fd` flag → fd 4 (second argument) -- Third `--with-fd` flag → fd 5 (third argument) -- And so on... - -**Naming Benefits**: Each file descriptor gets a semantic name that the cell can use: -- **Predictable positioning**: The child knows exactly which FD number to use -- **Semantic meaning**: Names like "db", "cache", "logs" make the purpose clear -- **Flexible ordering**: You can reorder `--with-fd` flags without breaking the child -- **Environment mapping**: Clear mapping from names to FD numbers - -**Environment Variables**: The cell receives environment variables that map names to FD numbers: -```bash -WW_FD_DB=3 # "db" is available at FD 3 -WW_FD_CACHE=4 # "cache" is available at FD 4 -WW_FD_INPUT=5 # "input" is available at FD 5 -``` - -**Usage in Child Process**: The cell can now access resources by both name and position: -```go -// Access by FD number (predictable positioning) -dbFD := os.NewFile(3, "database") -cacheFD := os.NewFile(4, "cache") - -// Or check environment variables for validation -if os.Getenv("WW_FD_DATABASE") == "" { - log.Fatal("Database FD not provided") -} -``` - -### File Descriptor Usage -The cell process must: -1. Establish RPC connection via fd 3 -2. Authenticate to obtain capabilities -3. Access additional file descriptors via predictable FD numbers (3, 4, 5...) or environment variables (`WW_FD_*`) - -**Key Benefits**: -- **Predictable access**: First `--with-fd` is always at FD 3, second at FD 4, etc. -- **Named resources**: Environment variables tell you what each FD represents -- **Flexible ordering**: Change the order of `--with-fd` flags without breaking the child -- **Self-documenting**: The child knows exactly what each FD is for - -## Capabilities - -### Available Capabilities -Currently, exactly one capability is available: - -- **IPFS**: Access to IPFS Core API - - Interface: `system.IPFS` (Cap'n Proto) - - Access: Policy-controlled based on authentication - - Scope: Determined by host's IPFS configuration - -### Future Capabilities -The interface is designed to support additional capabilities: -- Process execution -- Network access (via libp2p streams) -- Various decentralized services - -## Authentication - -*Note: Authentication details are currently being simplified. The specific authentication mechanism may vary between implementations.* - -### Current Implementation -The current `ww run` implementation uses a simplified approach where: -- The cell connects to the host via the Unix domain socket (fd 3) -- Capabilities are granted based on the connection establishment -- No separate identity file or cryptographic authentication is required - -### Future Authentication -Future versions may implement more sophisticated authentication mechanisms including: -- Cryptographic identity verification -- Policy-based capability grants -- Multi-factor authentication - -## Status Codes - -### Standard Exit Codes -- **0**: Success -- **1**: General error -- **2**: Usage error -- **126**: Command not executable -- **127**: Command not found -- **128+n**: Signal termination (n = signal number) - -### Future Standard Codes -The following status codes are reserved for future standardization: -- **64**: Cell authentication failed -- **65**: Capability access denied -- **66**: Resource limit exceeded -- **67**: Isolation violation detected - -## Reference Implementations - -### ww run -The `ww run` subcommand is a reference implementation that demonstrates the cell API. It: -- Creates a jailed execution environment -- Sets up the Unix domain socket pair for RPC communication -- Launches the specified executable with the required file descriptors -- Supports file descriptor passing via `--with-fd` flags - -**Example Usage**: -```bash -# Example usage: -# ww run --with-fd db=3 --with-fd cache=4 /ipfs/QmMyApp -# -# Environment variables in cell: -# - WW_FD_DB=3 (fd 3 mapped to fd 3 in cell) -# - WW_FD_CACHE=4 (fd 4 mapped to fd 4 in cell) -``` diff --git a/system/SPEC-PROC.md b/system/SPEC-PROC.md new file mode 100644 index 0000000..7b30ebb --- /dev/null +++ b/system/SPEC-PROC.md @@ -0,0 +1,150 @@ +# Wetware Process Specification + +This document specifies the design and behavior of Wetware processes (`Proc`) in the wetware system. + +## Overview + +A Wetware process is a WebAssembly (WASM) module that can operate in two distinct modes: +- **Synchronous Mode**: Processes one message and exits +- **Asynchronous Mode**: Processes multiple messages via stream handlers + +The mode is determined by the `Async` field in `ProcConfig`. + +## Core Design Principles + +### 1. WASM Lifecycle Constraint +WebAssembly modules have a fundamental constraint: `_start` can only run once during module instantiation. After `_start` returns, the module is closed and cannot be reused. + +This constraint forces us to choose between **at runtime**: +- **Running `_start`** (sync mode): Module processes one message and closes +- **Preventing `_start`** (async mode): Module stays alive, exports are called repeatedly + +Note the bold lettering above: a single WASM executable can support **both** synchronous and asynchronous modes. Usually, this maps onto a **client and server mode**. + +>**Recommendation.** Structure your Wetware applications as single-binary executables that behave as a server in async mode, and as a client in sync mode. + +### 2. Message Processing Protocol +**One stream (start to EOF) is one message.** This is the fundamental protocol for message delivery: + +- A complete message is defined as data read from a network stream from start until EOF +- The WASM module reads from stdin until EOF to consume one complete message +- After EOF, the message processing is complete +- In async mode, the next call to `poll()` will receive a new stream with a new message + +## Synchronous Mode (`Async: false`) + +### Behavior +- `_start` runs automatically during module instantiation (i.e. `main()` runs exactly once) +- `main()` function processes one complete message from stdin (start to EOF) +- Module closes after `main()` returns +- One module instance per message + +### Message Delivery Mechanism +1. **Stream Setup**: A network stream is connected to stdin before module instantiation +2. **Message Processing**: `main()` reads from stdin until EOF (one complete message) +3. **Module Closure**: Module closes after `main()` returns +4. **Next Message**: Requires a new module instance + +### Guest Code Pattern +The guest code implements a simple message processor: +```go +func main() { + // Process one complete message from stdin (start to EOF) + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error: " + err.Error() + "\n") + os.Exit(1) + } + // Return 0 to indicate successful completion +} +``` + +## Asynchronous Mode (`Async: true`) + +### Behavior +- `_start` is prevented from running during module instantiation (i.e. `main()` will not run at all) +- Module stays alive for multiple messages +- Each incoming stream calls the `poll()` export function +- One module instance for multiple messages + +### Message Delivery Mechanism +1. **Module Instantiation**: Module is created without running `_start` (main() never runs) +2. **Stream Handler Registration**: Module is registered to handle incoming streams +3. **Message Processing**: Each new stream triggers a call to `poll()` +4. **Stream Consumption**: `poll()` reads from stdin until EOF (one complete message) +5. **Stream Completion**: After EOF, the stream is closed and `poll()` returns +6. **Next Message**: A new stream triggers another `poll()` call + +### Guest Code Pattern +The guest code implements a stream-based message processor: +```go +//export poll +func poll() { + // Process one complete message from stdin (start to EOF) + if _, err := io.Copy(os.Stdout, os.Stdin); err != nil { + os.Stderr.WriteString("Error in poll: " + err.Error() + "\n") + os.Exit(1) + } +} +``` + +## Message Delivery Protocol + +### Stream-to-Message Mapping +- **One Network Stream = One Message** +- **Stream Start**: Beginning of message data +- **Stream EOF**: End of message data +- **Message Boundary**: EOF marks the complete message + +### Stream Handling +1. **Stream Connection**: Network stream is connected to stdin +2. **Message Reading**: WASM module reads from stdin until EOF +3. **Message Processing**: Complete message is processed by the module +4. **Stream Cleanup**: Stream is closed after EOF +5. **Next Message**: New stream triggers next message processing + +### Error Handling +- **Stream Errors**: Network errors during reading are propagated to the module +- **Processing Errors**: Module errors are logged and may cause module termination +- **Timeout Handling**: Stream timeouts are handled according to context deadlines + +## Configuration + +### ProcConfig +```go +type ProcConfig struct { + Host host.Host + Runtime wazero.Runtime + Bytecode []byte + ErrWriter io.Writer + Async bool // Gates sync vs async behavior +} +``` + +### Mode Selection +- **Sync Mode** (`Async: false`): One message per module instance +- **Async Mode** (`Async: true`): Multiple messages per module instance + +## Benefits + +1. **Explicit Mode Selection**: `Async` flag makes behavior clear and predictable +2. **WASM Lifecycle Compliance**: Respects fundamental WASM constraints +3. **Simple Message Protocol**: One stream to EOF is one message +4. **Flexible Implementation**: Processes can support one or both modes +5. **Clean Separation**: Sync and async logic are clearly separated +6. **Unified API**: Same `ProcessMessage` method works for both modes + +## Use Cases + +### Synchronous Mode +- **Command-line tools**: Process one input and exit +- **Batch processing**: Process one file or data stream +- **Simple transformations**: One-shot data processing + +### Asynchronous Mode +- **Stream processors**: Handle multiple incoming streams +- **Long-running services**: Persistent message processing +- **Real-time systems**: Continuous message handling + +## Conclusion + +The dual-mode design provides a clean, predictable way to handle both synchronous and asynchronous WebAssembly processes while respecting the fundamental constraints of the WASM runtime. The explicit `Async` flag makes the behavior clear, and the simple "one stream to EOF is one message" protocol provides a consistent interface for message delivery across both modes. \ No newline at end of file diff --git a/system/proc.go b/system/proc.go index ed7305f..c8a4185 100644 --- a/system/proc.go +++ b/system/proc.go @@ -2,114 +2,219 @@ package system import ( "context" + "crypto/rand" + "errors" "fmt" "io" - "log/slog" - "runtime" + "os" + "strings" "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/network" + "github.com/mr-tron/base58" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" + "github.com/tetratelabs/wazero/sys" "go.uber.org/multierr" + "golang.org/x/sync/semaphore" ) type ProcConfig struct { Host host.Host Runtime wazero.Runtime - Bytecode []byte + Src io.ReadCloser + Env, Args []string ErrWriter io.Writer + Async bool // If true, use WithStartFunctions() and set up stream handler } func (c ProcConfig) New(ctx context.Context) (*Proc, error) { var ok = false - cm, err := c.Runtime.CompileModule(ctx, c.Bytecode) + var cs CloserSlice + defer func() { + if !ok { + cs.Close(ctx) + } + }() + + if c.Src == nil { + return nil, errors.New("no source provided") + } + + bytecode, err := io.ReadAll(c.Src) if err != nil { return nil, err } + defer c.Src.Close() - sys, err := wasi_snapshot_preview1.Instantiate(ctx, c.Runtime) + cm, err := c.Runtime.CompileModule(ctx, bytecode) if err != nil { return nil, err } - defer func() { - if !ok { - sys.Close(ctx) - } - }() + cs = append(cs, cm) - e := NewEndpoint() - mod, err := c.Runtime.InstantiateModule(ctx, cm, wazero.NewModuleConfig(). - WithName(e.String()). - WithSysNanosleep(). - WithSysNanotime(). - WithSysWalltime(). - WithStdin(e). - WithStdout(e). - WithStderr(c.ErrWriter). - WithStartFunctions()) + wasi, err := wasi_snapshot_preview1.Instantiate(ctx, c.Runtime) if err != nil { return nil, err } - defer func() { - if !ok { - e.Close() + cs = append(cs, wasi) + + e := c.NewEndpoint() + cs = append(cs, e) + + // In sync mode, set up stdin/stdout for the endpoint + if !c.Async { + // bidirectional pipe that wraps stdin/stdout. + e.ReadWriteCloser = struct { + io.Reader + io.WriteCloser + }{ + Reader: os.Stdin, + WriteCloser: os.Stdout, } - }() + } - proc := &Proc{ - Sys: sys, - Module: mod, - Endpoint: e} - c.Host.SetStreamHandler(e.Protocol(), func(s network.Stream) { - defer s.Close() + // Configure module instantiation based on async mode + config := c.NewModuleConfig(e) - if err := proc.Poll(ctx, s, nil); err != nil { - slog.ErrorContext(ctx, "failed to poll process", "reason", err) - return + mod, err := c.Runtime.InstantiateModule(ctx, cm, config) + if err != nil { + // Check if the error is sys.ExitError with exit code 0 which indicates success + var exitErr *sys.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() == 0 { + // Exit code 0 means success, so we can continue + err = nil + } else { + return nil, err } - }) - runtime.SetFinalizer(proc, func(p *Proc) { - c.Host.RemoveStreamHandler(e.Protocol()) - }) + } + + // Check if module is closed after instantiation + // In sync mode, this is expected behavior as main() completes and exits + if mod.IsClosed() && !c.Async { + // In sync mode, module closure after successful execution is normal + // We'll create a minimal proc that can still be used for ID purposes + } else if mod.IsClosed() { + return nil, fmt.Errorf("module closed immediately after instantiation") + } + cs = append(cs, mod) + + // Mark proc as initialized and optionally bind stream handler. + //// ok = true + proc := &Proc{ + Config: c, + Module: mod, + Endpoint: e, + Closer: cs} return proc, nil } +type ReadWriteStringer interface { + String() string + io.ReadWriter +} + +func (c ProcConfig) NewModuleConfig(sock ReadWriteStringer) wazero.ModuleConfig { + config := wazero.NewModuleConfig(). + WithName(sock.String()). + WithArgs(c.Args...). + WithStdin(sock). + WithStdout(sock). + WithStderr(c.ErrWriter) + + // async mode? + if c.Async { + // prevent _start from running automatically + config = config.WithStartFunctions() + } + + // Add environment variables + for _, env := range c.Env { + if k, v, ok := strings.Cut(env, "="); ok { + config = config.WithEnv(k, v) + } + } + + return config +} + +func (p ProcConfig) NewEndpoint() *Endpoint { + var buf [8]byte + if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil { + panic(err) + } + + return &Endpoint{ + Name: base58.FastBase58Encoding(buf[:]), + sem: semaphore.NewWeighted(1), + } +} + type Proc struct { - Sys api.Closer + Config ProcConfig Endpoint *Endpoint Module api.Module + api.Closer } // ID returns the process identifier (endpoint name) without the protocol prefix. -func (p *Proc) ID() string { +func (p Proc) ID() string { return p.Endpoint.Name } -func (p *Proc) Close(ctx context.Context) error { - return multierr.Combine( - p.Endpoint.Close(), - p.Module.Close(ctx), - p.Sys.Close(ctx)) -} - -func (p Proc) Poll(ctx context.Context, s network.Stream, stack []uint64) error { +// ProcessMessage processes one complete message synchronously. +// In sync mode: lets _start run automatically and process one message +// In async mode: calls the poll export function +func (p Proc) ProcessMessage(ctx context.Context, s network.Stream) error { if deadline, ok := ctx.Deadline(); ok { if err := s.SetReadDeadline(deadline); err != nil { return fmt.Errorf("set read deadline: %w", err) } } + // Check if module is closed before we start + if p.Module.IsClosed() { + return fmt.Errorf("%s::ProcessMessage: module closed", p.ID()) + } + + // Set the stream as the endpoint's ReadWriteCloser for this message + // The Endpoint's Read/Write methods will delegate to the stream p.Endpoint.ReadWriteCloser = s defer func() { + // Reset to nil after processing this message p.Endpoint.ReadWriteCloser = nil }() - if poll := p.Module.ExportedFunction("poll"); poll == nil { - return fmt.Errorf("%s::poll: not found", p) - } else { - return poll.CallWithStack(ctx, stack) + // In async mode, call the poll export function + if p.Config.Async { + if poll := p.Module.ExportedFunction("poll"); poll == nil { + return fmt.Errorf("%s::poll: not found", p.ID()) + } else if err := poll.CallWithStack(ctx, nil); err != nil { + var exitErr *sys.ExitError + if errors.As(err, &exitErr) && exitErr.ExitCode() != 0 { + return fmt.Errorf("%s::poll: %w", p.ID(), err) + } + // If it's ExitError with code 0, treat as success + } + } + // In sync mode, _start already ran during module instantiation + + return nil +} + +// Poll is an alias for ProcessMessage for backward compatibility +func (p Proc) Poll(ctx context.Context, s network.Stream, stack []uint64) error { + return p.ProcessMessage(ctx, s) +} + +type CloserSlice []api.Closer + +func (cs CloserSlice) Close(ctx context.Context) error { + var errs []error + for _, c := range cs { + errs = append(errs, c.Close(ctx)) } + return multierr.Combine(errs...) } diff --git a/system/proc_test.go b/system/proc_test.go index 013e3d6..e2b68a0 100644 --- a/system/proc_test.go +++ b/system/proc_test.go @@ -5,6 +5,7 @@ import ( "context" "encoding/binary" "errors" + "fmt" "io" "os" "testing" @@ -15,6 +16,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "github.com/tetratelabs/wazero" + "github.com/tetratelabs/wazero/imports/wasi_snapshot_preview1" "github.com/wetware/go/system" "github.com/wetware/go/system/mocks" "go.uber.org/mock/gomock" @@ -42,6 +44,8 @@ func loadEchoWasm(t *testing.T) []byte { } func TestProcConfig_New(t *testing.T) { + t.Parallel() + ctx := context.Background() mockErrWriter := &bytes.Buffer{} @@ -62,14 +66,15 @@ func TestProcConfig_New(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: validBytecode, + Src: io.NopCloser(bytes.NewReader(validBytecode)), ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) require.NoError(t, err) require.NotNil(t, proc) - assert.NotNil(t, proc.Sys) + assert.NotNil(t, proc.Closer) assert.NotNil(t, proc.Module) assert.NotNil(t, proc.Endpoint) @@ -95,8 +100,9 @@ func TestProcConfig_New(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: []byte("invalid wasm bytecode"), + Src: io.NopCloser(bytes.NewReader([]byte("invalid wasm bytecode"))), ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) @@ -115,8 +121,9 @@ func TestProcConfig_New(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: nil, - Bytecode: []byte{}, + Src: io.NopCloser(bytes.NewReader([]byte{})), ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } // This will panic due to nil runtime, so we expect a panic @@ -127,7 +134,9 @@ func TestProcConfig_New(t *testing.T) { } func TestProc_ID(t *testing.T) { - endpoint := system.NewEndpoint() + t.Parallel() + + endpoint := system.ProcConfig{}.NewEndpoint() proc := &system.Proc{ Endpoint: endpoint, } @@ -139,6 +148,8 @@ func TestProc_ID(t *testing.T) { } func TestProc_Close(t *testing.T) { + t.Parallel() + ctx := context.Background() t.Run("successful close", func(t *testing.T) { @@ -157,8 +168,9 @@ func TestProc_Close(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: validBytecode, + Src: io.NopCloser(bytes.NewReader(validBytecode)), ErrWriter: &bytes.Buffer{}, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) @@ -172,6 +184,7 @@ func TestProc_Close(t *testing.T) { } func TestProc_Poll_WithGomock(t *testing.T) { + t.Parallel() ctx := context.Background() t.Run("successful poll", func(t *testing.T) { @@ -188,23 +201,22 @@ func TestProc_Poll_WithGomock(t *testing.T) { defer host.Close() runtime := wazero.NewRuntime(ctx) - defer runtime.Close(ctx) validBytecode := loadEchoWasm(t) config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: validBytecode, + Src: io.NopCloser(bytes.NewReader(validBytecode)), ErrWriter: &bytes.Buffer{}, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) require.NoError(t, err) require.NotNil(t, proc) defer proc.Close(ctx) - - stack := []uint64{1, 2, 3} + defer runtime.Close(ctx) // Test that poll function exists pollFunc := proc.Module.ExportedFunction("poll") @@ -215,7 +227,7 @@ func TestProc_Poll_WithGomock(t *testing.T) { mockStream.EXPECT().Read(gomock.Any()).Return(0, io.EOF).AnyTimes() // Actually call the Poll method - this should succeed since we have a valid WASM function - err = proc.Poll(ctx, mockStream, stack) + err = proc.ProcessMessage(ctx, mockStream) // The WASM function should execute successfully assert.NoError(t, err) }) @@ -239,35 +251,34 @@ func TestProc_Poll_WithGomock(t *testing.T) { defer host.Close() runtime := wazero.NewRuntime(ctx) - defer runtime.Close(ctx) validBytecode := loadEchoWasm(t) config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: validBytecode, + Src: io.NopCloser(bytes.NewReader(validBytecode)), ErrWriter: &bytes.Buffer{}, + Async: true, // Use async mode for these tests } proc, err := config.New(ctxWithDeadline) require.NoError(t, err) require.NotNil(t, proc) defer proc.Close(ctxWithDeadline) - - stack := []uint64{1, 2, 3} + defer runtime.Close(ctx) // Set up mock expectations mockStream.EXPECT().SetReadDeadline(deadline).Return(nil) // The echo WASM module will try to read from stdin mockStream.EXPECT().Read(gomock.Any()).Return(0, io.EOF).AnyTimes() - // Test that poll function exists + // Test that poll function exists (for async mode) pollFunc := proc.Module.ExportedFunction("poll") assert.NotNil(t, pollFunc, "poll function should exist") - // Actually call the Poll method to trigger the mock expectations - err = proc.Poll(ctxWithDeadline, mockStream, stack) + // Actually call the ProcessMessage method to trigger the mock expectations + err = proc.ProcessMessage(ctxWithDeadline, mockStream) // The WASM function should execute successfully assert.NoError(t, err) }) @@ -277,7 +288,7 @@ func TestProc_Poll_WithGomock(t *testing.T) { defer ctrl.Finish() mockStream := mocks.NewMockStreamInterface(ctrl) - endpoint := system.NewEndpoint() + endpoint := system.ProcConfig{}.NewEndpoint() // Create context with deadline deadline := time.Now().Add(time.Hour) @@ -288,13 +299,11 @@ func TestProc_Poll_WithGomock(t *testing.T) { Endpoint: endpoint, } - stack := []uint64{1, 2, 3} - // Set up mock expectations deadlineErr := errors.New("deadline error") mockStream.EXPECT().SetReadDeadline(deadline).Return(deadlineErr) - err := proc.Poll(ctxWithDeadline, mockStream, stack) + err := proc.ProcessMessage(ctxWithDeadline, mockStream) assert.Error(t, err) assert.Contains(t, err.Error(), "set read deadline") assert.Contains(t, err.Error(), "deadline error") @@ -318,8 +327,9 @@ func TestProc_Poll_WithGomock(t *testing.T) { configNoPoll := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: noPollBytecode, + Src: io.NopCloser(bytes.NewReader(noPollBytecode)), ErrWriter: &bytes.Buffer{}, + Async: true, // Use async mode for these tests } // This should fail to create the module due to invalid bytecode @@ -330,6 +340,8 @@ func TestProc_Poll_WithGomock(t *testing.T) { } func TestProc_StreamHandler_WithGomock(t *testing.T) { + t.Parallel() + ctx := context.Background() mockErrWriter := &bytes.Buffer{} @@ -356,8 +368,9 @@ func TestProc_StreamHandler_WithGomock(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: validBytecode, + Src: io.NopCloser(bytes.NewReader(validBytecode)), ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) @@ -407,7 +420,9 @@ func TestProc_StreamHandler_WithGomock(t *testing.T) { } func TestNewEndpoint(t *testing.T) { - endpoint := system.NewEndpoint() + t.Parallel() + + endpoint := system.ProcConfig{}.NewEndpoint() assert.NotNil(t, endpoint) assert.NotEmpty(t, endpoint.Name) @@ -422,18 +437,21 @@ func TestNewEndpoint(t *testing.T) { } func TestEndpoint_String(t *testing.T) { - endpoint := system.NewEndpoint() + t.Parallel() + + endpoint := system.ProcConfig{}.NewEndpoint() result := endpoint.String() - expected := string(endpoint.Protocol()) + expected := endpoint.Name assert.Equal(t, expected, result) - assert.Contains(t, result, "/ww/0.1.0/") - assert.Contains(t, result, endpoint.Name) + assert.Equal(t, endpoint.Name, result) } func TestEndpoint_Protocol(t *testing.T) { - endpoint := system.NewEndpoint() + t.Parallel() + + endpoint := system.ProcConfig{}.NewEndpoint() protocol := endpoint.Protocol() @@ -442,6 +460,8 @@ func TestEndpoint_Protocol(t *testing.T) { } func TestProcConfig_New_ErrorHandling(t *testing.T) { + t.Parallel() + ctx := context.Background() mockErrWriter := &bytes.Buffer{} @@ -452,8 +472,9 @@ func TestProcConfig_New_ErrorHandling(t *testing.T) { config := system.ProcConfig{ Host: nil, Runtime: runtime, - Bytecode: []byte{}, + Src: io.NopCloser(bytes.NewReader([]byte{})), ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) @@ -468,8 +489,9 @@ func TestProcConfig_New_ErrorHandling(t *testing.T) { config := system.ProcConfig{ Host: nil, Runtime: runtime, - Bytecode: nil, + Src: nil, ErrWriter: mockErrWriter, + Async: true, // Use async mode for these tests } proc, err := config.New(ctx) @@ -480,8 +502,8 @@ func TestProcConfig_New_ErrorHandling(t *testing.T) { func TestEndpoint_Concurrency(t *testing.T) { // Test that multiple endpoints have different names - endpoint1 := system.NewEndpoint() - endpoint2 := system.NewEndpoint() + endpoint1 := system.ProcConfig{}.NewEndpoint() + endpoint2 := system.ProcConfig{}.NewEndpoint() assert.NotEqual(t, endpoint1.Name, endpoint2.Name, "Endpoints should have unique names") assert.NotEqual(t, endpoint1.String(), endpoint2.String(), "Endpoint strings should be different") @@ -508,8 +530,9 @@ func TestProc_Integration_WithRealWasm(t *testing.T) { config := system.ProcConfig{ Host: host, Runtime: runtime, - Bytecode: completeBytecode, + Src: io.NopCloser(bytes.NewReader(completeBytecode)), ErrWriter: &bytes.Buffer{}, + Async: true, // Use async mode for integration test } proc, err := config.New(ctx) @@ -518,7 +541,7 @@ func TestProc_Integration_WithRealWasm(t *testing.T) { defer proc.Close(ctx) // Test that all components are properly initialized - assert.NotNil(t, proc.Sys, "Sys should be initialized") + assert.NotNil(t, proc.Closer, "Closer should be initialized") assert.NotNil(t, proc.Module, "Module should be initialized") assert.NotNil(t, proc.Endpoint, "Endpoint should be initialized") assert.NotEmpty(t, proc.Endpoint.Name, "Endpoint should have a name") @@ -528,3 +551,268 @@ func TestProc_Integration_WithRealWasm(t *testing.T) { pollFunc := proc.Module.ExportedFunction("poll") assert.NotNil(t, pollFunc, "poll function should be exported") } + +// TestEcho_Synchronous tests the echo example in synchronous mode +// where main() is called directly and processes stdin to stdout +func TestEcho_Synchronous(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Create a libp2p host with in-process transport + host, err := libp2p.New( + libp2p.Transport(inproc.New()), + ) + require.NoError(t, err) + defer host.Close() + + // Create runtime and load echo WASM + runtime := wazero.NewRuntime(ctx) + defer runtime.Close(ctx) + + bytecode := loadEchoWasm(t) + + // Test input data + testInput := "Hello, World!\nThis is a test message.\n" + expectedOutput := testInput + + // Create a pipe to simulate stdin/stdout + reader, writer := io.Pipe() + + // Write test data to the writer end + go func() { + defer writer.Close() + writer.Write([]byte(testInput)) + }() + + // Create a buffer to capture output + var outputBuffer bytes.Buffer + + // Compile the module first + cm, err := runtime.CompileModule(ctx, bytecode) + require.NoError(t, err) + defer cm.Close(ctx) + + // Instantiate WASI + wasi, err := wasi_snapshot_preview1.Instantiate(ctx, runtime) + require.NoError(t, err) + defer wasi.Close(ctx) + + // Create a new module instance with the pipe as stdin/stdout + // In sync mode, _start runs automatically and processes stdin/stdout + mod, err := runtime.InstantiateModule(ctx, cm, wazero.NewModuleConfig(). + WithName("echo-sync-test"). + WithSysNanosleep(). + WithSysNanotime(). + WithSysWalltime(). + WithStdin(reader). // Use pipe reader as stdin + WithStdout(&outputBuffer). // Capture output + WithStderr(&bytes.Buffer{})) + require.NoError(t, err) + defer mod.Close(ctx) + + // Wait for the pipe to be closed (indicating EOF) + time.Sleep(100 * time.Millisecond) + + // Verify the output matches the input + output := outputBuffer.String() + assert.Equal(t, expectedOutput, output, "Echo should output exactly what was input") +} + +// TestEcho_Asynchronous tests the echo example in asynchronous mode +// where poll() is called with a stream and processes one complete message +func TestEcho_Asynchronous(t *testing.T) { + // t.Parallel() // Temporarily disabled to debug runtime close issue + ctx := context.Background() + + // Create a libp2p host with in-process transport + host, err := libp2p.New( + libp2p.Transport(inproc.New()), + ) + require.NoError(t, err) + defer host.Close() + + // Create runtime and load echo WASM + runtime := wazero.NewRuntime(ctx) + defer runtime.Close(ctx) + + bytecode := loadEchoWasm(t) + config := system.ProcConfig{ + Host: host, + Runtime: runtime, + Src: io.NopCloser(bytes.NewReader(bytecode)), + ErrWriter: &bytes.Buffer{}, + Async: true, // Async mode + } + + proc, err := config.New(ctx) + require.NoError(t, err) + defer proc.Close(ctx) + + // In async mode, _start is prevented from running automatically + // We'll call poll() for each message + + // Test input data + testInput := "Hello, Async World!\nThis is an async test message.\n" + expectedOutput := testInput + + // Create a mock stream using gomock + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStream := mocks.NewMockStreamInterface(ctrl) + writeBuffer := &bytes.Buffer{} + + // Set up expectations for the mock stream + // The echo module will read from stdin until EOF, then write to stdout + mockStream.EXPECT(). + Read(gomock.Any()). + DoAndReturn(func(p []byte) (int, error) { + if len(testInput) == 0 { + return 0, io.EOF + } + n := copy(p, testInput) + testInput = testInput[n:] + return n, nil + }). + AnyTimes() + + mockStream.EXPECT(). + Write(gomock.Any()). + DoAndReturn(func(p []byte) (int, error) { + return writeBuffer.Write(p) + }). + AnyTimes() + + mockStream.EXPECT(). + Close(). + Return(nil). + AnyTimes() + + // Process message with the mock stream + // This should process one complete message (until EOF) + err = proc.ProcessMessage(ctx, mockStream) + require.NoError(t, err, "ProcessMessage should succeed") + + // Verify the output matches the input + output := writeBuffer.String() + assert.Equal(t, expectedOutput, output, "Async echo should output exactly what was input") +} + +// TestEcho_RepeatedAsync tests multiple asynchronous calls to verify +// the pattern works consistently across multiple messages +func TestEcho_RepeatedAsync(t *testing.T) { + t.Parallel() + + ctx := context.Background() + + // Create a libp2p host with in-process transport + host, err := libp2p.New( + libp2p.Transport(inproc.New()), + ) + require.NoError(t, err) + defer host.Close() + + bytecode := loadEchoWasm(t) + + // Test multiple messages + testMessages := []string{ + "Message 1: Hello World!\n", + "Message 2: This is a test.\n", + "Message 3: Multiple async calls.\n", + "Message 4: Each should be processed independently.\n", + "Message 5: One stream = one message.\n", + } + + // Process each message with a separate poll call + for i, testInput := range testMessages { + t.Run(fmt.Sprintf("message_%d", i+1), func(t *testing.T) { + t.Parallel() + + // Create a fresh runtime and proc for each sub-test + runtime := wazero.NewRuntime(ctx) + defer runtime.Close(ctx) + + config := system.ProcConfig{ + Host: host, + Runtime: runtime, + Src: io.NopCloser(bytes.NewReader(bytecode)), + ErrWriter: &bytes.Buffer{}, + Async: true, // Async mode + } + + proc, err := config.New(ctx) + require.NoError(t, err) + defer proc.Close(ctx) + + // Create a fresh mock stream for each message + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + mockStream := mocks.NewMockStreamInterface(ctrl) + writeBuffer := &bytes.Buffer{} + + // Create a local copy of testInput to avoid closure issues + localTestInput := testInput + + // Set up expectations for the mock stream + mockStream.EXPECT(). + Read(gomock.Any()). + DoAndReturn(func(p []byte) (int, error) { + if len(localTestInput) == 0 { + return 0, io.EOF + } + n := copy(p, localTestInput) + localTestInput = localTestInput[n:] + return n, nil + }). + AnyTimes() + + mockStream.EXPECT(). + Write(gomock.Any()). + DoAndReturn(func(p []byte) (int, error) { + return writeBuffer.Write(p) + }). + AnyTimes() + + mockStream.EXPECT(). + Close(). + Return(nil). + AnyTimes() + + // Process message with the mock stream + err = proc.ProcessMessage(ctx, mockStream) + require.NoError(t, err, "ProcessMessage should succeed for message %d", i+1) + + // Verify the output matches the input + output := writeBuffer.String() + assert.Equal(t, testInput, output, "Message %d should be echoed correctly", i+1) + }) + } +} + +// TestEndpoint_NilReadWriteCloser tests that Endpoint handles nil ReadWriteCloser correctly +func TestEndpoint_NilReadWriteCloser(t *testing.T) { + t.Parallel() + + // Create an endpoint with nil ReadWriteCloser + endpoint := &system.Endpoint{ + Name: "test-endpoint", + // ReadWriteCloser is nil + } + + // Test Read returns EOF immediately + buf := make([]byte, 10) + n, err := endpoint.Read(buf) + assert.Equal(t, 0, n, "Read should return 0 bytes") + assert.Equal(t, io.EOF, err, "Read should return EOF") + + // Test Write discards data + n, err = endpoint.Write([]byte("test data")) + assert.Equal(t, 9, n, "Write should return length of data") + assert.NoError(t, err, "Write should not return error") + + // Test Close doesn't panic + err = endpoint.Close(context.Background()) + assert.NoError(t, err, "Close should not return error") +} diff --git a/system/socket.go b/system/socket.go deleted file mode 100644 index d256f5a..0000000 --- a/system/socket.go +++ /dev/null @@ -1,56 +0,0 @@ -package system - -import ( - "context" - "os" - "syscall" - - capnp "capnproto.org/go/capnp/v3" - "capnproto.org/go/capnp/v3/rpc" -) - -const BOOTSTRAP_FD = 3 - -// SocketConfig configures a pair of Unix domain sockets for IPC between -// a host process and guest process. -type SocketConfig[T ~capnp.ClientKind] struct { - // BootstrapClient is the client that will be passed down to the guest - // process. It is used to bootstrap the guest process's RPC connection. - // - // The socket steals the reference to T, and releases it when the conn - // is closed. - BootstrapClient T -} - -// New creates a pair of connected Unix domain sockets. The host socket is -// returned first, followed by the guest socket. The caller is responsible -// for closing both sockets. -func (c SocketConfig[T]) New(ctx context.Context) (*rpc.Conn, *os.File, error) { - host, guest, err := c.Socketpair() - if err != nil { - return nil, nil, err - } - - conn := rpc.NewConn(rpc.NewStreamTransport(host), c.NewRPCOptions(ctx)) - return conn, guest, nil -} - -func (c SocketConfig[T]) NewRPCOptions(ctx context.Context) *rpc.Options { - return &rpc.Options{ - BaseContext: func() context.Context { return ctx }, - BootstrapClient: capnp.Client(c.BootstrapClient), - } -} - -// socketpair creates a pair of connected Unix domain sockets for bidirectional communication -func (SocketConfig[T]) Socketpair() (*os.File, *os.File, error) { - fds, err := syscall.Socketpair(syscall.AF_UNIX, syscall.SOCK_STREAM, 0) - if err != nil { - return nil, nil, err - } - - host := os.NewFile(uintptr(fds[0]), "host") - guest := os.NewFile(uintptr(fds[1]), "guest") - - return host, guest, nil -} diff --git a/system/system.capnp b/system/system.capnp deleted file mode 100644 index d3494d7..0000000 --- a/system/system.capnp +++ /dev/null @@ -1,17 +0,0 @@ -using Go = import "/go.capnp"; - -@0xda965b22da734daf; - -$Go.package("system"); -$Go.import("github.com/wetware/go/system"); - - -interface Terminal { - login @0 () -> ( - exec :Executor, - ); -} - -interface Executor { - exec @0 (bytecode :Data) -> (protocol :Text); -} diff --git a/system/system.capnp.go b/system/system.capnp.go deleted file mode 100644 index 0afb1d6..0000000 --- a/system/system.capnp.go +++ /dev/null @@ -1,685 +0,0 @@ -// Code generated by capnpc-go. DO NOT EDIT. - -package system - -import ( - capnp "capnproto.org/go/capnp/v3" - text "capnproto.org/go/capnp/v3/encoding/text" - fc "capnproto.org/go/capnp/v3/flowcontrol" - schemas "capnproto.org/go/capnp/v3/schemas" - server "capnproto.org/go/capnp/v3/server" - context "context" -) - -type Terminal capnp.Client - -// Terminal_TypeID is the unique identifier for the type Terminal. -const Terminal_TypeID = 0xab27f53bfa9ddf26 - -func (c Terminal) Login(ctx context.Context, params func(Terminal_login_Params) error) (Terminal_login_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xab27f53bfa9ddf26, - MethodID: 0, - InterfaceName: "system.capnp:Terminal", - MethodName: "login", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 0} - s.PlaceArgs = func(s capnp.Struct) error { return params(Terminal_login_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Terminal_login_Results_Future{Future: ans.Future()}, release - -} - -func (c Terminal) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c Terminal) String() string { - return "Terminal(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c Terminal) AddRef() Terminal { - return Terminal(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c Terminal) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c Terminal) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c Terminal) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (Terminal) DecodeFromPtr(p capnp.Ptr) Terminal { - return Terminal(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c Terminal) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c Terminal) IsSame(other Terminal) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c Terminal) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c Terminal) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A Terminal_Server is a Terminal with a local implementation. -type Terminal_Server interface { - Login(context.Context, Terminal_login) error -} - -// Terminal_NewServer creates a new Server from an implementation of Terminal_Server. -func Terminal_NewServer(s Terminal_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(Terminal_Methods(nil, s), s, c) -} - -// Terminal_ServerToClient creates a new Client from an implementation of Terminal_Server. -// The caller is responsible for calling Release on the returned Client. -func Terminal_ServerToClient(s Terminal_Server) Terminal { - return Terminal(capnp.NewClient(Terminal_NewServer(s))) -} - -// Terminal_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func Terminal_Methods(methods []server.Method, s Terminal_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 1) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xab27f53bfa9ddf26, - MethodID: 0, - InterfaceName: "system.capnp:Terminal", - MethodName: "login", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Login(ctx, Terminal_login{call}) - }, - }) - - return methods -} - -// Terminal_login holds the state for a server call to Terminal.login. -// See server.Call for documentation. -type Terminal_login struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Terminal_login) Args() Terminal_login_Params { - return Terminal_login_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Terminal_login) AllocResults() (Terminal_login_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Terminal_login_Results(r), err -} - -// Terminal_List is a list of Terminal. -type Terminal_List = capnp.CapList[Terminal] - -// NewTerminal creates a new list of Terminal. -func NewTerminal_List(s *capnp.Segment, sz int32) (Terminal_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[Terminal](l), err -} - -type Terminal_login_Params capnp.Struct - -// Terminal_login_Params_TypeID is the unique identifier for the type Terminal_login_Params. -const Terminal_login_Params_TypeID = 0xc3cb58f30a5551af - -func NewTerminal_login_Params(s *capnp.Segment) (Terminal_login_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Terminal_login_Params(st), err -} - -func NewRootTerminal_login_Params(s *capnp.Segment) (Terminal_login_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}) - return Terminal_login_Params(st), err -} - -func ReadRootTerminal_login_Params(msg *capnp.Message) (Terminal_login_Params, error) { - root, err := msg.Root() - return Terminal_login_Params(root.Struct()), err -} - -func (s Terminal_login_Params) String() string { - str, _ := text.Marshal(0xc3cb58f30a5551af, capnp.Struct(s)) - return str -} - -func (s Terminal_login_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Terminal_login_Params) DecodeFromPtr(p capnp.Ptr) Terminal_login_Params { - return Terminal_login_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Terminal_login_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Terminal_login_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Terminal_login_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Terminal_login_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} - -// Terminal_login_Params_List is a list of Terminal_login_Params. -type Terminal_login_Params_List = capnp.StructList[Terminal_login_Params] - -// NewTerminal_login_Params creates a new list of Terminal_login_Params. -func NewTerminal_login_Params_List(s *capnp.Segment, sz int32) (Terminal_login_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 0}, sz) - return capnp.StructList[Terminal_login_Params](l), err -} - -// Terminal_login_Params_Future is a wrapper for a Terminal_login_Params promised by a client call. -type Terminal_login_Params_Future struct{ *capnp.Future } - -func (f Terminal_login_Params_Future) Struct() (Terminal_login_Params, error) { - p, err := f.Future.Ptr() - return Terminal_login_Params(p.Struct()), err -} - -type Terminal_login_Results capnp.Struct - -// Terminal_login_Results_TypeID is the unique identifier for the type Terminal_login_Results. -const Terminal_login_Results_TypeID = 0xa584e813c754d96d - -func NewTerminal_login_Results(s *capnp.Segment) (Terminal_login_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Terminal_login_Results(st), err -} - -func NewRootTerminal_login_Results(s *capnp.Segment) (Terminal_login_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Terminal_login_Results(st), err -} - -func ReadRootTerminal_login_Results(msg *capnp.Message) (Terminal_login_Results, error) { - root, err := msg.Root() - return Terminal_login_Results(root.Struct()), err -} - -func (s Terminal_login_Results) String() string { - str, _ := text.Marshal(0xa584e813c754d96d, capnp.Struct(s)) - return str -} - -func (s Terminal_login_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Terminal_login_Results) DecodeFromPtr(p capnp.Ptr) Terminal_login_Results { - return Terminal_login_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Terminal_login_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Terminal_login_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Terminal_login_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Terminal_login_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Terminal_login_Results) Exec() Executor { - p, _ := capnp.Struct(s).Ptr(0) - return Executor(p.Interface().Client()) -} - -func (s Terminal_login_Results) HasExec() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Terminal_login_Results) SetExec(v Executor) error { - if !v.IsValid() { - return capnp.Struct(s).SetPtr(0, capnp.Ptr{}) - } - seg := s.Segment() - in := capnp.NewInterface(seg, seg.Message().CapTable().Add(capnp.Client(v))) - return capnp.Struct(s).SetPtr(0, in.ToPtr()) -} - -// Terminal_login_Results_List is a list of Terminal_login_Results. -type Terminal_login_Results_List = capnp.StructList[Terminal_login_Results] - -// NewTerminal_login_Results creates a new list of Terminal_login_Results. -func NewTerminal_login_Results_List(s *capnp.Segment, sz int32) (Terminal_login_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Terminal_login_Results](l), err -} - -// Terminal_login_Results_Future is a wrapper for a Terminal_login_Results promised by a client call. -type Terminal_login_Results_Future struct{ *capnp.Future } - -func (f Terminal_login_Results_Future) Struct() (Terminal_login_Results, error) { - p, err := f.Future.Ptr() - return Terminal_login_Results(p.Struct()), err -} -func (p Terminal_login_Results_Future) Exec() Executor { - return Executor(p.Future.Field(0, nil).Client()) -} - -type Executor capnp.Client - -// Executor_TypeID is the unique identifier for the type Executor. -const Executor_TypeID = 0xccef5e6e46834543 - -func (c Executor) Exec(ctx context.Context, params func(Executor_exec_Params) error) (Executor_exec_Results_Future, capnp.ReleaseFunc) { - - s := capnp.Send{ - Method: capnp.Method{ - InterfaceID: 0xccef5e6e46834543, - MethodID: 0, - InterfaceName: "system.capnp:Executor", - MethodName: "exec", - }, - } - if params != nil { - s.ArgsSize = capnp.ObjectSize{DataSize: 0, PointerCount: 1} - s.PlaceArgs = func(s capnp.Struct) error { return params(Executor_exec_Params(s)) } - } - - ans, release := capnp.Client(c).SendCall(ctx, s) - return Executor_exec_Results_Future{Future: ans.Future()}, release - -} - -func (c Executor) WaitStreaming() error { - return capnp.Client(c).WaitStreaming() -} - -// String returns a string that identifies this capability for debugging -// purposes. Its format should not be depended on: in particular, it -// should not be used to compare clients. Use IsSame to compare clients -// for equality. -func (c Executor) String() string { - return "Executor(" + capnp.Client(c).String() + ")" -} - -// AddRef creates a new Client that refers to the same capability as c. -// If c is nil or has resolved to null, then AddRef returns nil. -func (c Executor) AddRef() Executor { - return Executor(capnp.Client(c).AddRef()) -} - -// Release releases a capability reference. If this is the last -// reference to the capability, then the underlying resources associated -// with the capability will be released. -// -// Release will panic if c has already been released, but not if c is -// nil or resolved to null. -func (c Executor) Release() { - capnp.Client(c).Release() -} - -// Resolve blocks until the capability is fully resolved or the Context -// expires. -func (c Executor) Resolve(ctx context.Context) error { - return capnp.Client(c).Resolve(ctx) -} - -func (c Executor) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Client(c).EncodeAsPtr(seg) -} - -func (Executor) DecodeFromPtr(p capnp.Ptr) Executor { - return Executor(capnp.Client{}.DecodeFromPtr(p)) -} - -// IsValid reports whether c is a valid reference to a capability. -// A reference is invalid if it is nil, has resolved to null, or has -// been released. -func (c Executor) IsValid() bool { - return capnp.Client(c).IsValid() -} - -// IsSame reports whether c and other refer to a capability created by the -// same call to NewClient. This can return false negatives if c or other -// are not fully resolved: use Resolve if this is an issue. If either -// c or other are released, then IsSame panics. -func (c Executor) IsSame(other Executor) bool { - return capnp.Client(c).IsSame(capnp.Client(other)) -} - -// Update the flowcontrol.FlowLimiter used to manage flow control for -// this client. This affects all future calls, but not calls already -// waiting to send. Passing nil sets the value to flowcontrol.NopLimiter, -// which is also the default. -func (c Executor) SetFlowLimiter(lim fc.FlowLimiter) { - capnp.Client(c).SetFlowLimiter(lim) -} - -// Get the current flowcontrol.FlowLimiter used to manage flow control -// for this client. -func (c Executor) GetFlowLimiter() fc.FlowLimiter { - return capnp.Client(c).GetFlowLimiter() -} - -// A Executor_Server is a Executor with a local implementation. -type Executor_Server interface { - Exec(context.Context, Executor_exec) error -} - -// Executor_NewServer creates a new Server from an implementation of Executor_Server. -func Executor_NewServer(s Executor_Server) *server.Server { - c, _ := s.(server.Shutdowner) - return server.New(Executor_Methods(nil, s), s, c) -} - -// Executor_ServerToClient creates a new Client from an implementation of Executor_Server. -// The caller is responsible for calling Release on the returned Client. -func Executor_ServerToClient(s Executor_Server) Executor { - return Executor(capnp.NewClient(Executor_NewServer(s))) -} - -// Executor_Methods appends Methods to a slice that invoke the methods on s. -// This can be used to create a more complicated Server. -func Executor_Methods(methods []server.Method, s Executor_Server) []server.Method { - if cap(methods) == 0 { - methods = make([]server.Method, 0, 1) - } - - methods = append(methods, server.Method{ - Method: capnp.Method{ - InterfaceID: 0xccef5e6e46834543, - MethodID: 0, - InterfaceName: "system.capnp:Executor", - MethodName: "exec", - }, - Impl: func(ctx context.Context, call *server.Call) error { - return s.Exec(ctx, Executor_exec{call}) - }, - }) - - return methods -} - -// Executor_exec holds the state for a server call to Executor.exec. -// See server.Call for documentation. -type Executor_exec struct { - *server.Call -} - -// Args returns the call's arguments. -func (c Executor_exec) Args() Executor_exec_Params { - return Executor_exec_Params(c.Call.Args()) -} - -// AllocResults allocates the results struct. -func (c Executor_exec) AllocResults() (Executor_exec_Results, error) { - r, err := c.Call.AllocResults(capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Executor_exec_Results(r), err -} - -// Executor_List is a list of Executor. -type Executor_List = capnp.CapList[Executor] - -// NewExecutor creates a new list of Executor. -func NewExecutor_List(s *capnp.Segment, sz int32) (Executor_List, error) { - l, err := capnp.NewPointerList(s, sz) - return capnp.CapList[Executor](l), err -} - -type Executor_exec_Params capnp.Struct - -// Executor_exec_Params_TypeID is the unique identifier for the type Executor_exec_Params. -const Executor_exec_Params_TypeID = 0xb8078038290eb30b - -func NewExecutor_exec_Params(s *capnp.Segment) (Executor_exec_Params, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Executor_exec_Params(st), err -} - -func NewRootExecutor_exec_Params(s *capnp.Segment) (Executor_exec_Params, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Executor_exec_Params(st), err -} - -func ReadRootExecutor_exec_Params(msg *capnp.Message) (Executor_exec_Params, error) { - root, err := msg.Root() - return Executor_exec_Params(root.Struct()), err -} - -func (s Executor_exec_Params) String() string { - str, _ := text.Marshal(0xb8078038290eb30b, capnp.Struct(s)) - return str -} - -func (s Executor_exec_Params) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Executor_exec_Params) DecodeFromPtr(p capnp.Ptr) Executor_exec_Params { - return Executor_exec_Params(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Executor_exec_Params) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Executor_exec_Params) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Executor_exec_Params) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Executor_exec_Params) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Executor_exec_Params) Bytecode() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return []byte(p.Data()), err -} - -func (s Executor_exec_Params) HasBytecode() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Executor_exec_Params) SetBytecode(v []byte) error { - return capnp.Struct(s).SetData(0, v) -} - -// Executor_exec_Params_List is a list of Executor_exec_Params. -type Executor_exec_Params_List = capnp.StructList[Executor_exec_Params] - -// NewExecutor_exec_Params creates a new list of Executor_exec_Params. -func NewExecutor_exec_Params_List(s *capnp.Segment, sz int32) (Executor_exec_Params_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Executor_exec_Params](l), err -} - -// Executor_exec_Params_Future is a wrapper for a Executor_exec_Params promised by a client call. -type Executor_exec_Params_Future struct{ *capnp.Future } - -func (f Executor_exec_Params_Future) Struct() (Executor_exec_Params, error) { - p, err := f.Future.Ptr() - return Executor_exec_Params(p.Struct()), err -} - -type Executor_exec_Results capnp.Struct - -// Executor_exec_Results_TypeID is the unique identifier for the type Executor_exec_Results. -const Executor_exec_Results_TypeID = 0xa04c2cd2ed66b51c - -func NewExecutor_exec_Results(s *capnp.Segment) (Executor_exec_Results, error) { - st, err := capnp.NewStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Executor_exec_Results(st), err -} - -func NewRootExecutor_exec_Results(s *capnp.Segment) (Executor_exec_Results, error) { - st, err := capnp.NewRootStruct(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}) - return Executor_exec_Results(st), err -} - -func ReadRootExecutor_exec_Results(msg *capnp.Message) (Executor_exec_Results, error) { - root, err := msg.Root() - return Executor_exec_Results(root.Struct()), err -} - -func (s Executor_exec_Results) String() string { - str, _ := text.Marshal(0xa04c2cd2ed66b51c, capnp.Struct(s)) - return str -} - -func (s Executor_exec_Results) EncodeAsPtr(seg *capnp.Segment) capnp.Ptr { - return capnp.Struct(s).EncodeAsPtr(seg) -} - -func (Executor_exec_Results) DecodeFromPtr(p capnp.Ptr) Executor_exec_Results { - return Executor_exec_Results(capnp.Struct{}.DecodeFromPtr(p)) -} - -func (s Executor_exec_Results) ToPtr() capnp.Ptr { - return capnp.Struct(s).ToPtr() -} -func (s Executor_exec_Results) IsValid() bool { - return capnp.Struct(s).IsValid() -} - -func (s Executor_exec_Results) Message() *capnp.Message { - return capnp.Struct(s).Message() -} - -func (s Executor_exec_Results) Segment() *capnp.Segment { - return capnp.Struct(s).Segment() -} -func (s Executor_exec_Results) Protocol() (string, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.Text(), err -} - -func (s Executor_exec_Results) HasProtocol() bool { - return capnp.Struct(s).HasPtr(0) -} - -func (s Executor_exec_Results) ProtocolBytes() ([]byte, error) { - p, err := capnp.Struct(s).Ptr(0) - return p.TextBytes(), err -} - -func (s Executor_exec_Results) SetProtocol(v string) error { - return capnp.Struct(s).SetText(0, v) -} - -// Executor_exec_Results_List is a list of Executor_exec_Results. -type Executor_exec_Results_List = capnp.StructList[Executor_exec_Results] - -// NewExecutor_exec_Results creates a new list of Executor_exec_Results. -func NewExecutor_exec_Results_List(s *capnp.Segment, sz int32) (Executor_exec_Results_List, error) { - l, err := capnp.NewCompositeList(s, capnp.ObjectSize{DataSize: 0, PointerCount: 1}, sz) - return capnp.StructList[Executor_exec_Results](l), err -} - -// Executor_exec_Results_Future is a wrapper for a Executor_exec_Results promised by a client call. -type Executor_exec_Results_Future struct{ *capnp.Future } - -func (f Executor_exec_Results_Future) Struct() (Executor_exec_Results, error) { - p, err := f.Future.Ptr() - return Executor_exec_Results(p.Struct()), err -} - -const schema_da965b22da734daf = "x\xda\x84\x92\xbfk\x14A\x1c\xc5\xdf\x9b\xd9s\xfdq" + - "\xab\x8e#\"A9\x12\xfc\x19\xc2\x81\x8a Z\x18\x90" + - "3(\x0a7\x12A\x10\x84s\x1d%\xb0{{\xdcn" + - "\xf0\xd2\x09\xfa7\xd8\x09ZX\x88E \"\xd8[X" + - "\xa8\x8d`\xe15\xb66b!)\x14deF\xd6\x9c" + - "'\x9az\x1e\x9f\xf7\xde\xf7\xcd\xf6\xe1lp$\xba-" + - "!\xcc\xa1\xda\x86r\xcf\xf3\x9b\x9f\xdf\xcd\\x\x08\xb5" + - "\x8b@\x8d!p\xec\x11'\x08\xea'<\x0d\x96\xe9\x87" + - "\xf9W\xfa\xd3\xbd\xc7\xa3\x82\xb7\x9cr\x82\xf7^p\xe0" + - "\xe3\x83\xef\xa7V\x0f>\x85\x8ad\xb9|1\x1fN]" + - "\xbd?\x04\xa8W\xb9\xa2\x7f8\xbd\xfe\xc69=)B" + - "\xa0\xdc\xf2l\xeb\xe1\x13w\xc2\x17\xa3\xb4Mb\x87\xa3" + - ")\xe1h\xcb\xe6\xf2\xe6\xafW^\xbf\xfc%\x08\xdc\xfb" + - "q1A\x04\xe5\x99\xd6\xdd\xb3\xddk_\xde\xfc\xe5\xb3" + - "W\xac\xe8\xfd\x8e\xae'\xc5\x9c>'B\xcc\x94\xf9R" + - "^\xd8\xb4\x19\xcbN\xaf\xdb;\xd9\x1a\xd8x\xb1\xc8\xfa" + - "M;\xb0\xf1\xbeK6_Ld\x91\x9b@\x06@@" + - "@E\xe7\x01S\x974\xbb\x05\xcb^?+\xb28K" + - "\x00\xb0\x0e\xc1:8\xc6\x9b\xb7\xfdt\xa1\xdbI\x9aI" + - "vk\xa1\xeb\x81a\xf2'p\x1a0\x1b%\xcdN\xc1" + - "m\xce\x95j\xad\x00H5\xc2d\xc5lxh\x9b4" + - "\x81\xac\x01\xbfo\xc1j\x03\xa5\x8eB\xa8Z\xd8\xf0\xbe" + - "\xb3l\x93\xffm\xda\xee\xf4;)\xffY\xf4\xfaRa" + - "\xe3\xec\x86uE#\x08F\xeb\x15u<\x99\xe6c\xc9" + - "[\x03\xdb\xf0\xa6k\xc9\xab\x99Y}/\xa5\xa6}r" + - "\x7f\x0b\x1f\xfcg\x00\x00\x00\xff\xff\xbd\xa0\xb0\x92" - -func RegisterSchema(reg *schemas.Registry) { - reg.Register(&schemas.Schema{ - String: schema_da965b22da734daf, - Nodes: []uint64{ - 0xa04c2cd2ed66b51c, - 0xa584e813c754d96d, - 0xab27f53bfa9ddf26, - 0xb8078038290eb30b, - 0xc3cb58f30a5551af, - 0xccef5e6e46834543, - }, - Compressed: true, - }) -} diff --git a/system/system.go b/system/system.go index d58a481..1ce2804 100644 --- a/system/system.go +++ b/system/system.go @@ -1,124 +1,41 @@ -//go:generate capnp compile -I.. -I$GOPATH/src/capnproto.org/go/capnp/std -ogo system.capnp - package system import ( "context" - "crypto/rand" "io" - "os" - capnp "capnproto.org/go/capnp/v3" - server "capnproto.org/go/capnp/v3/server" - "github.com/libp2p/go-libp2p/core/host" "github.com/libp2p/go-libp2p/core/protocol" - "github.com/mr-tron/base58" - "github.com/tetratelabs/wazero" "golang.org/x/sync/semaphore" ) -type TerminalConfig struct { - Exec Executor -} - -func (c TerminalConfig) New() Terminal { - client := capnp.NewClient(c.NewServer()) - return Terminal(client) -} - -func (c TerminalConfig) NewServer() *server.Server { - return Terminal_NewServer(c) -} - -func (c TerminalConfig) Login(ctx context.Context, call Terminal_login) error { - results, err := call.AllocResults() - if err != nil { - return err - } - - exec := c.Exec.AddRef() - if err := results.SetExec(exec); err != nil { - defer exec.Release() - return err - } - - return nil -} - -type ExecutorConfig struct { - Host host.Host - Runtime wazero.RuntimeConfig -} - -func (c ExecutorConfig) New(ctx context.Context) Executor { - client := capnp.NewClient(c.NewServer(ctx)) - return Executor(client) -} - -func (c ExecutorConfig) NewServer(ctx context.Context) *server.Server { - return Executor_NewServer(DefaultExecutor{ - Host: c.Host, - Runtime: wazero.NewRuntimeWithConfig(ctx, c.Runtime), - }) -} - -type DefaultExecutor struct { - Background context.Context - Runtime wazero.Runtime - Host host.Host -} - -func (d DefaultExecutor) Exec(ctx context.Context, call Executor_exec) error { - b, err := call.Args().Bytecode() - if err != nil { - return err - } - - res, err := call.AllocResults() - if err != nil { - return err - } - - proc, err := ProcConfig{ - Host: d.Host, - Runtime: d.Runtime, - Bytecode: b, - ErrWriter: os.Stderr, // HACK - }.New(ctx) - if err != nil { - return err - } else if err = res.SetProtocol(proc.ID()); err != nil { - defer proc.Close(ctx) - } - - return err -} - -func newName() string { - var buf [8]byte - if _, err := io.ReadFull(rand.Reader, buf[:]); err != nil { - panic(err) - } - return base58.FastBase58Encoding(buf[:]) -} - type Endpoint struct { Name string io.ReadWriteCloser sem *semaphore.Weighted } -func NewEndpoint() *Endpoint { - return &Endpoint{ - Name: newName(), - sem: semaphore.NewWeighted(1), +// Read implements io.Reader for Endpoint +func (e *Endpoint) Read(p []byte) (n int, err error) { + if e.ReadWriteCloser == nil { + // If no stream is available, return EOF immediately + // This allows the WASM module to complete its main() function + return 0, io.EOF + } + return e.ReadWriteCloser.Read(p) +} + +// Write implements io.Writer for Endpoint +func (e *Endpoint) Write(p []byte) (n int, err error) { + if e.ReadWriteCloser == nil { + // If no stream is available, discard output + return len(p), nil } + return e.ReadWriteCloser.Write(p) } -// String returns the full protocol identifier including the /ww/0.1.0/ prefix. +// String returns the endpoint name without the protocol prefix. func (e Endpoint) String() string { - proto := e.Protocol() - return string(proto) + return e.Name } // Protocol returns the libp2p protocol ID for this endpoint. @@ -126,9 +43,9 @@ func (e Endpoint) Protocol() protocol.ID { return protocol.ID("/ww/0.1.0/" + e.Name) } -func (e *Endpoint) Close() error { +func (e *Endpoint) Close(context.Context) (err error) { if e.ReadWriteCloser != nil { - return e.ReadWriteCloser.Close() + err = e.ReadWriteCloser.Close() } - return nil + return } diff --git a/util/dht.go b/util/dht.go new file mode 100644 index 0000000..7699220 --- /dev/null +++ b/util/dht.go @@ -0,0 +1,78 @@ +package util + +import ( + "context" + "log/slog" + "math/rand" + "time" + + dht "github.com/libp2p/go-libp2p-kad-dht" + "github.com/libp2p/go-libp2p-kad-dht/dual" + "github.com/libp2p/go-libp2p/core/host" + "github.com/libp2p/go-libp2p/core/peer" +) + +// NewDHT creates a client-mode DHT seeded with IPFS peers +func (env *IPFSEnv) NewDHT(ctx context.Context, h host.Host) (*dual.DHT, error) { + known, err := env.IPFS.Swarm().KnownAddrs(ctx) + if err != nil { + return nil, err + } + + var infos []peer.AddrInfo + for id, addrs := range known { + infos = append(infos, peer.AddrInfo{ + ID: id, + Addrs: addrs, + }) + } + rand.Shuffle(len(infos), func(i, j int) { + infos[i], infos[j] = infos[j], infos[i] + }) + + slog.DebugContext(ctx, "found known peers from IPFS", + "count", len(infos)) + + return dual.New(ctx, h, dual.DHTOption( + dht.Mode(dht.ModeClient), + dht.BootstrapPeers(infos...))) +} + +// WaitForDHTReady waits for the DHT to be ready by monitoring both WAN and LAN routing tables +// +// Note: The go-libp2p-kad-dht library doesn't provide explicit events for DHT readiness. +// The recommended approach is to monitor the routing table size against the internal +// minRTRefreshThreshold. The dual DHT queries both WAN and LAN in parallel, so we +// monitor both routing tables to ensure the DHT can handle queries effectively. +func WaitForDHTReady(ctx context.Context, dht *dual.DHT) error { + // minRTRefreshThreshold is the minimum number of peers required in the routing table + // for the DHT to be considered ready. This matches the internal constant used by + // go-libp2p-kad-dht (minRTRefreshThreshold = 2). + const minRTRefreshThreshold = 2 + + slog.DebugContext(ctx, "Monitoring DHT routing tables", "wan", "lan") + + ticker := time.NewTicker(200 * time.Millisecond) + defer ticker.Stop() + + for { + select { + case <-ctx.Done(): + case <-ticker.C: + } + + // Check both routing tables periodically + // The dual DHT queries both WAN and LAN in parallel, so we need both to be ready + wanSize := dht.WAN.RoutingTable().Size() + lanSize := dht.LAN.RoutingTable().Size() + totalSize := wanSize + lanSize + + if totalSize >= minRTRefreshThreshold { + return nil + } + + if err := ctx.Err(); err != nil { + return err + } + } +} diff --git a/util/host.go b/util/host.go new file mode 100644 index 0000000..5af6417 --- /dev/null +++ b/util/host.go @@ -0,0 +1,45 @@ +package util + +import ( + "fmt" + + "github.com/libp2p/go-libp2p" + "github.com/libp2p/go-libp2p/core/host" + quic "github.com/libp2p/go-libp2p/p2p/transport/quic" + "github.com/libp2p/go-libp2p/p2p/transport/tcp" + ws "github.com/libp2p/go-libp2p/p2p/transport/websocket" + webtransport "github.com/libp2p/go-libp2p/p2p/transport/webtransport" +) + +func NewClient(opts ...libp2p.Option) (host.Host, error) { + return libp2p.New(append(DefaultClientOptions(), opts...)...) +} + +func DefaultClientOptions() []libp2p.Option { + return []libp2p.Option{ + libp2p.NoListenAddrs, + } +} + +func NewServer(port int, opts ...libp2p.Option) (host.Host, error) { + return libp2p.New(append(DefaultServerOptions(port), opts...)...) +} + +func DefaultServerOptions(port int) []libp2p.Option { + return []libp2p.Option{ + libp2p.Transport(tcp.NewTCPTransport), + libp2p.Transport(quic.NewTransport), + libp2p.Transport(ws.New), + libp2p.Transport(webtransport.New), + libp2p.ListenAddrStrings( + fmt.Sprintf("/ip4/0.0.0.0/tcp/%d", port), + fmt.Sprintf("/ip6/::/tcp/%d", port), + fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1", port), + fmt.Sprintf("/ip6/::/udp/%d/quic-v1", port), + fmt.Sprintf("/ip4/0.0.0.0/tcp/%d/ws", port), + fmt.Sprintf("/ip6/::/tcp/%d/ws", port), + fmt.Sprintf("/ip4/0.0.0.0/udp/%d/quic-v1/webtransport", port), + fmt.Sprintf("/ip6/::/udp/%d/quic-v1/webtransport", port), + ), + } +}