Goを使ってPlan 9とお話

9PプロトコルPlan 9MacOS Xとおしゃべりするのに、Goの9pパッケージ(go9p)を使ってみようという話。go9pは標準パッケージには含まれていないが、有志によって開発が進められている。copyというbuiltinを使っているので、新しいリリースを使う必要がある。

$ hg clone http://bitbucket.org/f2f/go9p/
$ cd go9p
$ make
$ make install

ちなみに9Pの実装はいろいろあり、ここにまとめられている。例えば、C言語だとlibixpが有名で、MacOS Xでも動く。

Plan 9側の準備としてexportfsを使って自分のホームディレクトリ以下の名前空間をエクスポートする。9Pプロトコルのリスナーとしてlisten1を使う。

% aux/listen1 tcp!*!10000 exportfs -R -r $home

go9pのクライアントサンプル(ls.go)を実行してみる。-addrオプションにはPlan 9マシンのIPアドレスとlisten1で指定したポート番号を与える。VMWare上で実行しているPlan 9名前空間が得られた。つまり、Mac OS Xからはルートディレクトリ(/)が$home(/usr/oraccha)として見える。

$ cd plan9/p/clnt/examples
$ 8g ls.go
$ 8l ls.8
$ ./8.out -addr="192.168.182.130:10000" /

9Pのやりとりを見たいので、デバッグオプション(-d、-D)を追加してみた*1。Tから始まるのがメッセージの送信、Rから始まるのが返信になる。これ以前に*versionや*attachのやりとりがあるが、表示されてない。これはclnt.Mount以前にデバッグレベルを設定する手段がないから。また、問答無用で9P2000.u拡張を利用するようだ(まぁ、通信相手がPlan 9だったら9P2000にfall backするけど)。

$ ./8.out -d -addr="192.168.182.130:10000" /
2009/12/31 11:39:23 {{{ Twalk tag 0 fid 0 newfid 1
2009/12/31 11:39:23 }}} Rwalk tag 0
2009/12/31 11:39:23 {{{ Topen tag 0 fid 1 mode 0
2009/12/31 11:39:23 }}} Ropen tag 0 qid (40fc f 'd') iounit 8192
2009/12/31 11:39:23 {{{ Tread tag 0 fid 1 offset 0 count 8192
2009/12/31 11:39:23 }}} Rread tag 0 count 442
2009/12/31 11:39:23 {{{ Tread tag 0 fid 1 offset 442 count 8192
2009/12/31 11:39:23 }}} Rread tag 0 count 0
bin
contrib
lib
src
tmp
www
2009/12/31 11:39:23 {{{ Tread tag 0 fid 1 offset 442 count 8192
2009/12/31 11:39:23 }}} Rread tag 0 count 0
2009/12/31 11:39:23 {{{ Tclunk tag 0 fid 1
2009/12/31 11:39:23 }}} Rclunk tag 0

ソースコードはこんな感じ。

package main

import "flag"
import "fmt"
import "log"
import "os"
import "plan9/p"
import "plan9/p/clnt"

var addr = flag.String("addr", "127.0.0.1:5640", "network address")
var debug = flag.Bool("d", false, "enable debugging (fcalls)")
var debugall = flag.Bool("D", false, "enable debugging (raw packets)")

func main() {
	var user p.User;
	var err *p.Error;
	var c *clnt.Clnt;
	var file *clnt.File;
	var d []*p.Dir;

	flag.Parse();
	user = p.OsUsers.Uid2User(os.Geteuid());
	c, err = clnt.Mount("tcp", *addr, "", user);
	if err != nil {
		goto error
	}

	if flag.NArg() != 1 {
		log.Stderr("invalid arguments");
		return;
	}

	if *debug {
		c.Debuglevel = 1
	}
	if *debugall {
		c.Debuglevel = 2
	}

	file, err = c.FOpen(flag.Arg(0), p.OREAD);
	if err != nil {
		goto error
	}

	for {
		d, err = file.Readdir(0);
		if err != nil {
			goto error
		}

		if d == nil || len(d) == 0 {
			break
		}

		for i := 0; i < len(d); i++ {
			os.Stdout.WriteString(d[i].Name + "\n")
		}
	}

	file.Close();
	return;

error:
	log.Stderr(fmt.Sprintf("Error: %s %d", err.Error, err.Errornum));
}

