Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

ns.gno

13.38 Kb · 419 lines
  1// Package ns is a Plan 9 namespace server for gno.land.
  2//
  3// Every account gets a private, persistent namespace: its own RAM root plus a
  4// mount table it alone controls. Realms publish file trees into /srv, accounts
  5// bind those trees wherever they like, and a read-only rc shell renders the
  6// whole thing in gnoweb.
  7//
  8// This is the part of Plan 9 that gno does not otherwise have. The chain has a
  9// single global tree of realm paths that looks the same to everybody; here a
 10// name means what YOU bound it to. Composing two realms that were never
 11// written to work together stops being a redeploy and becomes a transaction:
 12//
 13//	gnokey maketx call -pkgpath gno.land/r/nym-moultest123/x/plan9/ns/v0 -func Exec \
 14//	  -args 'bind -ac /srv/dev /dev; echo hello > /tmp/greeting'
 15//
 16// SECURITY. Mounted trees are READ-ONLY by construction: ninep.File has no
 17// mutating method, so grafting a foreign realm's tree into your namespace
 18// cannot be turned into a write against that realm. Writes only ever reach a
 19// memfs tree this realm created for you. A crossing write method would mint
 20// THIS realm's frame for the callee, which is the confused-deputy shape that
 21// r/gov/dao's Executor relies on deliberately and p/nt/grc20's Teller refuses
 22// deliberately; it is out of scope for v0. See moul/gno-contracts#136.
 23//
 24// NOTICE. Plan 9 from Bell Labs is the work of the Computing Science Research
 25// Center at Bell Labs; the name and the marks are theirs, and the copyright is
 26// held by the Plan 9 Foundation (https://p9f.org). This realm is not
 27// affiliated with, endorsed by, or sponsored by them, and contains no Plan 9
 28// code: it borrows the vocabulary so that the design reads without a glossary,
 29// and it is an homage, asking what that ecosystem's spirit looks like on a
 30// chain. Full attribution: NOTICE.md at the root of moul/gno-contracts.
 31package ns
 32
 33import (
 34	"chain/runtime"
 35	"errors"
 36	"strconv"
 37	"strings"
 38
 39	"gno.land/p/nt/avl/v0"
 40
 41	"gno.land/p/nym-moultest123/realmpath/v0"
 42
 43	memfs "gno.land/p/nym-moultest123/x/plan9/memfs/v0"
 44	ninep "gno.land/p/nym-moultest123/x/plan9/ninep/v0"
 45	nspkg "gno.land/p/nym-moultest123/x/plan9/ns/v0"
 46	rc "gno.land/p/nym-moultest123/x/plan9/rc/v0"
 47)
 48
 49// DemoKey names the namespace gnoweb browses when no ?u= is given. It is a
 50// plain string rather than an address so it can never collide with one.
 51const DemoKey = "demo"
 52
 53type service struct {
 54	file  ninep.File
 55	owner string  // pkgpath of the realm that posted it
 56	addr  address // that realm's address
 57	since int64   // block height of the posting
 58}
 59
 60type space struct {
 61	fs *memfs.FS
 62	ns *nspkg.Ns
 63}
 64
 65var (
 66	services = avl.NewTree() // name -> *service
 67	spaces   = avl.NewTree() // address string (or DemoKey) -> *space
 68)
 69
 70func init() {
 71	seedDemo()
 72}
 73
 74// ---------------------------------------------------------------- /srv
 75
 76// srvDir is the synthetic directory that makes posted services reachable by
 77// name. It is written out by hand rather than built with synfs because its
 78// contents change as realms post, and because implementing ninep.File
 79// directly is meant to look easy: four methods, no state of its own.
 80type srvDir struct{}
 81
 82func (d *srvDir) Stat() ninep.Stat {
 83	return ninep.Stat{
 84		Qid:   ninep.Qid{Type: ninep.QTDIR, Path: 1},
 85		Mode:  ninep.DMDIR | 0555,
 86		Mtime: runtime.ChainHeight(),
 87		Name:  "srv",
 88		Uid:   "sys",
 89		Gid:   "sys",
 90		Muid:  "sys",
 91	}
 92}
 93
 94func (d *srvDir) Walk(name string) (ninep.File, error) {
 95	if !ninep.ValidName(name) {
 96		return nil, ninep.ErrBadName
 97	}
 98	v := services.Get(name)
 99	if v == nil {
100		return nil, ninep.ErrNotExist
101	}
102	return v.(*service).file, nil
103}
104
105func (d *srvDir) Read(off, count int64) (string, error) { return "", ninep.ErrIsDir }
106
107func (d *srvDir) ReadDir() ([]ninep.Stat, error) {
108	out := []ninep.Stat{}
109	services.Iterate("", "", func(k string, v any) bool {
110		st := v.(*service).file.Stat()
111		st.Name = k // the posted name, not whatever the server calls its root
112		out = append(out, st)
113		return false
114	})
115	return out, nil
116}
117
118var srvRoot = &srvDir{}
119
120// ---------------------------------------------------------------- posting
121
122// Post publishes a file tree under name in /srv, where any account can bind
123// it. The posting realm is recorded and is the only one that may Unpost.
124//
125// Names are first come, first served, which is fine for an experiment and
126// would not be for anything else.
127func Post(cur realm, name string, f ninep.File) {
128	if !cur.IsCurrent() {
129		panic("post: cur is not the caller's live realm")
130	}
131	if !ninep.ValidName(name) {
132		panic("post: " + ninep.ErrBadName.Error())
133	}
134	if f == nil {
135		panic("post: nil file server")
136	}
137	prev := cur.Previous()
138	if existing := services.Get(name); existing != nil {
139		if existing.(*service).owner != prev.PkgPath() {
140			panic("post: " + name + " is already posted by " + existing.(*service).owner)
141		}
142	}
143	services.Set(name, &service{
144		file:  f,
145		owner: prev.PkgPath(),
146		addr:  prev.Address(),
147		since: runtime.ChainHeight(),
148	})
149}
150
151// Unpost withdraws a service. Only the realm that posted it may do so.
152func Unpost(cur realm, name string) {
153	if !cur.IsCurrent() {
154		panic("unpost: cur is not the caller's live realm")
155	}
156	v := services.Get(name)
157	if v == nil {
158		panic("unpost: " + ninep.ErrNotExist.Error())
159	}
160	if v.(*service).owner != cur.Previous().PkgPath() {
161		panic("unpost: " + name + " belongs to " + v.(*service).owner)
162	}
163	services.Remove(name)
164}
165
166// Services lists the posted service names, in order.
167func Services() []string {
168	out := []string{}
169	services.Iterate("", "", func(k string, _ any) bool {
170		out = append(out, k)
171		return false
172	})
173	return out
174}
175
176// ---------------------------------------------------------------- namespaces
177
178// newSpace builds the default namespace, which is this chain's /lib/namespace:
179// a private ram root, the mount points that Plan 9 requires to exist before
180// anything can be bound onto them, /srv mounted, and /dev bound from it when a
181// device server has been posted.
182func newSpace(owner string) *space {
183	now := runtime.ChainHeight()
184	fs := memfs.New(owner, now)
185	fs.MkdirAll("/srv", now)
186	fs.MkdirAll("/dev", now)
187	fs.MkdirAll("/tmp", now)
188
189	n := nspkg.New(fs.Root())
190	n.Mount(srvRoot, "#s", "/srv", nspkg.MREPL)
191	if services.Has("dev") {
192		n.Bind("/srv/dev", "/dev", nspkg.MREPL)
193	}
194	return &space{fs: fs, ns: n}
195}
196
197func spaceFor(key string) *space {
198	if v := spaces.Get(key); v != nil {
199		return v.(*space)
200	}
201	sp := newSpace(key)
202	spaces.Set(key, sp)
203	return sp
204}
205
206func peek(key string) *space {
207	if v := spaces.Get(key); v != nil {
208		return v.(*space)
209	}
210	return nil
211}
212
213// seedDemo builds the namespace gnoweb shows by default. It is deliberately
214// reproducible: an example test resets it before rendering.
215func seedDemo() {
216	spaces.Remove(DemoKey)
217	sp := spaceFor(DemoKey)
218	now := runtime.ChainHeight()
219	sp.fs.WriteFile("/tmp/greeting", "hello from a namespace\n", now)
220	sp.fs.MkdirAll("/usr/glenda/bin", now)
221	sp.fs.WriteFile("/usr/glenda/bin/rc", "#!/bin/rc\n", now)
222	sp.fs.MkdirAll("/bin", now)
223	sp.fs.WriteFile("/bin/ls", "system ls\n", now)
224	sp.ns.Bind("/usr/glenda/bin", "/bin", nspkg.MAFTER|nspkg.MCREATE)
225	sp.ns.Cd("/usr/glenda")
226}
227
228// ResetDemo rebuilds the demo namespace. Anyone may call it: it is a demo, and
229// the alternative is a demo that the first visitor ruins for everybody.
230func ResetDemo(cur realm) { seedDemo() }
231
232// ---------------------------------------------------------------- shell
233
234// Exec runs a command line against the CALLER's namespace and returns its
235// output. The namespace belongs to cur.Previous().Address(), so a user gets
236// theirs and a realm gets its own.
237//
238// An error panics, so a half-applied command line reverts with the
239// transaction rather than leaving a namespace nobody asked for.
240func Exec(cur realm, line string) string {
241	if !cur.IsCurrent() {
242		panic("exec: cur is not the caller's live realm")
243	}
244	key := cur.Previous().Address().String()
245	sp := spaceFor(key)
246	sh := rc.New(sp.ns, rc.ReadWrite, runtime.ChainHeight)
247	out, err := sh.Run(line)
248	if err != nil {
249		panic(err.Error())
250	}
251	return out
252}
253
254// Reset discards the caller's namespace, so the next use rebuilds the default.
255func Reset(cur realm) {
256	if !cur.IsCurrent() {
257		panic("reset: cur is not the caller's live realm")
258	}
259	spaces.Remove(cur.Previous().Address().String())
260}
261
262// Run executes a READ-ONLY command line against key's namespace. It is the
263// query side of Exec: no transaction, no writes, safe from Render.
264func Run(key, line string) (string, error) {
265	sp := peek(key)
266	if sp == nil {
267		return "", errors.New("no namespace for " + key)
268	}
269	sh := rc.New(sp.ns, rc.ReadOnly, runtime.ChainHeight)
270	return sh.Run(line)
271}
272
273// Namespace returns key's mount table, in ns(1) format.
274func Namespace(key string) string {
275	sp := peek(key)
276	if sp == nil {
277		return ""
278	}
279	return sp.ns.String()
280}
281
282// Keys lists the namespaces that exist, in order.
283func Keys() []string {
284	out := []string{}
285	spaces.Iterate("", "", func(k string, _ any) bool {
286		out = append(out, k)
287		return false
288	})
289	return out
290}
291
292// ---------------------------------------------------------------- render
293
294// Render browses a namespace.
295//
296//	Render("")                  overview, posted services, how to drive it
297//	Render("ns?u=<key>")        the mount table
298//	Render("ls/bin?u=<key>")    a directory listing (ls -l)
299//	Render("cat/tmp/greeting")  a file
300//	Render("stat/bin")          the 9P stat, with the union width
301//	Render("walk/bin/rc")       how each element of a path resolves
302//	Render("rc?c=<command>")    any read-only rc command line
303//
304// ?u= selects the namespace; it defaults to the demo one.
305func Render(path string) string {
306	req := realmpath.Parse(path)
307	key := req.Query.Get("u")
308	if key == "" {
309		key = DemoKey
310	}
311	parts := req.PathParts()
312	if len(parts) == 0 || parts[0] == "" {
313		return renderHome(key)
314	}
315
316	cmd := parts[0]
317	rest := "/" + strings.Join(parts[1:], "/")
318	var line string
319	switch cmd {
320	case "ns":
321		return renderCmd(key, "ns", "ns")
322	case "ls":
323		line = "ls -l " + quote(rest)
324	case "cat":
325		line = "cat " + quote(rest)
326	case "stat":
327		line = "stat " + quote(rest)
328	case "walk":
329		line = "walk " + quote(rest)
330	case "rc":
331		line = req.Query.Get("c")
332		if line == "" {
333			line = "help"
334		}
335	default:
336		return "# 404\n\nunknown command `" + cmd + "`. Try `ls`, `cat`, `stat`, `walk`, `ns` or `rc?c=...`.\n"
337	}
338	return renderCmd(key, line, cmd+" "+rest)
339}
340
341func renderCmd(key, line, title string) string {
342	var b strings.Builder
343	b.WriteString("# " + title + "\n\n")
344	b.WriteString("namespace: `" + key + "`\n\n")
345	out, err := Run(key, line)
346	if err != nil {
347		b.WriteString("```\n" + err.Error() + "\n```\n")
348	} else if out == "" {
349		b.WriteString("_(no output)_\n")
350	} else {
351		b.WriteString("```\n" + out + "```\n")
352	}
353	b.WriteString("\n[namespace](:ns?u=" + key + ") · [root](:ls?u=" + key +
354		") · [home](:)\n")
355	return b.String()
356}
357
358func renderHome(key string) string {
359	var b strings.Builder
360	b.WriteString("# plan9: namespaces for gno\n\n")
361	b.WriteString("A Plan 9 namespace server. Every account owns a private mount table over ")
362	b.WriteString("[9P-shaped](https://9p.io/sys/doc/9.html) file trees: realms post trees ")
363	b.WriteString("into `/srv`, you `bind` them where you want them, and a name means what ")
364	b.WriteString("*you* bound it to.\n\n")
365
366	b.WriteString("## /srv\n\n")
367	if services.Size() == 0 {
368		b.WriteString("_No service is posted yet._\n\n")
369	} else {
370		b.WriteString("| name | posted by | since |\n|---|---|---|\n")
371		services.Iterate("", "", func(k string, v any) bool {
372			s := v.(*service)
373			b.WriteString("| `" + k + "` | [`" + s.owner + "`](" +
374				strings.TrimPrefix(s.owner, "gno.land") + ") | " +
375				strconv.FormatInt(s.since, 10) + " |\n")
376			return false
377		})
378		b.WriteString("\n")
379	}
380
381	b.WriteString("## The demo namespace\n\n")
382	b.WriteString("```\n" + Namespace(DemoKey) + "```\n\n")
383	b.WriteString("`/bin` is a union: the system `/bin` first, then `/usr/glenda/bin`, ")
384	b.WriteString("with `-c` so new files land in the second. Listing it shows both members ")
385	b.WriteString("because a Plan 9 union is a concatenation, so shadowing stays visible.\n\n")
386	b.WriteString("- [ns](:ns) · [ls /](:ls) · [ls /bin](:ls/bin) · ")
387	b.WriteString("[cat /tmp/greeting](:cat/tmp/greeting) · [walk /bin/rc](:walk/bin/rc)\n")
388	b.WriteString("- any read-only command: [`rc?c=ls -l /srv`](:rc?c=ls%20-l%20/srv)\n\n")
389
390	b.WriteString("## Your own namespace\n\n")
391	b.WriteString("```\n")
392	b.WriteString("gnokey maketx call -pkgpath gno.land/r/nym-moultest123/x/plan9/ns/v0 \\\n")
393	b.WriteString("  -func Exec -args 'bind -ac /srv/dev /dev; echo hi > /tmp/f'\n")
394	b.WriteString("```\n\n")
395	b.WriteString("Then browse it with `?u=<your address>`. `Reset` throws it away.\n\n")
396
397	b.WriteString("## Namespaces\n\n")
398	keys := Keys()
399	for _, k := range keys {
400		b.WriteString("- [`" + k + "`](:ns?u=" + k + ")\n")
401	}
402	b.WriteString("\nDesign and analysis: ")
403	b.WriteString("[moul/gno-contracts#136](https://github.com/moul/gno-contracts/issues/136).\n")
404	b.WriteString("\n_Not affiliated with Plan 9. Plan 9 from Bell Labs is the ")
405	b.WriteString("work of the Computing Science Research Center at Bell Labs; the name ")
406	b.WriteString("and the marks are theirs, and the copyright is held by the ")
407	b.WriteString("[Plan 9 Foundation](https://p9f.org). This realm borrows the ")
408	b.WriteString("vocabulary and none of the code: it is an homage, asking what that ")
409	b.WriteString("ecosystem's spirit looks like on a chain._\n")
410	return b.String()
411}
412
413// quote wraps a path for rc if it needs it.
414func quote(s string) string {
415	if !strings.ContainsAny(s, " \t'") {
416		return s
417	}
418	return "'" + strings.ReplaceAll(s, "'", "''") + "'"
419}