package org.gscript.process;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
import org.gscript.data.ContentUri;
import org.gscript.data.ContentUri.LibraryPathSegments;
import org.gscript.data.library.ItemAttributes;
import org.gscript.data.library.Library;
import org.gscript.data.library.LibraryItem;
import org.gscript.jni.JNIReference.JNIReferenceException;
import org.gscript.settings.ShellProfile;
import org.gscript.terminal.ScreenBuffer;
import org.gscript.terminal.TerminalEmulator;
import android.content.ContentResolver;
import android.content.Context;
import android.content.Intent;
import android.database.Cursor;
import android.net.Uri;
import android.util.Log;
public class ProcessTask extends ProcessDescriptor implements TerminalEmulator.OnEmulatorEventListener {
static final String LOG_TAG = "ProcessTask";
static final String SCHEME_FILE = "file";
static final String SCHEME_HTTP = "http";
static final String SCHEME_HTTPS = "https";
static final String SCHEME_FTP = "ftp";
static final String SCHEME_CONTENT = "content";
static final String PATHS_DEFAULT = "/sbin:/vendor/bin:/system/sbin:/system/bin:/system/xbin";
static final String PATHS_GSCRIPT = ":/data/data/org.gscript/bin";
static final String PATH_ARG = "%path%";
static final String ENV_GSCRIPT_VERSION = "GSCRIPT_VERSION";
static final String ENV_GSCRIPT_PID = "GSCRIPT_PID";
static final String EOL = System.getProperty("line.separator", "\n");
static final String EXIT_CMD = EOL + "exit" + EOL;
static final String PROCESS_TASK_THREAD = "ProcessTask";
static final String PROCESS_INPUT_THREAD = "ProcessInput";
final ProcessStateListener mStateListener;
final Context mContext;
ItemAttributes mAttributes;
int mState;
boolean mKillRequested;
ProcessThread mProcessThread;
SubProcess mSubProcess;
TerminalEmulator mEmulator;
public ProcessTask(Context context, ProcessDescriptor pd,
ProcessStateListener l) {
super(pd);
mAttributes = new ItemAttributes();
mStateListener = l;
mContext = context;
mProcessThread = new ProcessThread();
mProcessThread.setName(PROCESS_TASK_THREAD);
mProcessThread.start();
}
private void setState(int state) {
mState = state;
mStateListener.OnProcessStateChanged(ProcessTask.this, mState);
}
private void dispatchEvent(int event) {
mStateListener.OnProcessEvent(ProcessTask.this, event);
}
@Override
public void OnEmulatorEvent(int event) {
dispatchEvent(event);
}
public void sendOutput(byte[] output, int offset, int length) {
if (isActive() && mEmulator != null) {
mEmulator.processOutput(output, offset, length);
}
}
public int getState() {
return mState;
}
Context getContext() {
return mContext;
}
public ScreenBuffer getScreenBuffer() {
return (mEmulator != null) ? mEmulator.getScreenBuffer() : null;
}
public String getTranscript() {
return (mEmulator != null) ? mEmulator.getScreenBuffer().getTranscript() : "";
}
public ItemAttributes getAttributes() {
return mAttributes;
}
public boolean isActive() {
return ProcessState.isActiveState(mState);
}
public void requestKill() {
if (isActive() && mSubProcess.sigKill(SubProcess.SIGHUP) > 0) {
setState(ProcessState.STATE_KILLED);
}
}
public void requestWindowSizeChange(int rows, int cols, int width, int height) {
if(mEmulator != null)
mEmulator.resizeWindow(rows, cols, width, height);
if(mSubProcess != null)
mSubProcess.resizeWindow(rows, cols, width, height);
}
class ProcessThread extends Thread {
@SuppressWarnings("unchecked")
@Override
public void run() {
final Intent intent = getIntent();
final Uri uri = intent.getData();
int error = 0;
File scriptFile = null;
String content = null;
ShellProfile profile = null;
String profileCommand = null;
if(ProcessService.ACTION_EXECUTE.equals(intent.getAction())) {
content = getContentForUri(uri);
if (content == null) {
error = ProcessState.ERROR_NO_CONTENT;
Log.e(LOG_TAG, String.format(
"Error trying to start process: %d", error));
setState(ProcessState.STATE_ERROR(error));
return;
}
/*
* merge attributes with any attributes received in initial intent
* so that we can have different defaults for processes launched
* from a specific condition
*/
if (intent.hasExtra(ProcessService.EXTRA_ATTRIBUTES))
mAttributes.putAll((HashMap<String, String>) intent
.getSerializableExtra(ProcessService.EXTRA_ATTRIBUTES));
/* get shell profile from attributes */
profile = ShellProfile.forKey(getContext(),
mAttributes.get(ItemAttributes.ATTRIBUTE_SHELL));
profileCommand = profile.cmdExec;
} if(ProcessService.ACTION_START.equals(intent.getAction())) {
/* get shell profile directly from extra */
profile = ShellProfile.forKey(getContext(), intent.getStringExtra(ProcessService.EXTRA_PROFILE));
profileCommand = profile.cmdStart;
}
mEmulator = new TerminalEmulator(profile, ProcessTask.this);
/*
* we now have everything ( content and attributes ) we need to
* enter a running state. notify callbacks we are running and then
* continue executing the actual process
*/
setState(ProcessState.STATE_RUNNING);
/* build argument list */
List<String> arguments = getArguments(profileCommand);
/* check if arguments contains %path% */
boolean hasPathArg = arguments.contains(PATH_ARG);
/* append exit if needed and we have content */
if (content != null && profile.appendExit) {
content += EXIT_CMD;
}
/* create executable script file if needed */
if (content != null && hasPathArg) {
scriptFile = createScriptFile(content);
/* replace PATH_ARG with real file path */
if (scriptFile != null) {
int args = arguments.size();
for (int i = 0; i < args; ++i) {
if (arguments.get(i).equals(PATH_ARG)) {
arguments.set(i, scriptFile.getPath());
}
}
}
}
/* create SubProcess and execute */
String env[] = getEnvironment(profile.appendPath);
String args[] = arguments.toArray(new String[0]);
String command = args[0];
try {
mSubProcess = new SubProcess(command, args, env);
/* execute and resize according to emulator dimensions */
mSubProcess.execute();
mSubProcess.resizeWindow(
mEmulator.getScreenRows(),
mEmulator.getScreenCols(),
mEmulator.getScreenWidth(),
mEmulator.getScreenHeight());
} catch (JNIReferenceException ex) {
Log.e(LOG_TAG, ex.getMessage());
error = ProcessState.ERROR_EXECUTION_FAILED;
}
if (error != 0) {
Log.e(LOG_TAG, String.format(
"Error trying to start process: %d", error));
setState(ProcessState.STATE_ERROR(error));
} else {
/* start input / output stream handling */
String initialOutput = null;
if (scriptFile != null && hasPathArg) {
initialOutput = null;
}
if (scriptFile != null && !hasPathArg) {
initialOutput = scriptFile.getPath() + EOL;
}
if (scriptFile == null) {
initialOutput = content;
}
final InputStream in = mSubProcess.getInputStream();
final OutputStream out = mSubProcess.getOutputStream();
/*
* write initial output directly to outputstream bypassing the
* emulator
*/
if (initialOutput != null) {
byte[] b = initialOutput.getBytes();
try {
out.write(b, 0, b.length);
} catch (IOException e) {
e.printStackTrace();
}
}
new InputThread(in, mEmulator).start();
mEmulator.setOutputStream(out);
/* wait for process to finish */
mSubProcess.waitFor();
// try {
//
// out.close();
// in.close();
//
// } catch (IOException e) {
// }
/*
* only change state if previous state was active because we
* might have received a kill request and finished because of
* that
*/
if (mState == ProcessState.STATE_RUNNING) {
setState(ProcessState.STATE_FINISHED);
}
}
/* clean up */
if (scriptFile != null && scriptFile.exists()) {
scriptFile.delete();
}
}
};
private String[] getEnvironment(String appendPath) {
String envPath = System.getenv("PATH");
{
if (envPath == null)
envPath = PATHS_DEFAULT;
envPath += PATHS_GSCRIPT;
if (appendPath != null && appendPath.length() > 0) {
if (!appendPath.startsWith(":"))
envPath += ":";
envPath += appendPath;
}
}
/* build environment */
String env[] = {
ENV_GSCRIPT_VERSION + "=0.1",
ENV_GSCRIPT_PID + "=" + getPid(),
"PATH=" + envPath,
"TERM=screen" };
return env;
}
private ArrayList<String> getArguments(String command) {
ArrayList<String> arguments = new ArrayList<String>();
Pattern regex = Pattern.compile("[^\\s\"']+|\"([^\"]*)\"|'([^']*)'");
Matcher regexMatcher = regex.matcher(command);
while (regexMatcher.find()) {
if (regexMatcher.group(1) != null) {
arguments.add(regexMatcher.group(1)); // double-quoted
} else if (regexMatcher.group(2) != null) {
arguments.add(regexMatcher.group(2)); // single-quoted
} else {
arguments.add(regexMatcher.group()); // unquoted
}
}
return arguments;
}
private String getContentForUri(Uri uri) {
String content = null;
String scheme = uri.getScheme();
if (SCHEME_FILE.equals(scheme) || SCHEME_HTTP.equals(scheme)
|| SCHEME_HTTPS.equals(scheme) || SCHEME_FTP.equals(scheme)) {
/* handle directly executed scripts from file or http/https/ftp */
try {
URL url = new URL(uri.toString());
StringBuilder sb = new StringBuilder(100);
BufferedReader in = new BufferedReader(new InputStreamReader(
url.openStream()));
String line;
while ((line = in.readLine()) != null) {
sb.append(line);
sb.append(EOL);
}
in.close();
content = sb.toString();
} catch (MalformedURLException e) {
} catch (IOException e) {
}
} else if (SCHEME_CONTENT.equals(uri.getScheme())) {
/* handle library content uris */
LibraryPathSegments seg = LibraryPathSegments.parse(uri);
final ContentResolver cr = getContext().getContentResolver();
/* get item content */
{
Uri itemUri = ContentUri.URI_LIBRARY_PATH(seg.id, seg.path,
Library.FLAG_INCLUDE_CONTENT);
Cursor itemCursor = cr.query(itemUri, null, null, null, null);
if (itemCursor != null) {
if (itemCursor.moveToFirst()) {
LibraryItem item = LibraryItem.fromCursor(itemCursor);
content = item.getContent();
}
}
itemCursor.close();
}
/* get item attributes */
{
Uri attributesUri = ContentUri.URI_ITEM_ATTRIBS_PATH(seg.id,
seg.path);
Cursor attributesCursor = cr.query(attributesUri, null, null,
null, null);
if (attributesCursor != null) {
ItemAttributes itemAttributes = new ItemAttributes(
attributesCursor);
mAttributes.putAll(itemAttributes);
attributesCursor.close();
}
}
}
return content;
}
private File createScriptFile(String content) {
File scriptFile = null;
try {
scriptFile = File.createTempFile(ProcessTask.this.getPid() + "_"
+ ProcessTask.this.getTime(), ".tmp", ProcessTask.this
.getContext().getCacheDir());
BufferedWriter writer = null;
try {
writer = new BufferedWriter(new FileWriter(scriptFile));
writer.write(content);
} catch (IOException e) {
} finally {
try {
if (writer != null)
writer.close();
} catch (IOException e) {
}
}
scriptFile.setExecutable(true);
return scriptFile;
} catch (IOException e1) {
}
return scriptFile;
}
@Override
public boolean equals(Object o) {
if (o instanceof ProcessDescriptor) {
ProcessDescriptor opd = (ProcessDescriptor) o;
ProcessDescriptor pd = (ProcessDescriptor) this;
return (opd.equals(pd));
}
return super.equals(o);
}
/* TODO: move to TerminalEmulator */
class InputThread extends Thread {
private final InputStream is;
private final TerminalEmulator emulator;
public InputThread(InputStream is, TerminalEmulator emulator) {
this.setName(PROCESS_INPUT_THREAD);
this.is = is;
this.emulator = emulator;
}
@Override
public void run() {
try {
byte buffer[] = new byte[4096];
int read = 0;
while ((read = is.read(buffer)) != -1) {
emulator.processInput(buffer, 0, read);
}
} catch (Exception e) {
}
}
}
}