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 now import 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 Conn ({@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). *
* A client attaches to one of those file systems. * If the server requires authentication, that attach request must be preceded by a * conversation on an authentication file (or "auth file") that establishes permission to request the attach. * A special auth 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 auth, authenticate on the resulting auth file if it succeeds, * then attach after successful authentication but if the auth fails, * to attempt the attach anyway, without authentication. * (If the auth succeeds, but the conversation on the auth file fails, the attach will not succeed.) * For example, the class NinepImport, which encapsulates importing a file system exported by a 9P cpu server, * also encapsulates the auth-authentication-attach sequence in its simple attach operation. *
* The result of a successful attach is an instance of the inner class FS ({@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 FD inner class ({@link NinepClient.FD}), allowing IO to a named file, compatible with the ByteChannel 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
* 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).
*
* 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.
*
* 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.
*
* 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 uname's access to the server's tree aname.
*
* 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 uname to the root of the server's tree aname.
* Return an FS instance giving access to that tree.
* If the server does not require authentication, afd should be null.
* If the server does require authentication, afd must be the result of a previous
* auth call specifying the same uname and aname,
* 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)
return null;
FD fd = newfd();
int afid = Ninep.NOFID;
if(afd != null)
afid = afd.fid;
Ninep.Rattach r = (Ninep.Rattach)ninepreq(ninep.new Tattach(fd.fid, afid, uname, aname));
if(r == null){
fd.free();
return null;
}
fd.name = "/";
fd.qid = r.qid;
return new FS(this, fd);
}
/** On an interrupt, send flush and wait for the reply */
synchronized public void waitflush(){
asyncflush = false;
}
/** On an interrupt, send flush but do not wait for the reply */
synchronized public void quickflush(){
asyncflush = true;
}
synchronized boolean isasyncflush(){
return asyncflush;
}
/** Close the 9P connection as soon as its FS and FD instances have all been closed. */
synchronized public void close(){
if(!closed){
if(fids == null)
return; // already done
if(!fids.close())
return; // still in use
fids = null;
}
shutdown();
}
/** Shut down the 9P connection abruptly, on error */
synchronized public void shutdown(){
if(!closed){
closed = true;
try{
writeq.put(null); // shut down Writer
}catch(IOException e){
// already dead
}
try{
fd.close(); // will have important side-effect of stopping Reader
}catch(IOException e){
// we don't care
}
if(log.tracing())
log.trace("9P channel closed");
}
}
void interrupted() throws InterruptedIOException {
werrstr(Eintr);
throw new InterruptedIOException(Eintr);
}
protected Ninep.Rmsg ninepreq(Ninep.Tmsg f) throws ConnectionFailed, InterruptedIOException {
int otype = f.mtype();
Request req = new Request(f);
boolean flushing = false;
if(optimer != 0)
req.timelimit(optimer);
Work:
for(;;){
try{
/* channel must not be non-blocking (short writes) */
writeq.put(req);
if(flushing && isasyncflush())
interrupted();
req.awaitreply();
break Work;
}catch(InterruptedException e){
if(f.tag == Ninep.NOTAG){
werrstr("9P version request interrupted");
throw new InterruptedIOException(errstr());
}
if(log.tracing())
log.trace("interrupt: flush %s", req.t);
req = new Request(ninep.new Tflush(f.tag), req); // ie, flush original request, not last Tflush
flushing = true;
}catch(InterruptedIOException e){
throw e; // just pass it on, can only be asyncflush
}catch(IOException e){ // anything else is fatal
flushout(req);
werrstr("i/o error: "+e);
active.shutdown(errstr());
throw new ConnectionFailed(errstr(), e);
}
}
if(log.tracing())
log.trace("rcvd reply "+req.r);
if(req.flushed != null){
flushout(req);
interrupted();
}
Ninep.Rmsg r = req.r;
if(r instanceof Ninep.Rerror){
Ninep.Rerror re = (Ninep.Rerror)r;
werrstr(re.ename);
if(re.hangup)
throw new ConnectionFailed(errstr());
return null;
}
if(r instanceof Ninep.Rflush)
interrupted();
if(r.mtype() != otype+1){
log.warn("unexpected reply: tag %d type %d(%s) otag %d otype %d", r.tag, r.mtype(), r.mname(), f.tag, otype);
werrstr("mismatched 9P reply");
return null;
}
return r;
}
// dispose of unanswered messages in a chain of flushes
private void flushout(Request req){
// we can't lock req because active.completed might do so indirectly via replied, but it
// doesn't matter, because the current process is the only one working with req.flushed
for(;;){
if(req.r == null){
// build flush request with tag matching original request
Ninep.Rmsg flush = ninep.new Rflush(req.t.tag);
active.completed(flush);
}
if(req.flushed == null)
break;
req = req.flushed;
}
}
protected final FD newfd(){
if(fids == null)
throw new RuntimeException("programming error: NinepClient used after shutdown");
return new FD(this, fids.alloc());
}
protected final void freefid(Fid fidp){
if(fidp != null){
fids.free(fidp);
if(fids.shouldclose())
close();
}
}
/*
* Java forces a daemon process for Channel I/O in both directions to allow
* waiting requests to be interrupted. An InterruptableChannel exists,
* but cannot be associated with old java.io, and furthermore, poisons
* the channel on the interrupt!
*/
private class Reader extends Thread {
Reader(){
setDaemon(true);
Misc.nominate(this);
}
private final void fillbuf(ByteChannel fd, ByteBuffer b) throws IOException {
while(b.remaining() > 0 && fd.read(b) > 0){
/* skip */
}
b.flip();
if(b.remaining() != b.capacity())
throw new IOException("Ninep message truncated");
}
public final void run(){
try{
Ninep.Unpack unpacker = ninep.new Unpack();
ByteBuffer sizeb = ByteBuffer.allocate(Ninep.BIT32SZ);
for(;;){
int msglim = msize;
if(msglim == 0)
msglim = Ninep.MAXRPC;
fillbuf(fd, sizeb);
int ml = (int)sizeb.get() & 0xFF;
ml |= ((int)sizeb.get() & 0xFF) << 8;
ml |= ((int)sizeb.get() & 0xFF) << 16;
ml |= ((int)sizeb.get() & 0xFF) << 24;
sizeb.clear();
if(ml > msglim){
active.shutdown(String.format("9P message longer than agreed: %d > %d [%s ...]", ml, msglim));
return;
}
if((ml -= Ninep.BIT32SZ) <= 0){
active.shutdown("invalid 9P message size");
return;
}
ByteBuffer b = ByteBuffer.allocate(ml);
fillbuf(fd, b);
try{
Ninep.Rmsg r = unpacker.unpackR(b);
if(log.tracing())
log.trace("<- %s", r);
active.completed(r);
}catch(Ninep.FormatError e){
active.diagnose("can't unpack: bad 9P type or format");
}
}
}catch(EOFException e){
active.shutdown(Ehungup);
}catch(ClosedChannelException e){
active.shutdown(Ehungup);
}catch(IOException e){
active.shutdown("error reading 9P message: "+e);
}
}
}
ByteBuffer packmsg(Ninep.Tmsg f) throws IOException {
int otype = f.mtype();
int tag;
if(otype == Ninep.MTversion)
f.tag = Ninep.NOTAG;
else if((tag = gettag()) != Ninep.NOTAG)
f.tag = tag;
else
throw new IOException("out of 9P message tags"); // unlikely: there are 2^16-2
int size = f.packedsize();
ByteBuffer buf = ByteBuffer.allocate(size);
f.packsize(buf, size);
f.pack(buf);
buf.flip();
return buf;
}
// could have more than one of these, if it's a bottleneck
private class Writer extends Thread {
Writer(){
setDaemon(true);
Misc.nominate(this);
}
public final void run(){
/* we rely on Channel's non-interleaving semantics (TO DO: test) */
/* channel must not be non-blocking (short writes) */
String err;
Service:
for(;;){
Request req;
err = Ehungup;
try{
req = writeq.get();
if(req == null)
break Service;
}catch(InterruptedException e){
break Service;
}
ByteBuffer buf = null;
try{
buf = packmsg(req.t);
}catch(IOException e){
log.trace("pack: "+req.t+": exception: "+e);
req.replyerr("error packing 9P request: "+e.getMessage());
continue;
}
try{
if(log.tracing())
log.trace("-> %s", req.t);
active.add(req); // must add before write, to avoid race with reply
int size = buf.remaining();
if(fd.write(buf) != size)
throw new IOException("short write on 9P channel");
}catch(AsynchronousCloseException e){
req.replyerr(err);
break Service;
}catch(ClosedChannelException e){
req.replyerr(err);
break Service;
}catch(IOException e){
err = Emountrpc+": "+e.getMessage();
req.replyerr(err);
// attempt to continue, as devmnt does
}
}
writeq.poison(err);
if(log.tracing())
log.trace("Writer exit: "+err);
}
}
}
/**
* FS represents an attached file system (file tree) on a 9P connection.
*
* It provides operations to access and change the file system using hierarchical names, including
* create, open, stat, wstat, remove, chdir and getwd. All but chdir and getwd apply to all file types including directories;
* chdir and getwd are restricted to directories.
*
* By convention, these operations do not consider failure to be exceptional, and do not throw exceptions.
* Instead, an error value is returned, appropriate for the operations return type.
* Errstr returns the cause of the last error, as a string. Usually it will be the error text returned by the 9P server,
* although a few errors are diagnosed locally.
*
* Create and open access named files for IO, returning instances of the class FD.
*/
public class FS {
int inuse = 1;
Conn conn;
FD root;
private ThreadLocal
* The underlying connection will be closed after all open files and all other file systems currently using the connection have been closed.
*/
public void close(){ // TO DO: unmount as better name?
synchronized(this){
if(--inuse != 0)
return;
}
root.close();
root = null;
FD fd = getdot();
if(fd != null){
fd.close();
fd = null;
}
conn.close();
}
/**
* Open the file name with the given open mode (eg, Ninep.OREAD, NInep.OWRITE, or Ninep.ORDWR), and return a {@link NinepClient.FD} for subsequent file IO.
* @return a {@link NinepClient.FD} value for IO, or null if the file could not be opened (the error string gives the reason)
* @throws InterruptedIOException operation was interrupted (eg, by an alarm)
* @throws ConnectionFailed 9P connection was shut down, by hangup or IO error
*/
public FD open(String name, int mode) throws ConnectionFailed, InterruptedIOException {
FD fd = walk(name);
if(fd == null)
return null;
Ninep.Ropen r = (Ninep.Ropen)conn.ninepreq(ninep.new Topen(fd.fid, mode));
if(r == null){
fd.close();
return null;
}
fd.open(openmode(mode), r.iounit, r.qid);
return fd;
}
/**
* Create the file name if it does not exist, or truncate it if it does, then open with the given open mode (eg, Ninep.OREAD, NInep.OWRITE, or Ninep.ORDWR), and return a {@link NinepClient.FD} for subsequent file IO.
* If the file had to be created, its permissions are set to perm. If the file already exists, perm is ignored.
* @return a {@link NinepClient.FD} value for IO, or null if the file could not be opened (the error string gives the reason)
* @throws InterruptedIOException operation was interrupted (eg, by an alarm)
* @throws ConnectionFailed 9P connection was shut down, by hangup or IO error
*/
public FD create(String name, int mode, int perm) throws ConnectionFailed, InterruptedIOException {
Parse p = new Parse(name);
if(p.els.length == 0){
werrstr(Eexists);
return null;
}
FD fd = conn.newfd();
if(!walk(walkfrom(p), fd, new Parse(p, 0, p.els.length-1))){ // walk to penultimate entry
fd.free();
return null;
}
/* try to walk to final entry */
switch(opentrunc(fd, p, mode)){
case -1:
fd.close();
return null;
case 1:
return fd;
}
/* must create */
String entry = p.els[p.els.length-1];
Ninep.Rcreate r = (Ninep.Rcreate)conn.ninepreq(ninep.new Tcreate(fd.fid, entry, perm, mode));
if(r != null){
fd.name = addname(fd.name, entry);
fd.mode = openmode(mode);
fd.iounit = r.iounit;
fd.qid = r.qid;
return fd;
}
/* to allow for create/create race, attempt a second open here */
String s = errstr();
if(opentrunc(fd, p, mode) <= 0){
fd.close();
werrstr(s); /* restore original diagnostic */
return null;
}
return fd;
}
/**
* Return the directory information for the named file, or null if an error occurred (setting 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 Dir stat(String name) throws ConnectionFailed, InterruptedIOException {
FD fd = walk(name);
if(fd == null)
return null;
Dir d = fd.qstat();
fd.close();
return d;
}
/**
* Update the directory information for the named file, returning true iff the update succeeded.
* The Dir parameter has a special form: values that should not change will either be the
* file's original values, or special "don't care" values, described in stat(5), typically null or
* empty strings for string values, and ~0 for integer values. Any other values will change the file's
* directory entry to match. If the request is rejected, wstat returns false and updates the error string.
* @param d A Dir instance containing the values to update
* @return true iff the file server accepted the update
* @throws InterruptedIOException operation was interrupted (eg, by an alarm)
* @throws ConnectionFailed 9P connection was shut down, by hangup or IO error
*/
public boolean wstat(String name, Dir d) throws ConnectionFailed, InterruptedIOException {
FD fd = walk(name);
if(fd == null)
return false;
boolean r = fd.wstat(d);
fd.close();
return r;
}
/**
* Remove the named file, returning true if it was successfully removed, and false on error (setting 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 boolean remove(String name) throws ConnectionFailed, InterruptedIOException {
FD fd = walk(name);
if(fd == null)
return false;
Ninep.Rremove r = (Ninep.Rremove)conn.ninepreq(ninep.new Tremove(fd.fid));
fd.close();
return r != null;
}
/**
* Set the current directory to the named directory, returning true on success, and false on any error (eg, doesn't exist, not a directory), setting 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 boolean chdir(String name) throws ConnectionFailed, InterruptedIOException {
if(name == null){
setdot(null);
return true;
}
FD fd = walk(name);
if(fd == null)
return false;
setdot(fd);
return true;
}
/** Return the name of the current directory */
public String getwd(){
FD fd = getdot();
if(fd == null)
return root.name;
return fd.name;
}
/* implementation */
protected class Parse {
String name;
String[] els;
boolean abs;
Parse(String name){
abs = name.length() != 0 && name.charAt(0) == '/';
this.name = name;
StringTokenizer st = new StringTokenizer(name, "/");
int n = st.countTokens();
els = new String[n];
int o = 0;
for(int i = 0; i < n; i++){ // squeeze out dots
String s = st.nextToken();
if(!s.equals("."))
els[o++] = s;
}
if(o == n)
return;
String[] shrink = new String[o];
System.arraycopy(els, 0, shrink, 0, o);
els = shrink;
shrink = null;
}
Parse(Parse p, int s, int e){ // slice
abs = p.abs && s == 0;
name = p.name;
els = new String[e-s];
for(int i = 0; i < els.length; i++)
els[i] = p.els[s+i];
}
}
protected boolean walk(FD ofd, FD fd, Parse p) throws ConnectionFailed, InterruptedIOException {
Ninep.Rwalk r = (Ninep.Rwalk)conn.ninepreq(ninep.new Twalk(ofd.fid, fd.fid, p.els));
if(r == null)
return false;
if(p.els.length > Ninep.MAXWELEM){
werrstr("walk too deep"); /* TO DO: chunks of MAXWELEM */
return false;
}
if(r.qids.length != p.els.length){ /* TO DO: chunks of MAXWELEM */
StringBuilder bs = new StringBuilder(64);
bs.append('\'');
if(p.abs)
bs.append('/');
int i;
for(i = 0; i < r.qids.length; i++){
if(i != 0)
bs.append('/');
bs.append(p.els[i]);
}
if(i == 0 || (r.qids[i-1].qtype & Qid.QTDIR) != 0){
if(bs.length() != 0)
bs.append('/');
bs.append(p.els[i]);
bs.append('\'');
bs.append(' ');
bs.append(Edoesnotexist); // could be permission problem
}else{
bs.append('\'');
bs.append(' ');
bs.append(Enotdir);
}
werrstr(bs.toString());
return false;
}
fd.name = ofd.name;
if(p.els.length > 0){
for(int i = 0; i < p.els.length; i++)
fd.name = addname(fd.name, p.els[i]);
fd.qid = r.qids[r.qids.length-1];
}else
fd.qid = ofd.qid;
return true;
}
protected FD walkfrom(Parse p){
FD ofd = root;
if(!p.abs){
FD cwd = getdot();
if(cwd != null)
return cwd; // the reference count in dot will hold it for this process
}
return ofd;
}
protected boolean walk(FD fd, String name) throws ConnectionFailed, InterruptedIOException {
Parse p = new Parse(name);
return walk(walkfrom(p), fd, new Parse(name));
}
protected FD walk(String name) throws ConnectionFailed, InterruptedIOException {
FD fd = conn.newfd();
try{
if(!walk(fd, name)){
fd.free();
fd = null;
}
return fd;
}catch(ConnectionFailed e){
fd.free();
throw e;
}catch(InterruptedIOException e){
fd.free();
throw e;
}
}
protected int opentrunc(FD fd, Parse p, int mode) throws ConnectionFailed, InterruptedIOException {
/* if able to walk one level, open and truncate */
if(!walk(fd, fd, new Parse(p, p.els.length-1, p.els.length)))
return 0; // doesn't exist
if((mode & Ninep.OEXCL) != 0){
werrstr(Eexists);
return -1;
}
/* try an open */
Ninep.Ropen r = (Ninep.Ropen)conn.ninepreq(ninep.new Topen(fd.fid, mode|Ninep.OTRUNC));
if(r == null)
return -1;
fd.mode = openmode(mode);
fd.iounit = r.iounit;
fd.qid = r.qid;
return 1;
}
protected final String addname(String n1, String n2){ // TO DO: move to Strings, or Names class (cf. Names.m)
if(n1 == null)
n1 = "";
if(n2 == null || n2.length() == 0)
return n1;
int l1 = n1.length();
if(n2.equals("..")){
for(int i = l1; --i >= 1;)
if(n1.charAt(i) == '/')
return n1.substring(0, i);
return "/";
}
if(l1 == 0 || n1.charAt(l1-1) != '/')
return n1+"/"+n2;
return n1+n2;
}
}
/**
* FD represents a file descriptor open on a file in a 9P file system, usually obtained from an operation in FS.
*
* FD implements a superset of Java nio's interface ByteChannel.
*
* Extensions include a wider range of read and write operations, both with and without a file offset, and
* a set of operations such as stat and dirread, reflecting the underlying 9P operations.
*/
public static class FD implements ByteChannel {
Conn conn;
Fid fidp;
int fid; // arguably should be long, since it's unsigned
int mode = -1;
int iounit;
Qid qid;
long offset;
String name; // not yet used
private FD(Conn conn, Fid fidp){
this.conn = conn;
this.fidp = fidp;
this.fid = fidp.fid;
this.mode = -1;
this.iounit = 0;
this.offset = 0;
this.name = null;
}
// TO DO: readn?
/**
* Return the per-thread error string, which gives the error text from the most recent error.
*/
public String errstr() { return NinepClient.errstr(); }
/**
* Set the per-thread error string.
*/
public final static void werrstr(String s) { NinepClient.werrstr(s); }
/**
* Return true iff the FD has not been closed.
*/
public boolean isOpen(){ return fidp != null; }
/**
* Close the file.
*
* If not already closed, close the file and clunk the underlying 9P file.
*
* Read as many bytes as remain in the buffer, returning the number of bytes read,
* which might be zero. (Implements ReadableByteChannel.read, except that concurrent access is allowed.)
*
* The file's current offset is updated to reflect the bytes read.
*
* @param dest Destination buffer for the bytes read.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int read(ByteBuffer dest) throws IOException {
return read(dest, dest.remaining());
}
/**
* Read up to n bytes into a ByteBuffer from the file starting at the given offset and return number of bytes read (0 at end of file).
* @param dest Destination buffer for the bytes read.
* @param n maximum number of bytes to read
* @param offset byte offset from which to read
* @return number of bytes read, or 0 at end-of-file
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int read(ByteBuffer dest, int n, long offset) throws IOException {
ByteBuffer rb = read(n, offset);
if(rb == null)
return 0;
n = rb.remaining();
dest.put(rb);
return n;
}
/**
* Read up to n bytes into a ByteBuffer from the file starting at the current offset and return number of bytes read (0 at end of file).
*
* Write the sequence of bytes remaining in the buffer, returning the number of bytes actually written.
* (Implements WritableByteChannel.write, except that concurrent access is allowed.)
*
* The file's current offset is updated to reflect the bytes written.
* @param src Source buffer for the bytes to write.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int write(ByteBuffer src) throws IOException {
return write(src, src.remaining());
}
/**
* Write up to n bytes from the buffer src to the file starting at the given offset in bytes.
*
* Write the sequence of bytes remaining in the buffer, but no more than n, returning the number of bytes actually written.
*
* Write the sequence of bytes remaining in the buffer, but no more than n, returning the number of bytes actually written.
*
* Returns the number of bytes written. Throws an exception if not all bytes were written.
*
* @param s The string to write to the file
* @return The number of bytes written.
* @throws IOException Not all the bytes were successfully written; and any other error.
*/
public int write(String s) throws IOException {
byte a[] = Strings.bytes(s);
if(write(a, a.length) != a.length)
ioerror("write truncated");
return a.length;
}
/**
* Read an array of Dir values from a directory.
*
* The file must be a directory, or an exception results.
* Return an array containing a sequence of directory entries read from the directory at its current offset,
* or null if at the end of the directory.
*
* @return An array of Dir values, or null if no more directory entries remain.
* @throws IOException On any error when trying to read the directory.
*/
public Dir[] dirread() throws IOException {
ByteBuffer b;
b = read(4096); // arbitrary value bigger than largest single directory entry
if(b == null)
return null;
ArrayList
* The file must be a directory, or an exception results.
* Return an array containing a sequence of all remaining directory entries read from the directory at its current offset,
* or null if at the end of the directory.
*
* @return An array of Dir values, which will be empty (length 0) if no more directory entries remain.
* @throws IOException On any error when trying to read the directory.
*/
public Dir[] dirreadall() throws IOException {
ByteBuffer b;
ArrayList
* @return A Dir instance that describes the current file.
* @throws IOException On any error.
*/
public Dir stat() throws IOException {
Dir d = qstat();
if(d == null)
ioerror();
return d;
}
// quiet version, for internal use: doesn't raise exception
private Dir qstat() {
try{
Ninep.Rstat r = (Ninep.Rstat)conn.ninepreq(ninep.new Tstat(fid));
if(r == null)
return null;
return r.stat;
}catch(InterruptedIOException e){
return null;
}catch(ConnectionFailed e){
return null;
}
}
/**
* Attempt to update the directory entry for the current file, returning true on success.
*
* The Dir parameter has a special form: values that should not change will either be the
* file's original values, or special "don't care" values, described in stat(5), typically null or
* empty strings for string values, and ~0 for integer values. Any other values will change the file's
* directory entry to match. If the request is rejected, wstat returns false and updates the error string.
* @param d A Dir instance containing the values to update
* @return true iff the file server accepted the update
* @throws InterruptedIOException operation was interrupted (eg, by an alarm)
* @throws ConnectionFailed 9P connection was shut down, by hangup or IO error
*/
public boolean wstat(Dir d) throws ConnectionFailed, InterruptedIOException {
Ninep.Rwstat r = (Ninep.Rwstat)conn.ninepreq(ninep.new Twstat(fid, d));
return r != null;
}
/**
* Change the current file position (offset)
*
* Interpret off as instructed by whence and change the file's current position to that value.
* @param off Either an absolute ({@link NinepClient#SEEKSTART}) or relative ({@link NinepClient#SEEKRELA}, {@link NinepClient#SEEKEND}) file offset in bytes.
* @param whence {@link NinepClient#SEEKSTART} (absolute offset), {@link NinepClient#SEEKRELA} (signed offset from the current position), or {@link NinepClient#SEEKEND} (signed offset from the length of the file)
* @return The resulting offset, or -1 if the attempt failed.
*/
public long seek(long off, int whence){
switch(whence){
case SEEKSTART:
if((qid.qtype & Qid.QTDIR) != 0 && off != 0){
werrstr(Eisdir);
return -1;
}
synchronized(this){ offset = off; }
break;
case SEEKRELA:
if((qid.qtype & Qid.QTDIR) != 0){
werrstr(Eisdir);
return -1;
}
synchronized(this){
off += offset;
if(off < 0){
werrstr(Enegoff);
return -1;
}
offset = off;
}
break;
case SEEKEND:
if((qid.qtype & Qid.QTDIR) != 0){
werrstr(Eisdir);
return -1;
}
Dir d = qstat();
if(d == null){
werrstr("internal error: stat error in seek: "+errstr());
return -1;
}
off += d.length;
if(off < 0){
werrstr(Enegoff);
return -1;
}
synchronized(this){ offset = off; }
break;
default:
werrstr("invalid argument");
return -1;
}
return off; // not offset, that might already have changed
}
/** Return the path name by which this file was opened */
public String path(){
return this.name;
}
/** Return the suggested buffer size in bytes for reads and writes, or 0 if unspecified */
public int iounit(){
return this.iounit;
}
// extensions for Java
/**
* Return the current file position (offset in bytes)
* @return A 64-bit integer giving the current file offset in bytes.
*/
public long position(){
synchronized(this){
return this.offset;
}
}
/**
* Set the current file position to the given offset in bytes.
*
* @param offset The desired offset
* @throws IOException If the offset is negative or the offset is non-zero for a directory, and on any other error.
*/
public void setPosition(long offset) throws IOException {
if(offset < 0)
ioerror(Enegoff);
if(offset != 0 && (this.qid.qtype & Qid.QTDIR) != 0)
ioerror(Eisdir);
synchronized(this){
this.offset = offset;
}
}
/**
* Return true if the file is a directory.
*/
public boolean isDirectory(){
return (this.qid.qtype & Qid.QTDIR) != 0;
}
/**
* Return true if the file is marked "exclusive-use" (DMEXCL)
*/
public boolean isExclusive(){
return (this.qid.qtype & Qid.QTEXCL) != 0;
}
/**
* Return true if the file is marked "append-only" (DMAPPEND)
*/
public boolean isAppendOnly(){
return (this.qid.qtype & Qid.QTAPPEND) != 0;
}
protected boolean checkfd(int m){
if(mode < 0)
return false; // not opened
if(mode != Ninep.ORDWR){
if((m & Ninep.OTRUNC) != 0 && mode == Ninep.OREAD ||
(m & ~Ninep.OTRUNC) != mode){
return false;
}
}
return true;
}
}
static final protected int openmode(int m){
m &= 3;
if(m == Ninep.OEXEC)
return Ninep.OREAD;
return m;
}
protected static final boolean dirunpack(ByteBuffer buf, ArrayList
* If already closed, do nothing.
*/
public void close(){
if(fidp != null && fidp.decref() == 0){
String err = lasterror.get();
try{
conn.ninepreq(ninep.new Tclunk(fid));
}catch(InterruptedIOException e){
// still considered clunked
}catch(ConnectionFailed e){
// let it go: file will be clunked
}
lasterror.set(err);
/* defined to succeed */
if(conn != null)
conn.freefid(fidp);
fidp = null;
}
}
private void open(int mode, int iounit, Qid qid){
this.mode = mode;
this.iounit = iounit;
this.qid = qid;
}
private void free(){
if(fidp != null && fidp.decref() == 0){
if(conn != null)
conn.freefid(fidp);
fidp = null;
}
conn = null;
fid = Ninep.NOFID;
}
final void checkio(int mode, int n, long off) throws IOException, ClosedChannelException {
if(fidp == null)
throw new ClosedChannelException();
if(!checkfd(mode))
ioerror(Ebadusefd);
if(n < 0)
ioerror(Etoosmall);
if(off < 0)
ioerror(Enegoff);
}
/**
* Read a sequence of bytes from the file at its current position, into the buffer dest, up to the number of bytes remaining in the buffer.
*
* The file offset is updated to reflect the number of bytes read.
* @param dest Destination buffer for the bytes read.
* @param n maximum number of bytes to read
* @return number of bytes read, or 0 at end-of-file
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int read(ByteBuffer dest, int n) throws IOException {
long off;
synchronized(this){ off = offset; }
n = read(dest, n, off);
if(n < 0)
ioerror();
synchronized(this){ offset += n; }
return n;
}
/**
* Return a ByteBuffer containing up to n bytes of data read from the given offset.
*
* The file's current offset is unchanged.
* The buffer is empty at end-of-file.
* @param n maximum number of bytes to read
* @param offset byte offset from which to read
* @return ByteBuffer containing the data read, or null at end-of-file.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public ByteBuffer read(int n, long offset) throws IOException {
checkio(Ninep.OREAD, n, offset);
ByteBuffer result = null;
for(;;){
int nreq = n;
if(nreq > conn.msize-IOHDRSZ)
nreq = conn.msize-IOHDRSZ;
Ninep.Rread r = (Ninep.Rread)conn.ninepreq(ninep.new Tread(fid, offset, nreq));
if(r == null)
ioerror();
int nr = r.data.remaining();
if(nr > nreq){ // guard against broken servers
r.data.limit(r.data.position()+nreq);
nr = nreq;
}
offset += nr;
n -= nr;
if(nr != nreq || n == 0){
// return buffer without copying if only one read was required
if(result == null || result.position() == 0){
if(nr == 0)
return null;
return r.data;
}
result.put(r.data);
result.flip();
return result;
}
// reading requested n bytes in msize chunks: need a buffer for the whole result
if(result == null)
result = ByteBuffer.allocate(n+nr);
result.put(r.data);
}
}
/**
* Read up to n bytes from the file starting at its current offset and return a ByteBuffer containing the bytes read (null at end of file).
* @param n maximum number of bytes to read
* @return ByteBuffer containing the data read, or null at end-of-file.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public ByteBuffer read(int n) throws IOException {
long off;
synchronized(this){ off = offset; }
ByteBuffer rb = read(n, off);
if(rb == null)
return null;
n = rb.remaining();
synchronized(this){ offset += n; }
return rb;
}
/**
* Read up to n bytes of UTF-8 starting at the current file offset and return the bytes converted to a String, returning null at end-of-file.
* @param n maximum number of bytes to read
* @return String containing the data read, or null at end-of-file.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public String reads(int n) throws IOException {
ByteBuffer b = read(n);
if(b == null)
return null;
return Strings.S(b);
}
/**
* Read up to n bytes of UTF-8 starting at the current file offset and return the bytes converted to a String, returning null at end-of-file.
* @param n maximum number of bytes to read
* @return String containing the data read, or null at end-of-file.
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public String reads(int n, long offset) throws IOException {
ByteBuffer b = read(n, offset);
if(b == null)
return null;
return Strings.S(b);
}
/**
* Write a sequence of bytes from the buffer src to the file starting at its current position.
*
* The file offset is unchanged.
* @param src Source buffer for the bytes to write.
* @param n maximum number of bytes to write.
* @param offset starting byte offset in file
* @return number of bytes written
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int write(ByteBuffer src, int n, long offset) throws IOException {
checkio(Ninep.OWRITE, n, offset);
if((qid.qtype & Qid.QTDIR) != 0)
ioerror(Eisdir); // probably can't happen: can't open directory OWRITE
int count = 0;
for(;;){
int nreq = n;
if(nreq > conn.msize-IOHDRSZ)
nreq = conn.msize-IOHDRSZ;
Ninep.Rwrite r = (Ninep.Rwrite)conn.ninepreq(ninep.new Twrite(fid, offset, src, nreq));
if(r == null)
ioerror();
int nr = r.count;
if(nr > nreq)
nr = nreq;
offset += nr;
n -= nr;
count += nr;
src.position(src.position()+nr);
if(nr != nreq || n == 0)
break;
}
return count;
}
/**
* Write up to n bytes from the buffer src to the file starting at the current file offset.
*
* The file offset is updated to reflect the number of bytes actually written.
* @param src Source buffer for the bytes to write.
* @param n maximum number of bytes to write.
* @return number of bytes written
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int write(ByteBuffer src, int n) throws IOException {
long off;
checkio(Ninep.OWRITE, n, 0);
synchronized(this){ off = offset; offset += n; } // assume it all goes
int r = 0;
try{
r = write(src, n, off);
}catch(IOException e){
synchronized(this){ offset -= n; }
throw e;
}
if(r < n)
synchronized(this){ offset -= n-r; }
return r;
}
/**
* Write all the bytes from the byte array buf to the file starting at the current file offset.
*
* The file offset is updated to reflect the number of bytes actually written.
* @param buf Source buffer for the bytes to write.
* @return number of bytes written
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int write(byte[] buf) throws IOException {
return write(ByteBuffer.wrap(buf));
}
/**
* Write all the bytes from the byte array buf to the file starting at the given file offset.
*
* The file offset is unchanged.
* @param buf Source buffer for the bytes to write.
* @param offset byte offset at which to start writing
* @return number of bytes written
* @throws IOException In case of any IO error: the exception's message gives the cause.
*/
public int write(byte[] buf, long offset) throws IOException {
return write(ByteBuffer.wrap(buf), buf.length, offset);
}
/**
* Write the bytes of the UTF-8 encoding of String s to the file.
*