Transaction
A32E3D1E25917E…26662C07F9FF
Block 272,413 · index 1 · indexed
Summary
- Hash
- A32E3D1E25917E04940941449AAC6256CF10DE701178F468B9B026662C07F9FF
- Block
- 272,413
- Size
- 27716 bytes
- Gas used
- 35,717,782 / 83,156,800
- Fee
- 249470ugnot
- Status
- success
Messages
- Attached funds
- 11000000ugnot
Arguments · 11
- #1ns
- #2README.md
- #3# `gno.land/r/moul/x/plan9/ns/v0` **A Plan 9 namespace server for gno.land.** Every account gets a private, persistent namespace: its own RAM root plus a mount table it alone controls. Realms publish file trees into `/srv`, accounts `bind` those trees wherever they like, and a read-only `rc` shell renders the whole thing in gnoweb. ```sh gnokey maketx call -pkgpath gno.land/r/moul/x/plan9/ns/v0 \ -func Exec -args 'bind -ac /srv/dev /dev; echo hello > /tmp/greeting' ``` Then browse it at `/r/moul/x/plan9/ns/v0:ns?u=<your address>`. This is the part of Plan 9 that gno does not otherwise have. The chain has a single global tree of realm paths that looks the same to everybody; here a name means what *you* bound it to. Composing two realms that were never written to work together stops being a redeploy and becomes a transaction. ## Surface | call | what it does | |---|---| | `Post(cur, name, f)` | publish a `ninep.File` tree under `/srv/<name>` | | `Unpost(cur, name)` | withdraw it; only the posting realm may | | `Exec(cur, line)` | run an `rc` command line against the caller's namespace | | `Reset(cur)` | throw the caller's namespace away | | `Run(key, line)` | the read-only query side, used by `Render` | | `Namespace(key)` | the mount table, in `ns(1)` format | `Render` routes: `ns`, `ls/<path>`, `cat/<path>`, `stat/<path>`, `walk/<path>`, and `rc?c=<command>` for any read-only command line. `?u=` picks whose namespace; it defaults to a seeded demo one, so gnoweb shows something live with no transaction. ## The default namespace This chain's `/lib/namespace`: a private ram root, the mount points Plan 9 requires to exist before anything can be bound onto them, `/srv` mounted, and `/dev` bound from it when a device server has been posted. ``` mount #s /srv bind /srv/dev /dev ``` ## Security **Mounted trees are read-only by construction.** `ninep.File` has no mutating method, so grafting a foreign realm's tree into your namespace cannot be turned into a write against that realm; writes only ever reach a memfs tree this realm created for you. A crossing write method would mint *this* realm's frame for the callee, which is the confused-deputy shape `r/gov/dao`'s `Executor` relies on deliberately and `p/nt/grc20`'s `Teller` refuses deliberately. It is out of scope for v0, and three abort tests pin the boundary: a second realm cannot take over a `/srv` name, a write to a mounted tree fails with `read-only file server`, and a command line that fails part way aborts the whole call. `/srv` names are first come, first served, with the posting realm recorded and the only one allowed to unpost. Squatting is possible and accepted for an experiment. Built on [`ninep`](../../../../../p/moul/x/plan9/ninep/v0), [`memfs`](../../../../../p/moul/x/plan9/memfs/v0), [`ns`](../../../../../p/moul/x/plan9/ns/v0) and [`rc`](../../../../../p/moul/x/plan9/rc/v0). Design and analysis: [moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136). --- **Not affiliated with Plan 9.** Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the [Plan 9 Foundation](https://p9f.org). This realm borrows the vocabulary and none of the code: it is an independent homage, asking what that ecosystem's spirit looks like on a chain. Full attribution: [NOTICE](../../../../../NOTICE.md). <!-- BEGIN GNOCONTRACTS FOOTER (generated by `make readmes`; do not edit below) --> --- Part of **[moul/gno-contracts](https://github.com/moul/gno-contracts)** — moul's versioned gno.land contracts. See the repository for the full catalog, build/test tooling, and usage. **Dependency graph:**  > 🧪 **Highly experimental — potentially vibe-coded.** Not audited; may break, change, or be removed at any time. Do not use with anything of value. Full disclaimer: [DISCLAIMER](https://github.com/moul/gno-contracts/blob/main/DISCLAIMER.md). <!-- END GNOCONTRACTS FOOTER -->
- #4gnomod.toml
- #5module = "gno.land/r/moul/x/plan9/ns/v0" gno = "0.9" # public: imported by r/moul/x/plan9/dev, which is what the pair is for
- #6ns.gno
- #7// Package ns is a Plan 9 namespace server for gno.land. // // Every account gets a private, persistent namespace: its own RAM root plus a // mount table it alone controls. Realms publish file trees into /srv, accounts // bind those trees wherever they like, and a read-only rc shell renders the // whole thing in gnoweb. // // This is the part of Plan 9 that gno does not otherwise have. The chain has a // single global tree of realm paths that looks the same to everybody; here a // name means what YOU bound it to. Composing two realms that were never // written to work together stops being a redeploy and becomes a transaction: // // gnokey maketx call -pkgpath gno.land/r/moul/x/plan9/ns/v0 -func Exec \ // -args 'bind -ac /srv/dev /dev; echo hello > /tmp/greeting' // // SECURITY. Mounted trees are READ-ONLY by construction: ninep.File has no // mutating method, so grafting a foreign realm's tree into your namespace // cannot be turned into a write against that realm. Writes only ever reach a // memfs tree this realm created for you. A crossing write method would mint // THIS realm's frame for the callee, which is the confused-deputy shape that // r/gov/dao's Executor relies on deliberately and p/nt/grc20's Teller refuses // deliberately; it is out of scope for v0. See moul/gno-contracts#136. // // NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research // Center at Bell Labs; the name and the marks are theirs, and the copyright is // held by the Plan 9 Foundation (https://p9f.org). This realm is not // affiliated with, endorsed by, or sponsored by them, and contains no Plan 9 // code: it borrows the vocabulary so that the design reads without a glossary, // and it is an homage, asking what that ecosystem's spirit looks like on a // chain. Full attribution: NOTICE.md at the root of moul/gno-contracts. package ns import ( "chain/runtime" "errors" "strconv" "strings" "gno.land/p/nt/avl/v0" "gno.land/p/moul/realmpath/v0" memfs "gno.land/p/moul/x/plan9/memfs/v0" ninep "gno.land/p/moul/x/plan9/ninep/v0" nspkg "gno.land/p/moul/x/plan9/ns/v0" rc "gno.land/p/moul/x/plan9/rc/v0" ) // DemoKey names the namespace gnoweb browses when no ?u= is given. It is a // plain string rather than an address so it can never collide with one. const DemoKey = "demo" type service struct { file ninep.File owner string // pkgpath of the realm that posted it addr address // that realm's address since int64 // block height of the posting } type space struct { fs *memfs.FS ns *nspkg.Ns } var ( services = avl.NewTree() // name -> *service spaces = avl.NewTree() // address string (or DemoKey) -> *space ) func init() { seedDemo() } // ---------------------------------------------------------------- /srv // srvDir is the synthetic directory that makes posted services reachable by // name. It is written out by hand rather than built with synfs because its // contents change as realms post, and because implementing ninep.File // directly is meant to look easy: four methods, no state of its own. type srvDir struct{} func (d *srvDir) Stat() ninep.Stat { return ninep.Stat{ Qid: ninep.Qid{Type: ninep.QTDIR, Path: 1}, Mode: ninep.DMDIR | 0555, Mtime: runtime.ChainHeight(), Name: "srv", Uid: "sys", Gid: "sys", Muid: "sys", } } func (d *srvDir) Walk(name string) (ninep.File, error) { if !ninep.ValidName(name) { return nil, ninep.ErrBadName } v := services.Get(name) if v == nil { return nil, ninep.ErrNotExist } return v.(*service).file, nil } func (d *srvDir) Read(off, count int64) (string, error) { return "", ninep.ErrIsDir } func (d *srvDir) ReadDir() ([]ninep.Stat, error) { out := []ninep.Stat{} services.Iterate("", "", func(k string, v any) bool { st := v.(*service).file.Stat() st.Name = k // the posted name, not whatever the server calls its root out = append(out, st) return false }) return out, nil } var srvRoot = &srvDir{} // ---------------------------------------------------------------- posting // Post publishes a file tree under name in /srv, where any account can bind // it. The posting realm is recorded and is the only one that may Unpost. // // Names are first come, first served, which is fine for an experiment and // would not be for anything else. func Post(cur realm, name string, f ninep.File) { if !cur.IsCurrent() { panic("post: cur is not the caller's live realm") } if !ninep.ValidName(name) { panic("post: " + ninep.ErrBadName.Error()) } if f == nil { panic("post: nil file server") } prev := cur.Previous() if existing := services.Get(name); existing != nil { if existing.(*service).owner != prev.PkgPath() { panic("post: " + name + " is already posted by " + existing.(*service).owner) } } services.Set(name, &service{ file: f, owner: prev.PkgPath(), addr: prev.Address(), since: runtime.ChainHeight(), }) } // Unpost withdraws a service. Only the realm that posted it may do so. func Unpost(cur realm, name string) { if !cur.IsCurrent() { panic("unpost: cur is not the caller's live realm") } v := services.Get(name) if v == nil { panic("unpost: " + ninep.ErrNotExist.Error()) } if v.(*service).owner != cur.Previous().PkgPath() { panic("unpost: " + name + " belongs to " + v.(*service).owner) } services.Remove(name) } // Services lists the posted service names, in order. func Services() []string { out := []string{} services.Iterate("", "", func(k string, _ any) bool { out = append(out, k) return false }) return out } // ---------------------------------------------------------------- namespaces // newSpace builds the default namespace, which is this chain's /lib/namespace: // a private ram root, the mount points that Plan 9 requires to exist before // anything can be bound onto them, /srv mounted, and /dev bound from it when a // device server has been posted. func newSpace(owner string) *space { now := runtime.ChainHeight() fs := memfs.New(owner, now) fs.MkdirAll("/srv", now) fs.MkdirAll("/dev", now) fs.MkdirAll("/tmp", now) n := nspkg.New(fs.Root()) n.Mount(srvRoot, "#s", "/srv", nspkg.MREPL) if services.Has("dev") { n.Bind("/srv/dev", "/dev", nspkg.MREPL) } return &space{fs: fs, ns: n} } func spaceFor(key string) *space { if v := spaces.Get(key); v != nil { return v.(*space) } sp := newSpace(key) spaces.Set(key, sp) return sp } func peek(key string) *space { if v := spaces.Get(key); v != nil { return v.(*space) } return nil } // seedDemo builds the namespace gnoweb shows by default. It is deliberately // reproducible: an example test resets it before rendering. func seedDemo() { spaces.Remove(DemoKey) sp := spaceFor(DemoKey) now := runtime.ChainHeight() sp.fs.WriteFile("/tmp/greeting", "hello from a namespace\n", now) sp.fs.MkdirAll("/usr/glenda/bin", now) sp.fs.WriteFile("/usr/glenda/bin/rc", "#!/bin/rc\n", now) sp.fs.MkdirAll("/bin", now) sp.fs.WriteFile("/bin/ls", "system ls\n", now) sp.ns.Bind("/usr/glenda/bin", "/bin", nspkg.MAFTER|nspkg.MCREATE) sp.ns.Cd("/usr/glenda") } // ResetDemo rebuilds the demo namespace. Anyone may call it: it is a demo, and // the alternative is a demo that the first visitor ruins for everybody. func ResetDemo(cur realm) { seedDemo() } // ---------------------------------------------------------------- shell // Exec runs a command line against the CALLER's namespace and returns its // output. The namespace belongs to cur.Previous().Address(), so a user gets // theirs and a realm gets its own. // // An error panics, so a half-applied command line reverts with the // transaction rather than leaving a namespace nobody asked for. func Exec(cur realm, line string) string { if !cur.IsCurrent() { panic("exec: cur is not the caller's live realm") } key := cur.Previous().Address().String() sp := spaceFor(key) sh := rc.New(sp.ns, rc.ReadWrite, runtime.ChainHeight) out, err := sh.Run(line) if err != nil { panic(err.Error()) } return out } // Reset discards the caller's namespace, so the next use rebuilds the default. func Reset(cur realm) { if !cur.IsCurrent() { panic("reset: cur is not the caller's live realm") } spaces.Remove(cur.Previous().Address().String()) } // Run executes a READ-ONLY command line against key's namespace. It is the // query side of Exec: no transaction, no writes, safe from Render. func Run(key, line string) (string, error) { sp := peek(key) if sp == nil { return "", errors.New("no namespace for " + key) } sh := rc.New(sp.ns, rc.ReadOnly, runtime.ChainHeight) return sh.Run(line) } // Namespace returns key's mount table, in ns(1) format. func Namespace(key string) string { sp := peek(key) if sp == nil { return "" } return sp.ns.String() } // Keys lists the namespaces that exist, in order. func Keys() []string { out := []string{} spaces.Iterate("", "", func(k string, _ any) bool { out = append(out, k) return false }) return out } // ---------------------------------------------------------------- render // Render browses a namespace. // // Render("") overview, posted services, how to drive it // Render("ns?u=<key>") the mount table // Render("ls/bin?u=<key>") a directory listing (ls -l) // Render("cat/tmp/greeting") a file // Render("stat/bin") the 9P stat, with the union width // Render("walk/bin/rc") how each element of a path resolves // Render("rc?c=<command>") any read-only rc command line // // ?u= selects the namespace; it defaults to the demo one. func Render(path string) string { req := realmpath.Parse(path) key := req.Query.Get("u") if key == "" { key = DemoKey } parts := req.PathParts() if len(parts) == 0 || parts[0] == "" { return renderHome(key) } cmd := parts[0] rest := "/" + strings.Join(parts[1:], "/") var line string switch cmd { case "ns": return renderCmd(key, "ns", "ns") case "ls": line = "ls -l " + quote(rest) case "cat": line = "cat " + quote(rest) case "stat": line = "stat " + quote(rest) case "walk": line = "walk " + quote(rest) case "rc": line = req.Query.Get("c") if line == "" { line = "help" } default: return "# 404\n\nunknown command `" + cmd + "`. Try `ls`, `cat`, `stat`, `walk`, `ns` or `rc?c=...`.\n" } return renderCmd(key, line, cmd+" "+rest) } func renderCmd(key, line, title string) string { var b strings.Builder b.WriteString("# " + title + "\n\n") b.WriteString("namespace: `" + key + "`\n\n") out, err := Run(key, line) if err != nil { b.WriteString("```\n" + err.Error() + "\n```\n") } else if out == "" { b.WriteString("_(no output)_\n") } else { b.WriteString("```\n" + out + "```\n") } b.WriteString("\n[namespace](:ns?u=" + key + ") · [root](:ls?u=" + key + ") · [home](:)\n") return b.String() } func renderHome(key string) string { var b strings.Builder b.WriteString("# plan9: namespaces for gno\n\n") b.WriteString("A Plan 9 namespace server. Every account owns a private mount table over ") b.WriteString("[9P-shaped](https://9p.io/sys/doc/9.html) file trees: realms post trees ") b.WriteString("into `/srv`, you `bind` them where you want them, and a name means what ") b.WriteString("*you* bound it to.\n\n") b.WriteString("## /srv\n\n") if services.Size() == 0 { b.WriteString("_No service is posted yet._\n\n") } else { b.WriteString("| name | posted by | since |\n|---|---|---|\n") services.Iterate("", "", func(k string, v any) bool { s := v.(*service) b.WriteString("| `" + k + "` | [`" + s.owner + "`](" + strings.TrimPrefix(s.owner, "gno.land") + ") | " + strconv.FormatInt(s.since, 10) + " |\n") return false }) b.WriteString("\n") } b.WriteString("## The demo namespace\n\n") b.WriteString("```\n" + Namespace(DemoKey) + "```\n\n") b.WriteString("`/bin` is a union: the system `/bin` first, then `/usr/glenda/bin`, ") b.WriteString("with `-c` so new files land in the second. Listing it shows both members ") b.WriteString("because a Plan 9 union is a concatenation, so shadowing stays visible.\n\n") b.WriteString("- [ns](:ns) · [ls /](:ls) · [ls /bin](:ls/bin) · ") b.WriteString("[cat /tmp/greeting](:cat/tmp/greeting) · [walk /bin/rc](:walk/bin/rc)\n") b.WriteString("- any read-only command: [`rc?c=ls -l /srv`](:rc?c=ls%20-l%20/srv)\n\n") b.WriteString("## Your own namespace\n\n") b.WriteString("```\n") b.WriteString("gnokey maketx call -pkgpath gno.land/r/moul/x/plan9/ns/v0 \\\n") b.WriteString(" -func Exec -args 'bind -ac /srv/dev /dev; echo hi > /tmp/f'\n") b.WriteString("```\n\n") b.WriteString("Then browse it with `?u=<your address>`. `Reset` throws it away.\n\n") b.WriteString("## Namespaces\n\n") keys := Keys() for _, k := range keys { b.WriteString("- [`" + k + "`](:ns?u=" + k + ")\n") } b.WriteString("\nDesign and analysis: ") b.WriteString("[moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136).\n") b.WriteString("\n_Not affiliated with Plan 9. Plan 9 from Bell Labs is the ") b.WriteString("work of the Computing Science Research Center at Bell Labs; the name ") b.WriteString("and the marks are theirs, and the copyright is held by the ") b.WriteString("[Plan 9 Foundation](https://p9f.org). This realm borrows the ") b.WriteString("vocabulary and none of the code: it is an homage, asking what that ") b.WriteString("ecosystem's spirit looks like on a chain._\n") return b.String() } // quote wraps a path for rc if it needs it. func quote(s string) string { if !strings.ContainsAny(s, " \t'") { return s } return "'" + strings.ReplaceAll(s, "'", "''") + "'" }
- #8ns_test.gno
- #9package ns import ( "strings" "testing" "gno.land/p/nt/testutils/v0" "gno.land/p/nt/uassert/v0" ninep "gno.land/p/moul/x/plan9/ninep/v0" synfs "gno.land/p/moul/x/plan9/synfs/v0" ) // probe is a tiny read-only server, standing in for another realm's tree. func probe(name, contents string) ninep.File { t := synfs.New(name, "sys", func() int64 { return 1 }) t.Root().Add("value", func() string { return contents }) return t.Root() } func TestDemoNamespaceIsSeeded(t *testing.T) { seedDemo() out, err := Run(DemoKey, "ls -u /bin") if err != nil { t.Fatalf("ls: %v", err) } if out != "ls\nrc\n" { t.Errorf("the demo /bin should be a union of two directories: %q", out) } if got := Namespace(DemoKey); !strings.Contains(got, "bind -ac /usr/glenda/bin /bin") { t.Errorf("mount table: %q", got) } if got, _ := Run(DemoKey, "cat /tmp/greeting"); got != "hello from a namespace\n" { t.Errorf("greeting: %q", got) } } func TestRunIsReadOnly(t *testing.T) { seedDemo() if _, err := Run(DemoKey, "echo x > /tmp/f"); err == nil { t.Fatal("Render's shell must refuse a write") } if _, err := Run(DemoKey, "rm /tmp/greeting"); err == nil { t.Fatal("Render's shell must refuse a remove") } if _, err := Run("nobody", "ls /"); err == nil { t.Fatal("an unknown namespace should not be created by a read") } } func TestPostAndBind(cur realm, t *testing.T) { services.Remove("probe") Post(cross(cur), "probe", probe("probe", "42")) found := false for _, s := range Services() { if s == "probe" { found = true } } if !found { t.Fatalf("probe is not in /srv: %v", Services()) } // A fresh namespace sees it through /srv without importing anything. spaces.Remove("tester") spaceFor("tester") defer spaces.Remove("tester") out, err := Run("tester", "cat /srv/probe/value") if err != nil { t.Fatalf("cat through /srv: %v", err) } if out != "42" { t.Errorf("got %q, want 42", out) } services.Remove("probe") } func TestExecWritesToTheCallersOwnNamespace(cur realm, t *testing.T) { alice := testutils.TestAddress("alice") bob := testutils.TestAddress("bob") spaces.Remove(alice.String()) spaces.Remove(bob.String()) testing.SetRealm(testing.NewUserRealm(alice)) Exec(cross(cur), "echo 'alice was here' > /tmp/note") testing.SetRealm(testing.NewUserRealm(bob)) Exec(cross(cur), "echo 'bob was here' > /tmp/note") got, err := Run(alice.String(), "cat /tmp/note") if err != nil { t.Fatalf("alice: %v", err) } if got != "alice was here\n" { t.Errorf("alice's namespace: %q", got) } if got, _ = Run(bob.String(), "cat /tmp/note"); got != "bob was here\n" { t.Errorf("bob's namespace: %q", got) } // Reset throws a namespace away; the next use rebuilds the default. testing.SetRealm(testing.NewUserRealm(alice)) Reset(cross(cur)) if _, err := Run(alice.String(), "cat /tmp/note"); err == nil { t.Error("the namespace should be gone after Reset") } spaces.Remove(alice.String()) spaces.Remove(bob.String()) } func TestExecBindsIntoTheCallersNamespace(cur realm, t *testing.T) { services.Remove("probe") Post(cross(cur), "probe", probe("probe", "42")) defer services.Remove("probe") erin := testutils.TestAddress("erin") spaces.Remove(erin.String()) testing.SetRealm(testing.NewUserRealm(erin)) Exec(cross(cur), "bind /srv/probe /dev") if got := mustRun(t, erin.String(), "cat /dev/value"); got != "42" { t.Errorf("read through the mount: %q", got) } if ns := Namespace(erin.String()); !strings.Contains(ns, "bind /srv/probe /dev") { t.Errorf("mount table: %q", ns) } spaces.Remove(erin.String()) } func TestSrvListingUsesThePostedName(cur realm, t *testing.T) { services.Remove("aliased") // The server calls its own root "probe"; /srv must show it as "aliased". Post(cross(cur), "aliased", probe("probe", "x")) defer services.Remove("aliased") spaces.Remove("lister") spaceFor("lister") out := mustRun(t, "lister", "ls /srv") if !strings.Contains(out, "aliased") { t.Errorf("ls /srv: %q", out) } if strings.Contains(out, "probe") { t.Errorf("/srv leaked the server's own root name: %q", out) } spaces.Remove("lister") } func mustRun(t *testing.T, key, line string) string { t.Helper() out, err := Run(key, line) if err != nil { t.Fatalf("%s: %v", line, err) } return out } // --- the boundary. Three properties the suite would be unsafe without. ------- // A /srv name belongs to the realm that posted it: first come, first served, // and then locked. func TestPostRejectsASecondOwner(cur realm, t *testing.T) { services.Remove("taken") Post(cross(cur), "taken", probe("taken", "first")) defer services.Remove("taken") testing.SetRealm(testing.NewCodeRealm("gno.land/r/other/thing/v0")) uassert.AbortsWithMessage(t, cur, "post: taken is already posted by gno.land/r/moul/x/plan9/ns/v0", func() { Post(cross(cur), "taken", probe("taken", "second")) }) } // Grafting another realm's tree into your namespace gives you reads and // nothing else. ninep.File has no mutating method, so there is no route from a // mount to a write against the realm that posted it. func TestMountedTreesAreReadOnly(cur realm, t *testing.T) { services.Remove("probe") Post(cross(cur), "probe", probe("probe", "read me")) defer services.Remove("probe") frank := testutils.TestAddress("frank") spaces.Remove(frank.String()) defer spaces.Remove(frank.String()) testing.SetRealm(testing.NewUserRealm(frank)) Exec(cross(cur), "bind /srv/probe /dev") uassert.Equal(t, "read me", mustRun(t, frank.String(), "cat /dev/value")) uassert.AbortsWithMessage(t, cur, "echo: read-only file server", func() { Exec(cross(cur), "echo nope > /dev/value") }) } // A command line stops at the first error and the error leaves the realm as an // abort, so a half-applied Exec reverts with its transaction instead of // leaving a namespace nobody asked for. func TestExecAbortsAtTheFirstError(cur realm, t *testing.T) { grace := testutils.TestAddress("grace") spaces.Remove(grace.String()) defer spaces.Remove(grace.String()) testing.SetRealm(testing.NewUserRealm(grace)) uassert.AbortsWithMessage(t, cur, "cat: file does not exist", func() { Exec(cross(cur), "echo ok > /tmp/a; cat /absent; echo never > /tmp/b") }) } func TestRenderUnknownCommand(t *testing.T) { if got := Render("nope/x"); !strings.Contains(got, "404") { t.Errorf("got %q", got) } }
- #10render_example_test.gno
- #11package ns import "gno.land/p/nt/avl/v0" // resetForExample rebuilds the whole realm state. Realm globals persist for // the entire test binary and examples run after every Test, so without this an // example would pin whatever the last test happened to leave behind. func resetForExample() { services = avl.NewTree() spaces = avl.NewTree() seedDemo() } // ExampleRender pins the realm's front page. func ExampleRender() { resetForExample() print(Render("")) // Output: // # plan9: namespaces for gno // // A Plan 9 namespace server. Every account owns a private mount table over [9P-shaped](https://9p.io/sys/doc/9.html) file trees: realms post trees into `/srv`, you `bind` them where you want them, and a name means what *you* bound it to. // // ## /srv // // _No service is posted yet._ // // ## The demo namespace // // ``` // mount #s /srv // bind -ac /usr/glenda/bin /bin // cd /usr/glenda // ``` // // `/bin` is a union: the system `/bin` first, then `/usr/glenda/bin`, with `-c` so new files land in the second. Listing it shows both members because a Plan 9 union is a concatenation, so shadowing stays visible. // // - [ns](:ns) · [ls /](:ls) · [ls /bin](:ls/bin) · [cat /tmp/greeting](:cat/tmp/greeting) · [walk /bin/rc](:walk/bin/rc) // - any read-only command: [`rc?c=ls -l /srv`](:rc?c=ls%20-l%20/srv) // // ## Your own namespace // // ``` // gnokey maketx call -pkgpath gno.land/r/moul/x/plan9/ns/v0 \ // -func Exec -args 'bind -ac /srv/dev /dev; echo hi > /tmp/f' // ``` // // Then browse it with `?u=<your address>`. `Reset` throws it away. // // ## Namespaces // // - [`demo`](:ns?u=demo) // // Design and analysis: [moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136). // // _Not affiliated with Plan 9. Plan 9 from Bell Labs is the work of the Computing Science Research Center at Bell Labs; the name and the marks are theirs, and the copyright is held by the [Plan 9 Foundation](https://p9f.org). This realm borrows the vocabulary and none of the code: it is an homage, asking what that ecosystem's spirit looks like on a chain._ } // ExampleRender_ls pins a union directory listing. func ExampleRender_ls() { resetForExample() print(Render("ls/bin")) // Output: // # ls /bin // // namespace: `demo` // // ``` // -rw-r--r-- demo demo 10 ls // -rw-r--r-- demo demo 10 rc // ``` // // [namespace](:ns?u=demo) · [root](:ls?u=demo) · [home](:) } // ExampleRender_walk pins a resolution trace, which is where a bind becomes // visible: the union column widens exactly at the bound name. func ExampleRender_walk() { resetForExample() print(Render("walk/bin/rc")) // Output: // # walk /bin/rc // // namespace: `demo` // // ``` // / (1 5 d) drwxr-xr-x union=1 // /bin (a 1 d) drwxr-xr-x union=2 // /bin/rc (9 1 f) -rw-r--r-- union=1 // ``` // // [namespace](:ns?u=demo) · [root](:ls?u=demo) · [home](:) }
Result log
msg:0,success:true,log:,events:[]