* Execute a given literal command with parameters (no shell processing occurs).
*/
- public static Return exec (final String resource, final Map<String,List<String>> params, final ShellSession.Instance session, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException
+ public static Return exec (final String resource, final Map<String,List<String>> params, final TaskData task) throws IOException, ServletException
{
Log.debug ("shell exec: "+resource);
- Return r = Call.valueOf (resource, params).exec (session, context, user, in, contentType);
+ Return r = Call.valueOf (resource, params).exec (task);
Log.debug ("shell exec returned: "+r);
return r;
}
/**
* @deprecated
*/
public static ShellSession.Instance getSessionFromPost (final HttpServletRequest req) throws IOException
* A formalized group (i.e. list of operations surrounded by curly braces, invoked sequentially in a sub-shell).
*
* @author shawn@lannocc.com
*/
class Block implements Graph, Task
{
Group ops;
public boolean assemble (final GraphReader in) throws GraphException, IOException
{
if ('{' != in.peek ()) return false;
// FIXME: we really need a double-peek so we can require whitespace or newline after opening brace
in.discard ();
Log.debug ("will assemble");
ops = new Group ();
if (!ops.assemble (in)) ops = null;
if ('}' != in.peek ()) throw new GraphException ("Missing closing curly brace");
in.discard ();
return true;
}
- public Return exec (final ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData task) throws IOException, ServletException
{
- return ops.exec (shell, context, user, in, contentType);
* Invoke a named executable with optional parameters/arguments.
*
* @author shawn@lannocc.com
*/
public class Call extends Command implements Graph, Task
{
List<CommandText> data;
List<Parameter> params;
// normalized
String cmd;
List<String> args;
Call ()
{
/*
this.args = new ArrayList<CommandText> ();
*/
this.data = new ArrayList<CommandText> ();
this.params = new ArrayList<Parameter> ();
this.cmd = null;
this.args = null;
}
public boolean assemble (final GraphReader in) throws IOException, GraphException
{
// re-usable objects for testing
CommandText text = new CommandText ();
Whitespace white = new Whitespace ();
Parameter param = new Parameter ();
if (! text.assemble (in)) return false;
Log.debug ("will assemble");
// System.out.println ("call name: "+this.name);
this.data.add (text);
text = new CommandText ();
for (int c; (c = in.peek ())>=0; )
{
if (white.assemble (in))
{
//white = new Whitespace ();
}
if (param.assemble (in))
{
this.params.add (param);
param = new Parameter ();
}
else if (text.assemble (in))
{
this.data.add (text);
text = new CommandText ();
}
else
{
return true;
}
}
return true;
}
/*
* FIXME: this should be improved (allow named parameters, too?)
*/
void normalize (ShellSession.Instance shell, final ServletContext context)
{
StringBuffer cmd = null;
final List<String> args = new ArrayList<String> ();
boolean inArgs = false;
StringBuffer arg = null;
for (final CommandText segment : data)
{
for (final Text text : segment.items)
{
final String str = text.value (shell, context);
if (str==null)
{
continue;
}
if (text instanceof Variable)
{
char c;
for (int i=0; i<str.length (); i++)
{
c = str.charAt (i);
if (' '==c || '\t'==c)
{
if (cmd==null)
{
continue;
}
inArgs = true;
if (arg!=null)
{
args.add (arg.toString ());
arg = null;
}
}
else
{
if (! inArgs)
{
if (cmd==null)
{
cmd = new StringBuffer ();
}
cmd.append (c);
}
else
{
if (arg==null)
{
arg = new StringBuffer ();
}
arg.append (c);
}
}
}
}
else
{
if (! inArgs)
{
if (cmd==null)
{
cmd = new StringBuffer ();
}
cmd.append (str);
}
else
{
if (arg==null)
{
arg = new StringBuffer ();
}
arg.append (str);
}
}
}
if (! inArgs)
{
inArgs = true;
}
else if (arg!=null)
{
args.add (arg.toString ());
arg = null;
}
}
if (arg!=null)
{
args.add (arg.toString ());
arg = null;
}
this.cmd = cmd.toString ();
this.args = args;
}
- public Return exec (ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData task) throws IOException, ServletException
Log.error ("executable resource not found (even after searching through $PATH, if relative): "+cmd);
return Utils.error ("executable resource not found (even after searching through $PATH, if relative): "+cmd);
}
}
static Return invoke (final ShellSession.Instance shell, final Transport t, final ServletContext context, final InputStream in, final String contentType, final Map<String,List<String>> params) throws IOException
* One or more comments (ignored), blocks and/or operations, invoked sequentially (an informal list), sometimes conditionally.
* Will not assemble an empty group (however a group may simply be whitespace or a comment). [IS THIS STILL TRUE???--SAW]
*
* Presently all output is combined.
*
* @author shawn@lannocc.com
*/
public class Group implements Graph, Task
{
List tasks; // list of Task items as well as possible conditionials (Or, And)
public Group ()
{
tasks = new ArrayList (20);
}
public boolean assemble (final GraphReader in) throws IOException, GraphException
{
// re-usable objects for testing
Whitespace white = new Whitespace ();
Comment blah = new Comment ();
Block block = new Block ();
Operation op = new Operation ();
Or or = new Or ();
And and = new And ();
if (in.peek ()<0) return false;
Log.debug ("will assemble");
while (true)
{
if (white.assemble (in))
{
//white = new Whitespace ();
}
else if (blah.assemble (in))
{
//blah = new Comment ();
}
else if (block.assemble (in))
{
tasks.add (block);
block = new Block ();
}
else if (';'==in.peek () || '\n'==in.peek ())
{
in.discard ();
}
else if (op.assemble (in))
{
tasks.add (op);
op = new Operation ();
}
else if (or.assemble (in))
{
if (tasks.isEmpty () || ! (tasks.get (tasks.size ()-1) instanceof Task))
{
throw new GraphException ("expecting a Task to precede 'Or' condition: "+or);
}
tasks.add (or);
or = new Or ();
}
else if (and.assemble (in))
{
if (tasks.isEmpty () || ! (tasks.get (tasks.size ()-1) instanceof Task))
{
throw new GraphException ("expecting a Task to precede 'And' condition: "+and);
}
tasks.add (and);
and = new And ();
}
else if (in.peek ()<0)
{
if (! tasks.isEmpty ())
{
final Object last = tasks.get (tasks.size ()-1);
if (! (last instanceof Task))
{
throw new GraphException ("expecting a Task to follow the last condition: "+last);
}
}
// FIXME: don't we actually want: return tasks.size()>1; ???
return true;
}
else if ('}'==in.peek ())
{
Log.debug ("found closing curly brace and returning");
// FIXME: don't we actually want: return tasks.size()>1; ???
return true;
}
else
{
throw new GraphException ("unexpected (Group): "+((char) in.peek ()));
}
}
}
- public Return exec (ShellSession.Instance shell, final ServletContext context, final String user, InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData tdata) throws IOException, ServletException
{
Log.debug ("exec: "+this);
final ByteArrayOutputStream out = new ByteArrayOutputStream ();
Return r = null;
Task task = null;
+ ShellSession.Instance shell = tdata.shell;
+ InputStream in = tdata.in;
+
boolean skip = false;
for (final Object taskOrCondition : tasks)
{
if (skip) // ignore current item
{
skip = false;
}
else if (taskOrCondition instanceof Task)
{
task = (Task) taskOrCondition;
final Return r_old = r;
Log.debug ("task exec: "+task);
- r = task.exec (shell, context, user, in, contentType);
+ r = task.exec (new TaskData (shell, tdata.context, tdata.user, in, tdata.contentType));
Log.debug ("task exec returned: "+r);
if (r==null) r = r_old; // may be null if operation was simply variable assignment
throw new RuntimeException ("unsupported task or condition type: "+taskOrCondition);
}
}
if (r!=null)
{
return new Return (r.type, r.disposition, new ByteArrayInputStream (out.toByteArray()), r.status);
}
else // r may be null for a single operation that is simply a variable assignment
{
return null;
}
}
public String toString ()
{
final StringBuffer s = new StringBuffer ();
String sep = "";
for (final Object taskOrCondition : tasks)
{
s.append (sep).append (taskOrCondition);
sep = "\n";
}
return s.toString ();
}
public static void main (final String [] args) throws Exception
{
// final GraphReader in = new GraphReader (new StringReader ("\n\t\t \n ab;{'c' foo; } bar$blah \"ok then\"$something and${more}stuff and\"not\"'$more' ; #ok\n x y z"));
final GraphReader in = new GraphReader (new StringReader (
public boolean assemble (final GraphReader in) throws IOException, GraphException
{
if ('`'!=in.peek ()) return false;
in.discard ();
// re-usable objects for testing
Escaped escaped = new Escaped ();
Variable variable = new Variable ();
Literal literal = new Literal ();
for (int c; (c = in.peek ()) >= 0; )
{
if ('`'==c)
{
in.discard ();
return true;
}
if (escaped.assemble (in))
{
items.add (escaped);
escaped = new Escaped ();
}
else if (variable.assemble (in))
{
items.add (variable);
variable = new Variable ();
}
else if (literal.assemble (in))
{
items.add (literal);
literal = new Literal ();
}
else
{
throw new GraphException ();
}
}
throw new GraphException ("Missing closing back-quote: "+string ());
}
public String toString ()
{
return '`'+string ()+'`';
}
String string ()
{
final StringBuffer s = new StringBuffer ();
for (final Graph item : items)
{
s.append (item.toString ());
}
return s.toString ();
}
public String value (final ShellSession.Instance shell, final ServletContext context)
{
try
{
/* FIXME (user) */
- final Return r = exec (shell, context, null, null, null);
+ final Return r = exec (new TaskData (shell, context, null));
if (r==null)
{
Log.error ("null return for "+this);
return null;
}
if (r.type==null || !r.type.startsWith ("text/"))
{
Log.warn ("non-text mime type returned for "+this+": "+r.type);
}
if (r.data==null)
{
Log.warn ("null data stream returned for "+this);
return null;
}
final StringBuffer s = new StringBuffer ();
final InputStreamReader in = new InputStreamReader (r.data);
for (int c; (c = in.read ())>=0; s.append ((char) c));
return s.toString ();
}
catch (final IOException e)
{
Log.error ("shell exec failed", e);
return "shell exec failed: "+e;
}
catch (final ServletException e)
{
Log.error ("shell exec failed", e);
return "shell exec failed: "+e;
}
}
- public Return exec (ShellSession.Instance shell, final ServletContext context, final String user, InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData task) throws IOException, ServletException
* A set of commands invoked together as a single unit.
*
* @author shawn@lannocc.com
*/
class Operation implements Graph, Task
{
List<Command> cmds;
public Operation ()
{
cmds = new ArrayList<Command> (10);
}
/**
* Note that this may gobble up whitespace yet fail assembly.
*/
public boolean assemble (final GraphReader in) throws GraphException, IOException
{
Log.debug ("attempting assembly");
// re-usable objects for testing
VariableAssign var = new VariableAssign ();
Call call = new Call ();
Input input = new Input ();
Output output = new Output ();
Chain chain = new Chain ();
Background back = new Background ();
Whitespace white = new Whitespace ();
boolean setVar = false; // have any variable assignments?
Object lastCallOrChain = null;
if (in.peek ()<0) return false;
// if there are any variable assignments they have to come first
while (var.assemble (in))
{
setVar = true;
cmds.add (var);
var = new VariableAssign ();
if (white.assemble (in))
{
//white = new Whitespace ();
}
}
while (true)
{
if (call.assemble (in))
{
if (lastCallOrChain!=null && lastCallOrChain instanceof Call)
{
throw new GraphException ("not expecting another call here: "+call);
}
cmds.add (call);
lastCallOrChain = call;
call = new Call ();
}
else if (input.assemble (in))
{
cmds.add (input);
input = new Input ();
}
else if (output.assemble (in))
{
cmds.add (output);
output = new Output ();
}
else if (chain.assemble (in))
{
Log.debug ("lastCallOrChain: "+lastCallOrChain);
if (lastCallOrChain==null || ! (lastCallOrChain instanceof Call))
{
throw new GraphException ("expecting a call to come somewhere before chain command: "+chain);
}
cmds.add (chain);
lastCallOrChain = chain;
chain = new Chain ();
}
else if (back.assemble (in))
{
if (lastCallOrChain==null)
{
throw new GraphException ("expecting a call or chain to come before background command: "+back);
}
cmds.add (back);
lastCallOrChain = back;
back = new Background ();
}
else if (white.assemble (in))
{
//white = new Whitespace ();
}
else if (cmds.size ()<1)
{
return false;
}
else if (lastCallOrChain==null)
{
if (setVar) return true;
throw new GraphException ("operation expects a call (or variable assignment, at least) to come somewhere: "+(char) in.peek ());
}
else if (! (lastCallOrChain instanceof Call || lastCallOrChain instanceof Background))
{
throw new GraphException ("operation needs to end with a call");
}
else
{
return true;
}
}
}
/**
* Note this returns <tt>null</tt> if the operation is merely a variable assignment.
*/
- public Return exec (ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData task) throws IOException, ServletException
* An optional named interpreter followed by group of operations.
* Empty input will not assemble.
*
* @author shawn@lannocc.com
*/
public class Script implements Graph, Task
{
HashBang interpreter;
Group ops;
public boolean assemble (final GraphReader in) throws IOException, GraphException
{
interpreter = new HashBang ();
if (! interpreter.assemble (in))
{
interpreter = null;
}
Log.debug ("hashbang: "+interpreter);
if (interpreter != null) Log.info ("interpreter: "+interpreter.getVal());
ops = new Group ();
if (! ops.assemble (in))
{
Log.debug ("no group assembly");
return false;
}
return true;
}
/*
public HashBang getInterpreter ()
{
return interpreter;
}
*/
/*
* this is shell exec only: the calling party is responsible for invoking any other interpreter
*/
- public Return exec (final ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException
+ public Return exec (final TaskData task) throws IOException, ServletException
{
- return ops.exec (shell, context, user, in, contentType);
//Status run (final Reader in, final Writer out, final Writer err) throws IOException;
- Return exec (ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in, final String contentType) throws IOException, ServletException;
* Support for jobs (currently that means backgrounded tasks).
*
* @author shawn@lannocc.com
*/
public class Job implements Runnable
{
Thread thread;
final Call call;
- final ShellSession.Instance shell;
- final ServletContext context;
- final String user;
- final InputStream in;
+ final TaskData task;
Return r;
ByteArrayOutputStream out;
- Job (final Call call, final ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in)
+ Job (final Call call, final TaskData task)
{
this.call = call;
- this.shell = shell;
- this.context = context;
- this.user = user;
- this.in = in;
+ this.task = task;
this.out = new ByteArrayOutputStream ();
}
public void run ()
{
try
{
- // FIXME: contentType not yet utilized here
- r = call.exec (shell, context, user, in, "");
+ r = call.exec (task);
Utils.pipe (r.data, out);
}
catch (final Exception e)
{
try
{
out.write (("job: while buffering background output: "+e).getBytes ());
}
catch (final IOException ee)
{
Log.error ("ack! while handling: "+e, ee);
throw new RuntimeException (ee);
}
}
}
public Return join (final ServletContext context) throws InterruptedException
{
thread.join();
thread = null;
final Map<Integer,Job> jobs = getJobs (context);
jobs.remove (getId ());
// FIXME: session may have changed after exec
final ByteArrayInputStream in = new ByteArrayInputStream (out.toByteArray ());
return new Return (in, this.r.status);
}
public String toString ()
{
// FIXME: track a local (easier) job number for the brackets
return "["+getId ()+"] "+getId ();
}
int getId ()
{
return System.identityHashCode (this);
}
- public static Job background (final Call call, final ShellSession.Instance shell, final ServletContext context, final String user, final InputStream in)
+ public static Job background (final Call call, final TaskData task)
- HertzJob (final int secs, final String cmd, final List<String> args, final ShellSession.Instance shell, final ServletContext context, final String user)
+ HertzJob (final int secs, final String cmd, final List<String> args, final TaskData task)
{
- super (cmd, args, shell, context, user);
+ super (cmd, args, task);
this.secs = secs;
}
void idle () throws InterruptedException
{
Thread.sleep (secs*1000);
}
public String toString ()
{
return "[every "+secs+" secs] "+call;
}
}
static class DailyJob extends Job
{
final Calendar next;
- DailyJob (final String cmd, final List<String> args, final ShellSession.Instance shell, final ServletContext context, final String user, final Calendar time)
+ DailyJob (final String cmd, final List<String> args, final TaskData task, final Calendar time)