1763 lines · 54.2 KB · Java
RawDownload
package plan9; /* * 9P2000 client * (a renaming of the StyxClient of styx-n-9p.googlecode.com) * * * TO DO *	full walk (chunks of MAXWELEM, ..) * * Copyright © 2005 Vita Nuova Holdings Limited [C H Forsyth, forsyth@vitanuova.com] * Subject to the terms of the MIT-template (google for a copy) */ import java.nio.ByteBuffer;	// not ideal, but will do for nowimport java.nio.channels.Channel;import java.nio.channels.ByteChannel;import java.util.StringTokenizer;import java.util.ArrayList;import java.util.concurrent.atomic.AtomicInteger; import java.io.EOFException;import java.io.IOException;import java.io.InterruptedIOException;import java.nio.channels.ClosedChannelException;import java.nio.channels.AsynchronousCloseException; import static plan9.Ninep.*; import plan9.lib.Misc;import plan9.lib.Strings; /** * NinepClient provides application-level access to a set of 9P servers, each accessed over a separate ByteChannel connection. * An instance of NinepClient provides state for a set of 9P connections. * Each 9P connection is represented by an instance of the inner class <i>Conn</i> ({@link NinepClient.Conn}), * associated with a ByteChannel connected to its server. Each 9P server has one or more named file systems, * including a default file system (named by the empty string). * <p> * A client <i>attaches</i> to one of those file systems. * If the server requires authentication, that <i>attach</i> request must be preceded by a * conversation on an authentication file (or "auth file") that establishes permission to request the attach. * A special <i>auth</i> request creates a new auth file. (The "file" is virtual and anonymous.) * If the server does not demand authentication, it will refuse an attempt to create an auth file. The usual * sequence is to attempt an <i>auth</i>, authenticate on the resulting auth file if it succeeds, * then <i>attach</i> after successful authentication but if the <i>auth</i> fails, * to attempt the <i>attach</i> anyway, without authentication. * (If the <i>auth</i> succeeds, but the conversation on the auth file fails, the <i>attach</i> will not succeed.) * For example, the class <i>NinepImport</i>, which encapsulates importing a file system exported by a 9P cpu server, * also encapsulates the <i>auth</i>-authentication-<i>attach</i> sequence in its simple <i>attach</i> operation. * <p> * The result of a successful attach is an instance of the inner class <i>FS</i> ({@link NinepClient.FS}), which provides file-system level * operations, including open, create, stat, wstat, and remove. The open and create operations return an instance * of the <i>FD</i> inner class ({@link NinepClient.FD}), allowing IO to a named file, compatible with the <i>ByteChannel</i> interface, * with extensions to support 9P's stat and wstat operations. */public class NinepClient { 	/** open for read */	public static final int	OREAD = 0; 	 	/** open for write */	public static final int	OWRITE = 1; 	 	/** open for read and write */	public static final int	ORDWR = 2; 	 	/** open for execute, == read but check execute permission */	public static final int	OEXEC = 3; 	 	/** OR'ed in to open mode: truncate file first */	public static final int	OTRUNC = 16; 	 	/** OR'ed in to open mode: remove on close */	public static final int	ORCLOSE = 64;  	/** OR'ed in to open mode: exclusive-create */	public static final int OEXCL = 0x1000; 	/** The offset in seek is an absolute offset from the start of the file */	static public final int SEEKSTART = 0; 	/** The offset in seek is a signed offset relative to the current file position */	static public final int SEEKRELA = 1; 	/** The offset in seek is a signed offset relative to the length of the file (ie, end of file) */	static public final int SEEKEND = 2; 	static public final String Enomem = "out of memory";	static public final String Eexists = "file already exists";	static public final String Eisdir = "file is a directory";	static public final String Enegoff = "negative i/o offset";	static public final String Etoosmall = "read or write too small";	static public final String Ebadusefd = "inappropriate use of fd";	static public final String Ehungup = "i/o on hungup channel";	static public final String Eversion = "version not established for mount channel";	static public final String Eintr = "interrupted";	static public final String Enotdir = "not a directory";	static public final String Edoesnotexist = "does not exist";	static public final String Emountrpc = "mount rpc error"; 	private static ThreadLocal<String> lasterror = new ThreadLocal<String>(); 	/**	 * Return the per-thread error string, which gives the error text from the most recent error.	 */	public final static String errstr() { return lasterror.get(); } 	/**	 * Set the per-thread error string.	 */	public final static void werrstr(String s) { lasterror.set(s); } 	final static void ioerror(String s) throws InterruptedIOException, IOException {		lasterror.set(s);		if(s != null && s.equals(Eintr))			throw new InterruptedIOException(s);		throw new IOException(s);	}	final static void ioerror() throws IOException {		ioerror(errstr());	} 	private static Ninep ninep = new Ninep(); 	private static Log log = LogFactory.logger(NinepClient.class); 	static String where(){		String s = "";		for(StackTraceElement e: Thread.currentThread().getStackTrace())			s += e + "\n";		return s;	} 	/**	 * Create a new 9P client instance.	 * Given a NinepClient, the next step is to create a Conn, establishing a 9P connection on a given ByteChannel.	 * Conn, FS, and FD do all the real work.	 */	public NinepClient(){		settag(0);		settag(Ninep.NOTAG);	} 	private static class Request {		Ninep.Tmsg	t;		Ninep.Rmsg	r;		Request	flushed;		long	timeout; 		Request(Ninep.Tmsg t){ this.t = t; timeout = 0; }		Request(Ninep.Tmsg t, Request old){ this.t = t; this.flushed = old; timeout = old.timeout; } 		void timelimit(long ms){ timeout = ms; } 		synchronized final void awaitreply() throws InterruptedException {			if(timeout == 0){				while(r == null)					this.wait();			}else{				long t0 = System.currentTimeMillis();				while(r == null){					this.wait(timeout);					if(r != null)						break;					if(t0+timeout < System.currentTimeMillis())						throw new InterruptedException("file system operation timed-out");				}			}		} 		synchronized final void replied(Ninep.Rmsg reply){			if(this.t instanceof Ninep.Tflush && reply instanceof Ninep.Rerror)				reply = ninep.new Rflush(reply.tag);	// must not Rerror a Tflush			this.r = reply;			this.notify();			if(false && log.tracing())				log.trace("notify: %d %s %s", t.tag, t.mname(), reply.mname());		} 		synchronized final void replyerr(String why, boolean hangup){			Ninep.Rmsg reply; 			if(this.t instanceof Ninep.Tflush)				reply = ninep.new Rflush(this.t.tag);	// must not Rerror a Tflush			else				reply = ninep.new Rerror(this.t.tag, why, hangup);			this.r = reply;			this.notify();		} 		final void replyerr(String why){			replyerr(why, false);		}	} 	static class ReqEl {		ReqEl	next;		Request	r;			ReqEl(Request r, ReqEl next){				this.r = r; this.next = next;			}	} 	static class RQ {		ReqEl	qh;		ReqEl	qt;		String	err; 		synchronized Request get() throws InterruptedException {			ReqEl el; 			while((el = qh) == null && err == null)				wait();			if(err != null)				throw new InterruptedException(Eintr);			qh = el.next;			return el.r;		}		synchronized void put(Request r) throws InterruptedIOException, IOException {			if(err != null)				ioerror(err);			ReqEl el = new ReqEl(r, null);			if(qh == null){				qh = el;				notify();			}else				qt.next = el;			qt = el;		}		synchronized void poison(String reason){			ReqEl el; 			if(err == null)				err = reason;			for(; (el = qh) != null; qh = el.next)				el.r.replyerr(err, true);		}	} 	// record a set of outstanding Requests, in order to ensure replying to flushes in correct order on error	private static class Requests {		ReqEl	set;	// set of requests; tags are unique		String	err;	// !=null: fail incoming requests with this error 		// add a new Request; ordering is unimportant		synchronized final void add(Request req){			if(err == null)				set = new ReqEl(req, set);			else				req.replied(ninep.new Rerror(req.t.tag, err, true));		} 		// find the Request corresponding to r's tag, and mark it done, notifying its author, and freeing the tag		final void completed(Ninep.Rmsg r){			ReqEl el, prev;			synchronized(this){				prev = null;				for(el = set; el != null; el = el.next){					if(el.r.t.tag == r.tag){						if(prev != null)							prev.next = el.next;						else							set = el.next;						puttag(r.tag);						el.r.replied(r);						return;					}					prev = el;				}			}			log.warn("unexpected reply tag %d type %d (%s)", r.tag, r.mtype(), r.mname());			//System.out.print("TAG: "+where());		}		final void diagnose(String why){			log.warn("Ninep message error: "+why);		} 		// shut down the channel, with reason, forcing all pending requests to fail, and answering flushes		final void shutdown(String why){			if(log.debugging())				log.debug("shut down: "+why);			synchronized(this){				if(err == null)	// first error might be most accurate					err = why;				ReqEl el;				while((el = set) != null){					set = el.next;					el.r.replyerr(err, true);				}			}		}	} 	// thanks to Java's ByteChannel, which allows close to be repeated, we can't recycle fids just by recycling FDs	// because there isn't any operation to mark a ByteChannel as no longer used	// instead, separate Fid and FD	private static class Fid {		int	fid;		int	inuse;		Fid	next; 		Fid(int v){			this.fid = v;			this.inuse = 1;			this.next = null;		} 		synchronized Fid	incref(){			inuse++;			return this;		} 		synchronized int	decref(){			return --inuse;		} 		synchronized boolean isfree(){			return inuse == 0;		}	} 	// recycle fids by recycling FDs	private static class Fids {		Fid	avail;		int	fidgen = 0;		int	inuse = 0;		boolean	closing = false; 		synchronized Fid alloc() {			Fid f;			f = avail;			if(f != null){				avail = f.next;				f.next = null;				f.inuse = 1;			}else				f = new Fid(++fidgen);			inuse++;			return f;		} 		synchronized void free(Fid f) {			if(f != null && f.isfree()){				f.next = avail;				avail = f;				inuse--;			}		} 		synchronized boolean close(){			if(inuse != 0){				closing = true;				return false;			}			closing = false;			return true;		} 		synchronized boolean shouldclose(){			return inuse == 0 && closing;		}	} 	/**	 * Conn represents a 9P connection on a given ByteChannel.	 * <p>	 * It provides operations to initialise the connection (version), obtain an authentication file (auth),	 * attach to the root of a file system offered by the server (attach), and close the connection (close).	 * <p>	 * A successful attach returns an FS instance allowing operations on files by name.	 * Two of those operations, create and open, return an FD instance allowing IO on files.	 * Conn's auth operation also returns an FD instance, allowing IO on the resulting authentication file.	 * <p>	 * None of the operations regard failure as exceptional, and therefore do not throw exceptions.	 * Instead they return an error value suitable for the normal return type of the function.	 * A per-thread error string contains text giving the diagnostic for the most recent error.	 * The function errstr returns the error string.	 */	public class Conn {		ByteChannel	fd;		int	msize = 0; 		RQ	writeq = new RQ();	// waiting to be sent to server		Requests	active = new Requests();	// sent to server; waiting for reply		Fids		fids = new Fids(); 		Object	versioning = new Object();	// Tversion queue lock		String	versioned; 		boolean asyncflush = false;		boolean closed = false;		long	optimer = 0; 		public Conn(ByteChannel fd){			this.fd = fd;			(new Writer()).start();			(new Reader()).start();		} 		/**		 * Return the per-thread error string, which gives the error text from the most recent error.		 */		public String errstr(){			return NinepClient.this.errstr();		} 		/**		 * Set time limit in milliseconds for each 9P operation, until reset to zero.		 */		public void setTimeLimit(long ms){			optimer = ms;		} 		/**		 * Optionally specify a maximum message size and protocol version.		 * By default, the protocol will use messages no larger than Ninep.MAXRPC, and negotiate the version "9P2000".		 * The 9P server can negotiate a message size limit that is still smaller.		 * <p>		 * Version does a 9P Tversion transaction.		 *	@param	msize	desired maximum message size in bytes (if 0, use default Ninep.MAXRPC)		 *	@param	v		desired protocol version (if null, use Ninep.VERSION, which is "9P2000")		 *	@return	protocol version negotiated, or null if the negotiation failed (see the error string)		 *	@throws	InterruptedIOException	operation was interrupted (eg, by an alarm)		 *	@throws	ConnectionFailed	9P connection was shut down, by hangup or IO error		 */		public String version(int msize, String v) throws ConnectionFailed, InterruptedIOException {			if(msize == 0)				msize = Ninep.MAXRPC;			if(v == null)				v = Ninep.VERSION;			synchronized(versioning){	// only one Tversion active at once on a connection				if(versioned != null)					return versioned;	// already done; TO DO: check compatibility				Ninep.Rversion r = (Ninep.Rversion)ninepreq(ninep.new Tversion(msize, v));				if(r == null)					return null;				// check compatibility				if(r.msize < 256 || r.msize > 1024*1024){					werrstr("nonsense value of remote msize in version");					return null;				}				if(r.msize > msize){					werrstr("server tries to increase msize in version");					return null;				}				int n = r.version.length();				if(!v.substring(0, n).equals(r.version)){					werrstr("bad 9P version received from server");					return null;				}				this.msize = r.msize;				this.versioned = r.version;			}			return this.versioned;		} 		/**		 * Return a FD (file descriptor) that represents a channel to the authentication system on the server.		 * If the server requires authentication, a successful auth will return an FD instance that allows an		 * auth file on the server to be read and written to exchange authentication data with the server.		 * The aim is to authenticate <i>uname</i>'s access to the server's tree <i>aname</i>.		 *<p>		 * If the server does not require authentication, it will reply with an error, and auth will return null.		 * That does not represent a failure to authenticate, but rather that authentication is not required.		 *	@param	uname	user name that will authenticate		 *	@param	aname	name of server tree; use "" or null to get the default (main) tree		 *	@return	an FD instance connected to an authentication file; returns null if the server does not require authentication		 *	@throws	InterruptedIOException	operation was interrupted (eg, by an alarm)		 *	@throws	ConnectionFailed	9P connection was shut down, by hangup or IO error		 */		public FD auth(String uname, String aname) throws ConnectionFailed, InterruptedIOException {			if(versioned == null && version(0, null) == null)				return null;			FD fd = newfd();			Ninep.Rauth r = (Ninep.Rauth)ninepreq(ninep.new Tauth(fd.fid, uname, aname));			if(r == null){				fd.free();				return null;			}			fd.qid = r.aqid;			fd.mode = Ninep.ORDWR;			return fd;		} 		/**		 * Attach as user <i>uname</i> to the root of the server's tree <i>aname</i>.		 * Return an FS instance giving access to that tree.		 * If the server does not require authentication, <i>afd</i> should be null.		 * If the server does require authentication, <i>afd</i> must be the result of a previous		 * <i>auth</i> call specifying the same <i>uname</i> and <i>aname</i>,		 * over which an authentication protocol has been run successfully.		 *	@param	afd	file descriptor for authentication file, or null if none is needed		 *	@param	uname	user name accessing the file system		 *	@param	aname	access the tree with the given name (null or "" for default tree)		 *	@throws	InterruptedIOException	operation was interrupted (eg, by an alarm)		 *	@throws	ConnectionFailed	9P connection was shut down, by hangup or IO error		 */		public FS attach(FD afd, String uname, String aname) throws ConnectionFailed, InterruptedIOException {			if(versioned == null && version(0, null) == null)

Lines 1–500 of 1763.