Commit Diff


commit - 97423e8b7909c86abde24bd416798b1172759758
commit + fb37ce2aa1f717002944b5aec393b3318e2e7261
blob - 49f36bf99fe75ae3bdfb40e35d93b4c68c440fac
blob + 30c2205d3c5f55a717ead6a71e823d1445b868fa
--- client.go
+++ client.go
@@ -10,6 +10,8 @@ import (
 )
 
 type client struct {
+	version   string
+	msize     uint32
 	ctx       context.Context
 	transport roundTripper
 }
@@ -18,14 +20,41 @@ type client struct {
 // a context for out of bad messages, such as flushes, that may be sent by the
 // session. The session can effectively shutdown with this context.
 func NewSession(ctx context.Context, conn net.Conn) (Session, error) {
+	const msize = 64 << 10
+	const vers = "9P2000"
+
+	ch := newChannel(conn, codec9p{}, msize) // sets msize, effectively.
+
+	// negotiate the protocol version
+	smsize, svers, err := version(ctx, ch, msize, vers)
+	if err != nil {
+		return nil, err
+	}
+
+	if svers != vers {
+		// TODO(stevvooe): A stubborn client indeed!
+		return nil, fmt.Errorf("unsupported server version: %v", vers)
+	}
+
+	if smsize > msize {
+		// upgrade msize if server differs.
+		ch.setmsize(int(smsize))
+	}
+
 	return &client{
+		version:   vers,
+		msize:     msize,
 		ctx:       ctx,
-		transport: newTransport(ctx, conn),
+		transport: newTransport(ctx, ch),
 	}, nil
 }
 
 var _ Session = &client{}
 
+func (c *client) Version() (uint32, string) {
+	return c.msize, c.version
+}
+
 func (c *client) Auth(ctx context.Context, afid Fid, uname, aname string) (Qid, error) {
 	panic("not implemented")
 }
@@ -169,10 +198,10 @@ func (c *client) Open(ctx context.Context, fid Fid, mo
 		return Qid{}, 0, fmt.Errorf("invalid rpc response for open message: %v", resp)
 	}
 
-	return respmsg.Qid, respmsg.Msize, nil
+	return respmsg.Qid, respmsg.IOUnit, nil
 }
 
-func (c *client) Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, error) {
+func (c *client) Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, uint32, error) {
 	panic("not implemented")
 }
 
@@ -184,16 +213,27 @@ func (c *client) WStat(context.Context, Fid, Dir) erro
 	panic("not implemented")
 }
 