とこれだけでは面白くないので、psを実装してみる。psを実装するには/procが見えればよい。NFSだと/procはexportできないけど、9Pではこんなこともできちゃう。

注意点はlisten1を実行するときに-tオプションを付けること。これがないとNoneユーザで実行されてしまい、exportfs以外の/proc/pid/statusのopenにpermission deniedで失敗してしまう。これに気づかずしばらくはまってしまった。

package main

import "flag"
import "fmt"
import "log"
import "os"
import "sort"
import "strconv"
import "strings"
import "plan9/p"
import "plan9/p/clnt"

var addr = flag.String("addr", "127.0.0.1:5640", "network address")
var debug = flag.Bool("d", false, "enable debugging (fcalls)")
var debugall = flag.Bool("D", false, "enable debugging (raw packets)")

// A dirList implements sort.Interface.
type dirList []*p.Dir

func (d dirList) Len() int           { return len(d) }
func (d dirList) Less(i, j int) bool {
	a, err := strconv.Atoi(d[i].Name);
	if err != nil { return false; }
	b, err := strconv.Atoi(d[j].Name);
	if err != nil { return false; }
	return a < b;
}
func (d dirList) Swap(i, j int)      { d[i], d[j] = d[j], d[i] }

func ps(c *clnt.Clnt, s string) {
	buf := make([]byte, 4096);

	f, err := c.FOpen("/proc/" + s + "/status", p.OREAD);
	if err != nil {
		goto error
	}
	nr, err := f.Read(buf);
	if err != nil {
		goto error
	}
	stats := strings.Fields(string(buf[0:nr]));

	/*
	 * 0  text
	 * 1  user
	 * 2  state
	 * 3  cputime[6]
	 * 9  memory
	 * 10 basepri
	 * 11 pri
	 */
	utime, er := strconv.Atoi(stats[3]);
	if er != nil {
		fmt.Fprintf(os.Stderr, "%v\n", er);
	}
	utime /= 1000;
	stime, er := strconv.Atoi(stats[4]);
	if er != nil {
		fmt.Fprintf(os.Stderr, "%v\n", er);
	}
	stime /= 1000;

	fmt.Printf("%-10s %8s %4d:%.2d %3d:%.2d %7sK %-8.8s %s\n",
		stats[1],
		s,
		utime/60, utime%60,
		stime/60, stime%60,
		stats[9],
		stats[2],
		stats[0]);

	return;

error:
	log.Stderr(fmt.Sprintf("Error: %s", err.Error));
}

func main() {
	var user p.User;
	var err *p.Error;
	var c *clnt.Clnt;
	var file *clnt.File;
	var d []*p.Dir;

	flag.Parse();
	if *debug {
		c.Debuglevel = 1
	}
	if *debugall {
		c.Debuglevel = 2
	}

	user = p.OsUsers.Uid2User(os.Geteuid());
	c, err = clnt.Mount("tcp", *addr, "", user);
	if err != nil {
		goto error
	}

	file, err = c.FOpen("/proc", p.OREAD);
	if err != nil {
		goto error
	}
	d, err = file.Readdir(0);
	file.Close();
	if err != nil {
		goto error
	}
	if d == nil || len(d) == 0 {
		fmt.Fprintf(os.Stderr, "ps: empty directory /proc\n");
		return;
	}

	nnp := 0;
	dirs := make(dirList, len(d));
	for i := range d {
		if d[i].Name[0] < '0' || '9' < d[i].Name[0] {
			nnp += 1;
			continue;
		}
		dirs[i-nnp] = d[i];
	}
	dirs = dirs[0:len(dirs)-nnp];
	sort.Sort(dirs);

	for i := 0; i < len(dirs); i++ {
		ps(c, dirs[i].Name);
	}

	return;

error:
	log.Stderr(fmt.Sprintf("Error: %s", err.Error));
}

*1:よく考えたらsnoopyで調べる手もあるな。ちゃんと見やすい形式に整形してくれる。