/*
* This file is a part of Alchemy OS project.
* Copyright (C) 2011-2013, Sergey Basalaev <sbasalaev@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package alchemy.system;
import alchemy.evm.EtherLoader;
import alchemy.fs.Filesystem;
import alchemy.io.NullInputStream;
import alchemy.io.NullOutputStream;
import alchemy.io.UTFReader;
import alchemy.types.Int32;
import alchemy.util.ArrayList;
import alchemy.util.Arrays;
import alchemy.util.HashMap;
import alchemy.util.Strings;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import javax.microedition.io.Connection;
/**
* Single process in Alchemy OS.
* Process represents an instance of a program
* executing in Alchemy environment.
*
* <h3>Process lifecycle</h3>
* Process lifecycle consists of three steps:
* <ol>
* <li>
* <code>NEW</code><br/>
* This is the state of new process. In this state process
* environment can be modified using methods
* <ul>
* <li><code>setEnv</code>
* <li><code>setCurrentDirectory</code>
* <li><code>setPriority</code>
* </ul>
* <li>
* <code>RUNNING</code><br/>
* This is the state of process which executes program in separate
* thread. Process becomes <code>RUNNING</code> after executing
* {@link #start()} method. If the program fails to be loaded then
* exception is thrown and process remains in <code>NEW</code> state.
* <li>
* <code>ENDED</code><br/>
* The <code>RUNNING</code> process becomes <code>ENDED</code>
* when the main thread dies. In this state program exit code
* can be examined with {@link #getExitCode()} method and
* if program has ended by throwing an exception that exception
* can be obtained with {@link #getError()} method.
* </ol>
*
* <h3><a name="LibraryLoading"></a>Loading of programs and libraries</h3>
* Methods that load libraries are <code>start</code>
* and <code>loadLibrary</code>.
* Loading of library consists of the following steps:
* <ol>
* <li>
* Library file is resolved from given string argument and
* searched in a set of paths. If file can not be found then
* <code>IOException</code> is thrown.
* <ul>
* <li>
* If argument starts with <code>'/'</code> character, then it is regarded
* as absolute path.
* <li>
* If argument contains </code>'/'</code> but not starts with it then it
* is regarded as path relative to the {@link #getCurrentDirectory() current directory}.
* <li>
* If argument does not contain any slashes then search is performed in a
* set of paths defined in an environment variables. Method <code>start</code>
* reads paths from <code>PATH</code> variable and <code>readLibrary</code>
* method reads them from <code>LIBPATH</code>.
* These variables should contain colon-separated list of paths.
* </ul>
* <li>
* If library is already stored in {@link Cache} then it is returned.
* Caching is based on file name and timestamp. So the cached version is
* used if file was not changed since the last caching.
* <li>
* If cache has no library or has older version of library then file is
* used to construct new library instance.
* </ol>
*
* @author Sergey Basalaev
*/
public final class Process {
/** Magic number for Ether libraries. */
private static final int MAGIC_ETHER = 0xC0DE;
/** Magic number for native libraries. */
private static final int MAGIC_NATIVE = ('#' << 8) | '@';
/** Magic number for interpreter scripts. */
private static final int MAGIC_INTERPRETER = ('#' << 8) | '!';
/** Magic number for symbolic links. */
private static final int MAGIC_LINK = ('#' << 8) | '=';
/** Returned by getState() for new process. */
public static final int NEW = 0;
/** Returned by getState() for alive running process. */
public static final int RUNNING = 1;
/** Returned by getState() for ended process. */
public static final int ENDED = 5;
/** Standard input stream. */
public InputStream stdin;
/** Standard output stream. */
public OutputStream stdout;
/** Standard error stream. */
public OutputStream stderr;
/**
* Killed status of this process.
* Read-only, please.
*/
public volatile boolean killed;
/** Parent of this process. */
private final Process parent;
/** Command that invoked this process. */
private final String command;
/** Command-line arguments passed to this process. */
private final String[] cmdArgs;
/**
* Environment variables.
* <pre>String -> String</pre>.
*/
private HashMap env;
/**
* Global variables.
* <pre>Library -> String -> Object</pre>
*/
private HashMap globals;
/** Listeners of this process. */
private final ArrayList listeners = new ArrayList();
/** Connections owned by this process. */
private final ArrayList connections = new ArrayList();
/** Current directory. */
private String curdir;
/** Main thread of this process. */
private ProcessThread mainThread;
/** Additional threads of this process. */
private final ArrayList threads = new ArrayList();
/** Priority of this process. */
private int priority;
/**
* Creates new parentless process.
*/
public Process(String command, String[] args) {
this.parent = null;
this.curdir = "";
this.stdin = new NullInputStream(-1);
this.stdout = this.stderr = new NullOutputStream();
this.command = command;
this.cmdArgs = args;
}
/** Creates new process that inherits environment from its parent. */
public Process(Process parent, String command, String[] args) {
this.parent = parent;
this.curdir = parent.curdir;
this.stdin = parent.stdin;
this.stdout = parent.stdout;
this.stderr = parent.stderr;
this.command = command;
this.cmdArgs = args;
}
public static Process currentProcess() {
Thread thread = Thread.currentThread();
if (thread instanceof ProcessThread) {
return ((ProcessThread)thread).getProcess();
} else {
return null;
}
}
/** Returns program name. */
public String getName() {
return Filesystem.fileName(command);
}
/** Returns command-line arguments. */
public String[] getArgs() {
String[] newargs = new String[cmdArgs.length];
System.arraycopy(cmdArgs, 0, newargs, 0, newargs.length);
return newargs;
}
/** Returns string representation of this process. */
public String toString() {
return "Process(" + getName() + ')';
}
/**
* Returns exit code of this process.
* Can be called only in ENDED state.
*/
public int getExitCode() {
if (mainThread == null || mainThread.isAlive()) throw new IllegalStateException();
return mainThread.getExitCode();
}
/**
* Returns exception that caused this process to crash.
* Returns null if process ended normally.
* Can be called only in ENDED state.
*/
public AlchemyException getError() {
if (mainThread == null || mainThread.isAlive()) throw new IllegalStateException();
return mainThread.getError();
}
/**
* Starts execution of this process.
* Can be called only in NEW state.
* If program is successfully instantiated and started,
* turns into RUNNING state.
*/
public Process start() throws IOException, InstantiationException {
synchronized (threads) {
if (mainThread != null) throw new IllegalStateException();
Library program = loadBinary(command, getEnv("PATH"));
Function main = program.getFunction("main");
if (main == null)
throw new InstantiationException("No main function in " + command);
mainThread = new ProcessThread(this, main, new Object[] {cmdArgs});
mainThread.start();
}
return this;
}
/**
* Waits for this process to end and returns its exit code.
* Can be called only in RUNNING or ENDED state.
*/
public int waitFor() throws InterruptedException {
if (mainThread == null) throw new IllegalStateException();
mainThread.join();
return mainThread.getExitCode();
}
/** Searches program or library in given path list and loads it. */
private Library loadBinary(String libname, String pathlist) throws IOException, InstantiationException {
// resolve file name and check permissions
String libfile = resolveFile(libname, pathlist);
if (libfile == null)
throw new IOException("File not found: " + libname);
if (!Filesystem.canExec(libfile))
throw new SecurityException("Permission denied: " + libfile);
// search library in cache
long tstamp = Filesystem.lastModified(libfile);
Object cachedlib = Cache.get(libfile, tstamp);
if (cachedlib != null) {
if (cachedlib instanceof Library)
return (Library) cachedlib;
else
throw new ClassCastException("Unknown library format: " + libfile);
}
// read library from file
InputStream libin = Filesystem.read(libfile);
Library lib;
try {
int magic = (libin.read() << 8) | libin.read();
switch (magic) {
case MAGIC_NATIVE: {
String classname = new UTFReader(libin).readLine();
try {
lib = (Library)Class.forName(classname).newInstance();
break;
} catch (ClassNotFoundException cnfe) {
throw new InstantiationException("Not supported in this build of Alchemy OS: " + classname);
} catch (NoClassDefFoundError cndfe) {
throw new InstantiationException("Not supported by this device: " + classname);
} catch (Throwable t) {
throw new InstantiationException("Not a library class: " + classname);
}
}
case MAGIC_LINK: {
String filename = new UTFReader(libin).readLine();
libin.close();
if (filename.charAt(0) != '/') {
filename = Filesystem.fileParent(libfile) + '/' + filename;
}
lib = loadBinary(filename, pathlist);
break;
}
case MAGIC_INTERPRETER: {
String intcmd = new UTFReader(libin).readLine();
String[] intargs = Strings.split(intcmd, ' ', true);
intcmd = intargs[0];
System.arraycopy(intargs, 1, intargs, 0, intargs.length-1);
intargs[intargs.length-1] = libfile;
lib = new Library();
lib.putFunction(new InterpreterMain(lib, intcmd, intargs));
break;
}
case MAGIC_ETHER:
lib = EtherLoader.load(this, libin);
break;
default:
throw new InstantiationException("Unknown library format: " + libfile);
}
} finally {
try { libin.close(); } catch (IOException ioe) { }
}
// assign name to the library and put it into the cache
if (lib != null) {
if (lib.name == null) lib.name = Filesystem.fileName(libfile);
Cache.put(libfile, tstamp, lib);
}
return lib;
}
/**
* Loads library with given name.
* If library name contains slashes then it is regarded as
* absolute or relative path. Otherwise it is searched in
* paths specified by LIBPATH environment variable.
*/
public Library loadLibrary(String name) throws IOException, InstantiationException {
return loadBinary(name, getEnv("LIBPATH"));
}
/**
* Searches file in given list of paths.
* If file does not exist then null is returned.
*
* @param name file name
* @param pathlist colon separated list of paths
*/
public String resolveFile(String name, String pathlist) {
if (name.length() == 0)
throw new IllegalArgumentException();
if (name.indexOf('/') >= 0) {
name = toFile(name);
if (Filesystem.exists(name)) return name;
} else {
String[] paths = Strings.split(pathlist, ':', true);
for (int i=0; i<paths.length; i++) {
String path = paths[i];
if (path.length() == 0) continue;
String testname = toFile(path + '/' + name);
if (Filesystem.exists(testname)) return testname;
}
}
return null;
}
/** Attaches process listener to this process. */
public void addProcessListener(ProcessListener l) {
synchronized (listeners) {
if (!listeners.contains(l)) listeners.add(l);
}
}
/** Detaches process listener from this process. */
public void removeProcessListener(ProcessListener l) {
synchronized (listeners) {
listeners.remove(l);
}
}
/** Returns state of this process. */
public int getState() {
if (mainThread == null) return NEW;
if (mainThread.isAlive()) return RUNNING;
return ENDED;
}
/**
* Returns value of the environment variable.
* If variable is not defined then null is returned.
*/
public String getEnv(String key) {
if (env != null) {
String val = (String)env.get(key);
if (val != null) return val;
}
if (parent != null) {
return parent.getEnv(key);
}
return null;
}
/**
* Sets new value to the environment variable.
* If value is null, resets variable.
*/
public void setEnv(String key, String value) {
if (value == null) {
// remove variable
if (env != null) env.remove(key);
} else {
// set variable
if (env == null) env = new HashMap();
env.set(key, value);
}
}
/**
* Returns value of the global variable.
* If variable is not set, return default value.
*/
public Object getGlobal(Library lib, String name, Object dflt) {
if (globals == null) return dflt;
HashMap libKeys = (HashMap)globals.get(lib == null ? Process.class : (Object)lib);
if (libKeys == null) return dflt;
Object ret = libKeys.get(name);
return (ret != null) ? ret : dflt;
}
/**
* Sets value to the global variable.
*/
public void setGlobal(Library lib, String name, Object value) {
if (globals == null) globals = new HashMap();
HashMap vars = (HashMap)globals.get(lib == null ? Process.class : (Object)lib);
if (vars == null) {
vars = new HashMap();
globals.set(lib == null ? Process.class : (Object)lib, vars);
}
vars.set(name, value);
}
/** Returns current priority of the process. */
public int getPriority() {
return priority;
}
/** Sets new priority to the process. */
public void setPriority(int newPriority) {
if (newPriority < Thread.MIN_PRIORITY || newPriority > Thread.MAX_PRIORITY)
throw new IllegalArgumentException();
synchronized (threads) {
mainThread.setPriority(newPriority);
for (int i=threads.size()-1; i>=0; i--) {
((ProcessThread)threads.get(i)).setPriority(newPriority);
}
}
}
/**
* Registers connection in this process.
* All connections registered within process are
* closed when the process ends.
*/
public void addConnection(Connection conn) {
synchronized (connections) {
connections.add(conn);
}
}
/** Removes connection from this process. */
public void removeConnection(Connection conn) {
synchronized (connections) {
connections.remove(conn);
}
}
/** Returns current directory of this process. */
public String getCurrentDirectory() {
return curdir;
}
/** Sets current directory for this process. */
public synchronized void setCurrentDirectory(String dir) throws IOException {
dir = toFile(dir);
if (!Filesystem.isDirectory(dir))
throw new IOException("Not a directory: " + dir);
curdir = dir;
}
/** Converts file path to the normalized absolute path. */
public String toFile(String path) {
if (path.length() == 0 || path.charAt(0) == '/') {
return Filesystem.normalize(path);
} else {
return Filesystem.normalize(curdir + '/' + path);
}
}
/** Creates new thread in this process. */
public ProcessThread createThread(Function func) {
return new ProcessThread(this, func, Arrays.EMPTY);
}
/** Stops all threads and finalizes process. */
public void kill() {
killed = true;
synchronized (threads) {
for (int idx = threads.size()-1; idx >= 0; idx--) {
((ProcessThread)threads.get(idx)).interrupt();
}
threads.clear();
finalizeProcess();
}
}
/** Called when thread execution starts. */
void threadStarted(ProcessThread thread) {
synchronized (threads) {
threads.add(thread);
}
}
/** Called when thread execution ends. */
void threadEnded(ProcessThread thread) {
synchronized (threads) {
threads.remove(thread);
if (thread == mainThread) {
for (int idx = threads.size()-1; idx >= 0; idx--) {
((ProcessThread)threads.get(idx)).interrupt();
}
threads.clear();
finalizeProcess();
}
}
}
/**
* Flushes output streams and closes all connections.
*/
private void finalizeProcess() {
try { stdout.flush(); } catch (IOException ioe) { }
try { stderr.flush(); } catch (IOException ioe) { }
for (int i=connections.size()-1; i >= 0; i--) {
try {
((Connection)connections.get(i)).close();
} catch (IOException ioe) { }
}
for (int i=listeners.size()-1; i >= 0; i--) {
((ProcessListener)listeners.get(i)).processEnded(this);
}
}
/** Main function of the interpreter script. */
private static class InterpreterMain extends Function {
private final String intcmd;
private final String[] intargs;
public InterpreterMain(Library owner, String command, String[] args) {
super(owner, "main");
this.intcmd = command;
this.intargs = args;
}
public Object invoke(Process p, Object[] args) throws AlchemyException {
String[] params = new String[intargs.length + args.length];
System.arraycopy(intargs, 0, params, 0, intargs.length);
System.arraycopy(args, 0, params, intargs.length, args.length);
try {
return Int32.toInt32(new Process(p, intcmd, params).start().waitFor());
} catch (Throwable t) {
throw new AlchemyException(t);
}
}
}
}