-func (c *client) Version(ctx context.Context, msize uint32, version string) (uint32, string, error) {
-	msg := MessageTversion{
+func (c *client) flush(ctx context.Context, tag Tag) error {
+	// TODO(stevvooe): We need to fire and forget flush messages when a call
+	// context gets cancelled.
+
+	panic("not implemented")
+}
+
+// version negiotiates the protocol version using channel, blocking until a
+// response is received. This should be called before starting the transport.
+func version(ctx context.Context, ch *channel, msize uint32, version string) (uint32, string, error) {
+	req := newFcall(MessageTversion{
 		MSize:   uint32(msize),
 		Version: version,
+	})
+
+	if err := ch.writeFcall(ctx, req); err != nil {
+		return 0, "", err
 	}
 
-	fcall := newFcall(msg)
-
-	resp, err := c.transport.send(ctx, fcall)
-	if err != nil {
+	resp := new(Fcall)
+	if err := ch.readFcall(ctx, resp); err != nil {
 		return 0, "", err
 	}
 
@@ -202,15 +242,5 @@ func (c *client) Version(ctx context.Context, msize ui
 		return 0, "", fmt.Errorf("invalid rpc response for version message: %v", resp)
 	}
 
-	// TODO(stevvooe): Use this response to set iounit and version on this
-	// client instance.
-
 	return mv.MSize, mv.Version, nil
 }
-
-func (c *client) flush(ctx context.Context, tag Tag) error {
-	// TODO(stevvooe): We need to fire and forget flush messages when a call
-	// context gets cancelled.
-
-	panic("not implemented")
-}
blob - /dev/null
blob + 9de03f0d26e4cad75685fe0929ef3cc725232ec9 (mode 644)
--- /dev/null
+++ channel.go
@@ -0,0 +1,173 @@
+package p9pnew
+
+import (
+	"bufio"
+	"encoding/binary"
+	"fmt"
+	"io"
+	"io/ioutil"
+	"log"
+	"net"
+
+	"golang.org/x/net/context"
+)
+
+// channel provides bidirectional protocol framing for 9p over net.Conn.
+// Operations are not thread-safe but reads and writes may be carried out
+// concurrently, supporting separate read and write loops.
+type channel struct {
+	conn   net.Conn
+	codec  Codec
+	brd    *bufio.Reader
+	bwr    *bufio.Writer
+	closed chan struct{}
+	rdbuf  []byte
+	wrbuf  []byte
+}
+
+func newChannel(conn net.Conn, codec Codec, msize int) *channel {
+	return &channel{
+		conn:   conn,
+		codec:  codec,
+		brd:    bufio.NewReaderSize(conn, msize), // msize may not be optimal buffer size
+		bwr:    bufio.NewWriterSize(conn, msize),
+		closed: make(chan struct{}),
+		rdbuf:  make([]byte, msize),
+		wrbuf:  make([]byte, msize),
+	}
+}
+
+// setmsize resizes the buffers for use with a separate msize. This call must
+// be protected by a mutex or made before passing to other goroutines.
+func (ch *channel) setmsize(msize int) {
+	// NOTE(stevvooe): We cannot safely resize the buffered reader and writer.
+	// Proceed assuming that original size is sufficient.
+
+	if msize < len(ch.rdbuf) {
+		// just change the cap
+		ch.rdbuf = ch.rdbuf[:msize]
+		ch.wrbuf = ch.wrbuf[:msize]
+		return
+	}
+
+	ch.rdbuf = make([]byte, msize)
+	ch.wrbuf = make([]byte, msize)
+}
+
+// ReadFcall reads the next message from the channel into fcall.
+func (ch *channel) readFcall(ctx context.Context, fcall *Fcall) error {
+	select {
+	case <-ch.closed:
+		return ErrClosed
+	default:
+	}
+	log.Println("channel: readFcall", fcall)
+
+	if deadline, ok := ctx.Deadline(); ok {
+		if err := ch.conn.SetReadDeadline(deadline); err != nil {
+			log.Printf("transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err)
+		}
+	}
+
+	n, err := readmsg(ch.brd, ch.rdbuf)
+	if err != nil {
+		// TODO(stevvooe): There may be more we can do here to detect partial
+		// reads. For now, we just propagate the error untouched.
+		return err
+	}
+
+	if n > len(ch.rdbuf) {
+		// TODO(stevvooe): Make this error detectable and respond with error
+		// message.
+		return fmt.Errorf("message large than buffer:", n)
+	}
+
+	return ch.codec.Unmarshal(ch.rdbuf[:n], fcall)
+}
+
+func (ch *channel) writeFcall(ctx context.Context, fcall *Fcall) error {
+	select {
+	case <-ch.closed:
+		return ErrClosed
+	default:
+	}
+	log.Println("channel: writeFcall", fcall)
+
+	if deadline, ok := ctx.Deadline(); ok {
+		if err := ch.conn.SetWriteDeadline(deadline); err != nil {
+			log.Printf("transport: error setting read deadline on %v: %v", ch.conn.RemoteAddr(), err)
+		}
+	}
+
+	n, err := ch.codec.Marshal(ch.wrbuf, fcall)
+	if err != nil {
+		return err
+	}
+
+	p := ch.wrbuf[:n]
+
+	if err := sendmsg(ch.bwr, p); err != nil {
+		return err
+	}
+
+	return ch.bwr.Flush()
+}
+
+// readmsg reads a 9p message into p from rd, ensuring that all bytes are
+// consumed from the size header. If the size header indicates the message is
+// larger than p, the entire message will be discarded, leaving a truncated
+// portion in p. Any error should be treated as a framing error unless n is
+// zero. The caller must check that n is less than or equal to len(p) to
+// ensure that a valid message has been read.
+func readmsg(rd io.Reader, p []byte) (n int, err error) {
+	var msize uint32
+
+	if err := binary.Read(rd, binary.LittleEndian, &msize); err != nil {
+		return 0, err
+	}
+
+	n += binary.Size(msize)
+	mbody := int(msize) - 4
+
+	if mbody < len(p) {
+		p = p[:mbody]
+	}
+
+	np, err := io.ReadFull(rd, p)
+	if err != nil {
+		return np + n, err
+	}
+	n += np
+
+	if mbody > len(p) {
+		// message has been read up to len(p) but we must consume the entire
+		// message. This is an error condition but is non-fatal if we can
+		// consume msize bytes.
+		nn, err := io.CopyN(ioutil.Discard, rd, int64(mbody-len(p)))
+		n += int(nn)
+		if err != nil {
+			return n, err
+		}
+	}
+
+	return n, nil
+}
+
+// sendmsg writes a message of len(p) to wr with a 9p size header. All errors
+// should be considered terminal.
+func sendmsg(wr io.Writer, p []byte) error {
+	size := uint32(len(p) + 4) // message size plus 4-bytes for size.
+	if err := binary.Write(wr, binary.LittleEndian, size); err != nil {
+		return nil
+	}
+
+	// This assume partial writes to wr aren't possible. Not sure if this
+	// valid. Matters during timeout retries.
+	if n, err := wr.Write(p); err != nil {
+		return err
+	} else if n < len(p) {
+		return io.ErrShortWrite
+	}
+
+	return nil
+}
blob - d003a08067db2beb05f4e2124a29c59f1d0a43b8
blob + 1db7807e7ff9d6b0336ace83a50ede2453e8270a
--- cmd/9pr/main.go
+++ cmd/9pr/main.go
@@ -74,6 +74,12 @@ func main() {
 	}
 	commander.readline = rl
 
+	msize, version := commander.session.Version()
+	if err != nil {
+		log.Fatalln(err)
+	}
+	log.Println("9p version", version, msize)
+
 	log.Println("attach root")
 	// attach root
 	commander.nextfid = 1
@@ -123,7 +129,7 @@ func main() {
 			}
 		}
 
-		ctx, _ = context.WithTimeout(commander.ctx, time.Second)
+		ctx, _ = context.WithTimeout(commander.ctx, 5*time.Second)
 		if err := cmd(ctx, args[1:]...); err != nil {
 			log.Printf("👹 %s: %v", name, err)
 		}
@@ -170,11 +176,12 @@ func (c *fsCommander) cmdls(ctx context.Context, args 
 		}
 		defer c.session.Clunk(ctx, targetfid)
 
-		if _, _, err := c.session.Open(ctx, targetfid, p9pnew.OREAD); err != nil {
+		_, msize, err := c.session.Open(ctx, targetfid, p9pnew.OREAD)
+		if err != nil {
 			return err
 		}
 
-		p := make([]byte, 4<<20)
+		p := make([]byte, msize)
 
 		n, err := c.session.Read(ctx, targetfid, p, 0)
 		if err != nil {
@@ -469,7 +476,7 @@ func (s *simpleSession) Open(ctx context.Context, fid 
 	return fi.dir.Qid, 4 << 20, nil
 }
 
-func (s *simpleSession) Create(ctx context.Context, parent p9pnew.Fid, name string, perm uint32, mode uint32) (p9pnew.Qid, error) {
+func (s *simpleSession) Create(ctx context.Context, parent p9pnew.Fid, name string, perm uint32, mode uint32) (p9pnew.Qid, uint32, error) {
 	panic("not implemented")
 }
 
@@ -481,9 +488,6 @@ func (s *simpleSession) WStat(context.Context, p9pnew.
 	panic("not implemented")
 }
 
-// TODO(stevvooe): The version message affects a lot of protocol behavior.
-// Consider hiding it behind the implementation, letting the version get
-// negotiated. The API user should still be able to query it.
-func (s *simpleSession) Version(ctx context.Context, msize uint32, version string) (uint32, string, error) {
-	return 4096, "9P2000", nil
+func (s *simpleSession) Version() (uint32, string) {
+	return 64 << 10, "9P2000"
 }
blob - 200e30422567e89a0b07142ec27544ad2d5364dc
blob + 10d52740dedc0b060356f11de11880aee4f26b6b
--- encoding.go
+++ encoding.go
@@ -1,6 +1,7 @@
 package p9pnew
 
 import (
+	"bytes"
 	"encoding/binary"
 	"fmt"
 	"io"
@@ -23,6 +24,35 @@ func EncodeDir(wr io.Writer, d *Dir) error {
 func DecodeDir(rd io.Reader, d *Dir) error {
 	dec := &decoder{rd}
 	return dec.decode(d)
+}
+
+type Codec interface {
+	Unmarshal(data []byte, v interface{}) error
+	Marshal(data []byte, v interface{}) (n int, err error)
+}
+
+type codec9p struct{}
+
+func (c codec9p) Unmarshal(data []byte, v interface{}) error {
+	dec := &decoder{bytes.NewReader(data)}
+	return dec.decode(v)
+}
+
+func (c codec9p) Marshal(data []byte, v interface{}) (n int, err error) {
+	n = int(size9p(v))
+
+	buf := bytes.NewBuffer(data[:0])
+	enc := &encoder{buf}
+
+	if err := enc.encode(v); err != nil {
+		return buf.Len(), nil
+	}
+
+	if len(data) < buf.Len() {
+		return len(data), io.ErrShortBuffer
+	}
+
+	return buf.Len(), nil
 }
 
 // NOTE(stevvooe): This file covers 9p encoding and decoding (despite just
@@ -118,7 +148,7 @@ func (e *encoder) encode(vs ...interface{}) error {
 				return err
 			}
 		case *Fcall:
-			if err := e.encode(size9p(v), v.Type, v.Tag, v.Message); err != nil {
+			if err := e.encode(v.Type, v.Tag, v.Message); err != nil {
 				return err
 			}
 		default:
@@ -189,17 +219,20 @@ func (d *decoder) decode(vs ...interface{}) error {
 				return err
 			}
 		case *Fcall:
-			var size uint32
-			if err := d.decode(&size, &v.Type, &v.Tag); err != nil {
+			if err := d.decode(&v.Type, &v.Tag); err != nil {
 				return err
 			}
 
 			var err error
 			v.Message, err = newMessage(v.Type)
 			if err != nil {
+				log.Printf("unknown message type %#v", v.Type)
 				return err
 			}
 
+			// take the address of v.Message from the struct and encode into
+			// that.
+
 			if err := d.decode(v.Message); err != nil {
 				return err
 			}
@@ -300,7 +333,7 @@ func size9p(vs ...interface{}) uint32 {
 
 			s += size9p(elements...)
 		case Fcall:
-			s += size9p(uint32(0), v.Type, v.Tag, v.Message)
+			s += size9p(v.Type, v.Tag, v.Message)
 		case *Fcall:
 			s += size9p(*v)
 		default:
@@ -330,11 +363,6 @@ func fields9p(v interface{}) ([]interface{}, error) {
 			return nil, fmt.Errorf("can't interface: %v", f)
 		}
 
-		if !f.CanSet() {
-			panic("asdf")
-			return nil, fmt.Errorf("cannot set %v", f)
-		}
-
 		if f.CanAddr() {
 			f = f.Addr()
 		}
blob - 6e11cca7e4518ea4c56ec3c5b7877dce8cd47fb6
blob + 31ff66b1312bc7cae6d6b1ec9f8cc17ece4b5744
--- encoding_test.go
+++ encoding_test.go
@@ -40,7 +40,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x13, 0x0, 0x0, 0x0,
 				0x64, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0,
 				0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54},
 		},
@@ -55,7 +54,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x13, 0x0, 0x0, 0x0,
 				0x65, 0xcf, 0x8, 0x0, 0x4, 0x0, 0x0,
 				0x6, 0x0, 0x39, 0x50, 0x54, 0x45, 0x53, 0x54},
 		},
@@ -71,7 +69,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x1a, 0x0, 0x0, 0x0,
 				0x6e, 0x22, 0x16, 0xf2, 0x3, 0x0, 0x0, 0xf3, 0x3, 0x0, 0x0,
 				0x3, 0x0, // len(wnames)
 				0x1, 0x0, 0x61, // "a"
@@ -97,7 +94,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x23, 0x0, 0x0, 0x0,
 				0x6f, 0xb4, 0x15,
 				0x2, 0x0,
 				0x80, 0x68, 0x2b, 0x0, 0x0, 0x57, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0,
@@ -113,7 +109,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x1b, 0x0, 0x0, 0x0,
 				0x75, 0xb4, 0x15,
 				0x12, 0x0,
 				0x61, 0x20, 0x6c, 0x6f, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x62, 0x79, 0x74, 0x65, 0x20, 0x64, 0x61, 0x74, 0x61},
@@ -144,7 +139,6 @@ func TestEncodeDecode(t *testing.T) {
 				},
 			},
 			marshaled: []byte{
-				0x47, 0x0, 0x0, 0x0,
 				0x7d, 0xb4, 0x15,
 				// 0x40, 0x0, // TODO(stevvooe): Include Dir size. Not straightforward.
 				0xff, 0xff, // type
blob - 9e03f7e1d58923ae84789c8cf90fab13bc7c9b6e
blob + 5689477107327c6ad5e089a80ad44d7096a206a3
--- fcall.go
+++ fcall.go
@@ -109,6 +109,7 @@ type Fcall struct {
 func newFcall(msg Message) *Fcall {
 	return &Fcall{
 		Type:    msg.Type(),
+		Tag:     NOTAG,
 		Message: msg,
 	}
 }
@@ -246,8 +247,8 @@ type MessageTopen struct {
 }
 
 type MessageRopen struct {
-	Qid   Qid
-	Msize uint32
+	Qid    Qid
+	IOUnit uint32
 }
 
 type MessageTcreate struct {
blob - db1076f7a47a1f6a1475b98a52947c464adaccc5
blob + 325347028d2ac5a31e07eb8ed43af174b5a3beb1
--- server.go
+++ server.go
@@ -51,6 +51,11 @@ func (s *server) run() {
 
 		log.Println("server:", "wait")
 		fcall := new(Fcall)
+
+		// BUG(stevvooe): The decoder is not reliably consuming all of the
+		// bytes of the wire. Needs to be setup to consume all bytes indicated
+		// in the size portion. Should use msize from the version negotiation.
+
 		if err := dec.decode(fcall); err != nil {
 			log.Println("server decoding fcall:", err)
 			continue
@@ -136,14 +141,14 @@ func (s *server) handle(ctx context.Context, req *Fcal
 			return nil, fmt.Errorf("bad message: %v message=%v", req, req.Message)
 		}
 
-		qid, msize, err := s.session.Open(ctx, reqmsg.Fid, reqmsg.Mode)
+		qid, iounit, err := s.session.Open(ctx, reqmsg.Fid, reqmsg.Mode)
 		if err != nil {
 			return nil, err
 		}
 
 		resp = newFcall(&MessageRopen{
-			Qid:   qid,
-			Msize: msize,
+			Qid:    qid,
+			IOUnit: iounit,
 		})
 	case Tread:
 		reqmsg, ok := req.Message.(*MessageTread)
blob - 37779b341b219f43c948e22ba0287ac579e16625
blob + 91e26ec436170573317f400b7da56a6a2971d62b
--- session.go
+++ session.go
@@ -26,14 +26,14 @@ type Session interface {
 	Read(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error)
 	Write(ctx context.Context, fid Fid, p []byte, offset int64) (n int, err error)
 	Open(ctx context.Context, fid Fid, mode uint8) (Qid, uint32, error)
-	Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, error)
+	Create(ctx context.Context, parent Fid, name string, perm uint32, mode uint32) (Qid, uint32, error)
 	Stat(context.Context, Fid) (Dir, error)
 	WStat(context.Context, Fid, Dir) error
 
-	// TODO(stevvooe): The version message affects a lot of protocol behavior.
-	// Consider hiding it behind the implementation, letting the version get
-	// negotiated. The API user should still be able to query it.
-	Version(ctx context.Context, msize uint32, version string) (uint32, string, error)
+	// Version returns the supported version and msize of the session. This
+	// can be affected by negotiating or the level of support provided by the
+	// session implementation.
+	Version() (msize uint32, version string)
 }
 
 func Dial(ctx context.Context, addr string) (Session, error) {
blob - 7c956dd15cc2364349bdac627b8a8af380784de7
blob + 4b25bc02d50aef020b58e0cb0ca06ea43cedede0
--- transport.go
+++ transport.go
@@ -1,11 +1,9 @@
 package p9pnew
 
 import (
-	"bufio"
 	"fmt"
 	"log"
 	"net"
-	"time"
 
 	"golang.org/x/net/context"
 )
@@ -20,17 +18,17 @@ type roundTripper interface {
 
 type transport struct {
 	ctx      context.Context
-	conn     net.Conn
+	ch       *channel
 	requests chan *fcallRequest
 	closed   chan struct{}
 
 	tags uint16
 }
 
-func newTransport(ctx context.Context, conn net.Conn) roundTripper {
+func newTransport(ctx context.Context, ch *channel) roundTripper {
 	t := &transport{
 		ctx:      ctx,
-		conn:     conn,
+		ch:       ch,
 		requests: make(chan *fcallRequest),
 		closed:   make(chan struct{}),
 	}
@@ -77,6 +75,7 @@ func (t *transport) send(ctx context.Context, fcall *F
 	case <-ctx.Done():
 		return nil, ctx.Err()
 	case resp := <-req.response:
+		log.Println("resp", resp)
 		if resp.Type == Rerror {
 			// pack the error into something useful
 			respmesg, ok := resp.Message.(*MessageRerror)
@@ -100,56 +99,42 @@ func (t *transport) handle() {
 		tags      Tag
 		// outstanding provides a map of tags to outstanding requests.
 		outstanding = map[Tag]*fcallRequest{}
-		brd         = bufio.NewReader(t.conn)
-		bwr         = bufio.NewWriter(t.conn)
-		enc         = &encoder{bwr}
-		dec         = &decoder{brd}
 	)
 
 	// loop to read messages off of the connection
 	go func() {
-
+		defer func() {
+			log.Println("exited read loop")
+			close(t.closed)
+		}()
 	loop:
 		for {
-			const pump = time.Second
-
-			// Continuously set the read dead line pump the loop below. We can
-			// probably set a connection dead threshold that can count these.
-			// Usually, this would only matter when there are actually
-			// outstanding requests.
-			deadline, ok := t.ctx.Deadline()
-			if !ok {
-				deadline = time.Now().Add(pump)
-			} else {
-				// if the deadline is before
-				nd := time.Now().Add(pump)
-				if nd.Before(deadline) {
-					deadline = nd
-				}
-			}
-
-			if err := t.conn.SetReadDeadline(deadline); err != nil {
-				log.Printf("error setting read deadline: %v", err)
-			}
-
-			fc := new(Fcall)
-			if err := dec.decode(fc); err != nil {
+			fcall := new(Fcall)
+			if err := t.ch.readFcall(t.ctx, fcall); err != nil {
 				switch err := err.(type) {
 				case net.Error:
 					if err.Timeout() || err.Temporary() {
-						break loop
+						// BUG(stevvooe): There may be partial reads under
+						// timeout errors where this is actually fatal.
+
+						// can only retry if we haven't offset the frame.
+						continue loop
 					}
 				}
 
-				panic(fmt.Sprintf("connection read error: %v", err))
+				log.Println("fatal error reading msg:", err)
+				t.Close()
+				return
 			}
 
 			select {
 			case <-t.ctx.Done():
+				log.Println("ctx done")
 				return
 			case <-t.closed:
+				log.Println("transport closed")
 				return
-			case responses <- fc:
+			case responses <- fcall:
 			}
 		}
 	}()
@@ -157,37 +142,33 @@ func (t *transport) handle() {
 	for {
 		select {
 		case req := <-t.requests:
-			tags++
-			req.fcall.Tag = tags
-			outstanding[req.fcall.Tag] = req
 
-			// use deadline to set write deadline for this request.
-			deadline, ok := req.ctx.Deadline()
-			if !ok {
-				deadline = time.Now().Add(time.Second)
+			log.Println("send", req.fcall)
+			if req.fcall.Type != Tversion {
+				tags++
+				req.fcall.Tag = tags
+				outstanding[req.fcall.Tag] = req
+			} else {
+				// TODO(stevvooe): Man this protocol is bad. Version messages
+				// have no response tag. Effectively, the client can only have
+				// one version call outstanding at a time. We have to create
+				// an entire special code path to handle it. The client
+				// shouldn't proceed until the version reply is completed.
+				req.fcall.Tag = NOTAG
 			}
 
-			if err := t.conn.SetWriteDeadline(deadline); err != nil {
-				log.Printf("error setting write deadline: %v", err)
-			}
-
 			// TODO(stevvooe): Consider the case of requests that never
 			// receive a response. We need to remove the fcall context from
 			// the tag map and dealloc the tag. We may also want to send a
 			// flush for the tag.
-
-			log.Println("send", req.fcall)
-			if err := enc.encode(req.fcall); err != nil {
+			if err := t.ch.writeFcall(req.ctx, req.fcall); err != nil {
 				delete(outstanding, req.fcall.Tag)
 				req.err <- err
 			}
-			if err := bwr.Flush(); err != nil {
-				delete(outstanding, req.fcall.Tag)
-				req.err <- err
-			}
 
 			log.Println("sent", req.fcall)
 		case b := <-responses:
+			log.Println("recv", b)
 			req, ok := outstanding[b.Tag]
 			if !ok {
 				panic("unknown tag received")
blob - a721ab636b2e82ae5b06a92c70f3c5b17e96a47e
blob + 604d967a1ddcf792eb4bba17b3110e4ac5814534
--- types.go
+++ types.go
@@ -1,6 +1,9 @@
 package p9pnew
 
-import "time"
+import (
+	"fmt"
+	"time"
+)
 
 const (
 	NOFID = ^Fid(0)
@@ -14,20 +17,35 @@ const (
 	DMMOUNT  = 0x10000000 // mode bit for mounted channel
 	DMAUTH   = 0x08000000 // mode bit for authentication file
 	DMTMP    = 0x04000000 // mode bit for non-backed-up files
-	DMREAD   = 0x4        // mode bit for read permission
-	DMWRITE  = 0x2        // mode bit for write permission
-	DMEXEC   = 0x1        // mode bit for execute permission
+
+	// 9p2000.u extensions
+	DMSYMLINK   = 0x02000000
+	DMDEVICE    = 0x00800000
+	DMNAMEDPIPE = 0x00200000
+	DMSOCKET    = 0x00100000
+	DMSETUID    = 0x00080000
+	DMSETGID    = 0x00040000
+
+	DMREAD  = 0x4 // mode bit for read permission
+	DMWRITE = 0x2 // mode bit for write permission
+	DMEXEC  = 0x1 // mode bit for execute permission
 )
 
 const (
-	OREAD   = 0      // open for read
-	OWRITE  = 1      // write
-	ORDWR   = 2      // read and write
-	OEXEC   = 3      // execute, == read but check execute permission
-	OTRUNC  = 16     // or'ed in (except for exec), truncate file first
-	OCEXEC  = 32     // or'ed in, close on exec
-	ORCLOSE = 64     // or'ed in, remove on close
-	OEXCL   = 0x1000 // or'ed in, exclusive use (create only)
+	OREAD  = 0x00 // open for read
+	OWRITE = 0x01 // write
+	ORDWR  = 0x02 // read and write
+	OEXEC  = 0x03 // execute, == read but check execute permission
+
+	// PROPOSAL(stevvooe): Possible protocal extension to allow the create of
+	// symlinks. Initially, the link is created with no value. Read and write
+	// to read and set the link value.
+	OSYMLINK = 0x04
+
+	// or'd in
+	OTRUNC  = 0x10 // or'ed in (except for exec), truncate file first
+	OCEXEC  = 0x20 // or'ed in, close on exec
+	ORCLOSE = 0x40 // or'ed in, remove on close
 )
 
 type QType uint8
@@ -42,28 +60,58 @@ const (
 	QTFILE   QType = 0x00 // plain file
 )
 
+func (qt QType) String() string {
+	switch qt {
+	case QTDIR:
+		return "dir"
+	case QTAPPEND:
+		return "append"
+	case QTEXCL:
+		return "excl"
+	case QTMOUNT:
+		return "mount"
+	case QTAUTH:
+		return "auth"
+	case QTTMP:
+		return "tmp"
+	case QTFILE:
+		return "file"
+	}
+
+	return "unknown"
+}
+
 type Fid uint32
 
 type Qid struct {
-	Type    QType
+	Type    QType `9p:type,1`
 	Version uint32
 	Path    uint64
 }
 
+func (qid Qid) String() string {
+	return fmt.Sprintf("Qid(%v, version=%x, path=%x)",
+		qid.Type, qid.Version, qid.Path)
+}
+
 type Dir struct {
-	Type       uint16
-	Dev        uint32
-	Qid        Qid
-	Mode       uint32
+	Type uint16
+	Dev  uint32
+	Qid  Qid
+	Mode uint32
+
+	// BUG(stevvooe): The Year 2038 is coming soon. 9p wire protocol has these
+	// as 4 byte epoch times. Some possibilities include time dilation fields
+	// or atemporal files. We can also just not use them and set them to zero.
+
 	AccessTime time.Time
 	ModTime    time.Time
-	Length     uint64
-	Name       string
-	UID        string
-	GID        string
-	MUID       string
 
-	// TODO(stevvooe): 9p2000.u/L should go here.
+	Length uint64
+	Name   string
+	UID    string
+	GID    string
+	MUID   string
 }
 
 //