/*
* Copyright (C) 2007 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package jackpal.androidterm;
import android.os.Handler;
import android.os.Message;
import android.os.ParcelFileDescriptor;
import android.util.Log;
import jackpal.androidterm.compat.FileCompat;
import jackpal.androidterm.util.TermSettings;
import java.io.*;
import java.util.ArrayList;
/**
* A terminal session, controlling the process attached to the session (usually
* a shell). It keeps track of process PID and destroys it's process group
* upon stopping.
*/
public class ShellTermSession extends GenericTermSession {
private int mProcId;
private Thread mWatcherThread;
private String mInitialCommand;
private static final int PROCESS_EXITED = 1;
private Handler mMsgHandler = new Handler() {
@Override
public void handleMessage(Message msg) {
if (!isRunning()) {
return;
}
if (msg.what == PROCESS_EXITED) {
onProcessExit((Integer) msg.obj);
}
}
};
public ShellTermSession(TermSettings settings, String initialCommand) throws IOException {
super(ParcelFileDescriptor.open(new File("/dev/ptmx"), ParcelFileDescriptor.MODE_READ_WRITE),
settings, false);
initializeSession();
setTermOut(new ParcelFileDescriptor.AutoCloseOutputStream(mTermFd));
setTermIn(new ParcelFileDescriptor.AutoCloseInputStream(mTermFd));
mInitialCommand = initialCommand;
mWatcherThread = new Thread() {
@Override
public void run() {
Log.i(TermDebug.LOG_TAG, "waiting for: " + mProcId);
int result = TermExec.waitFor(mProcId);
Log.i(TermDebug.LOG_TAG, "Subprocess exited: " + result);
mMsgHandler.sendMessage(mMsgHandler.obtainMessage(PROCESS_EXITED, result));
}
};
mWatcherThread.setName("Process watcher");
}
private void initializeSession() throws IOException {
TermSettings settings = mSettings;
String path = System.getenv("PATH");
if (settings.doPathExtensions()) {
String appendPath = settings.getAppendPath();
if (appendPath != null && appendPath.length() > 0) {
path = path + ":" + appendPath;
}
if (settings.allowPathPrepend()) {
String prependPath = settings.getPrependPath();
if (prependPath != null && prependPath.length() > 0) {
path = prependPath + ":" + path;
}
}
}
if (settings.verifyPath()) {
path = checkPath(path);
}
String[] env = new String[3];
env[0] = "TERM=" + settings.getTermType();
env[1] = "PATH=" + path;
env[2] = "HOME=" + settings.getHomePath();
mProcId = createSubprocess(settings.getShell(), env);
}
private String checkPath(String path) {
String[] dirs = path.split(":");
StringBuilder checkedPath = new StringBuilder(path.length());
for (String dirname : dirs) {
File dir = new File(dirname);
if (dir.isDirectory() && FileCompat.canExecute(dir)) {
checkedPath.append(dirname);
checkedPath.append(":");
}
}
return checkedPath.substring(0, checkedPath.length()-1);
}
@Override
public void initializeEmulator(int columns, int rows) {
super.initializeEmulator(columns, rows);
mWatcherThread.start();
sendInitialCommand(mInitialCommand);
}
private void sendInitialCommand(String initialCommand) {
if (initialCommand.length() > 0) {
write(initialCommand + '\r');
}
}
private int createSubprocess(String shell, String[] env) throws IOException {
ArrayList<String> argList = parse(shell);
String arg0;
String[] args;
try {
arg0 = argList.get(0);
File file = new File(arg0);
if (!file.exists()) {
Log.e(TermDebug.LOG_TAG, "Shell " + arg0 + " not found!");
throw new FileNotFoundException(arg0);
} else if (!FileCompat.canExecute(file)) {
Log.e(TermDebug.LOG_TAG, "Shell " + arg0 + " not executable!");
throw new FileNotFoundException(arg0);
}
args = argList.toArray(new String[1]);
} catch (Exception e) {
argList = parse(mSettings.getFailsafeShell());
arg0 = argList.get(0);
args = argList.toArray(new String[1]);
}
return TermExec.createSubprocess(mTermFd, arg0, args, env);
}
private ArrayList<String> parse(String cmd) {
final int PLAIN = 0;
final int WHITESPACE = 1;
final int INQUOTE = 2;
int state = WHITESPACE;
ArrayList<String> result = new ArrayList<String>();
int cmdLen = cmd.length();
StringBuilder builder = new StringBuilder();
for (int i = 0; i < cmdLen; i++) {
char c = cmd.charAt(i);
if (state == PLAIN) {
if (Character.isWhitespace(c)) {
result.add(builder.toString());
builder.delete(0,builder.length());
state = WHITESPACE;
} else if (c == '"') {
state = INQUOTE;
} else {
builder.append(c);
}
} else if (state == WHITESPACE) {
if (Character.isWhitespace(c)) {
// do nothing
} else if (c == '"') {
state = INQUOTE;
} else {
state = PLAIN;
builder.append(c);
}
} else if (state == INQUOTE) {
if (c == '\\') {
if (i + 1 < cmdLen) {
i += 1;
builder.append(cmd.charAt(i));
}
} else if (c == '"') {
state = PLAIN;
} else {
builder.append(c);
}
}
}
if (builder.length() > 0) {
result.add(builder.toString());
}
return result;
}
private void onProcessExit(int result) {
onProcessExit();
}
@Override
public void finish() {
hangupProcessGroup();
super.finish();
}
/**
* Send SIGHUP to a process group, SIGHUP notifies a terminal client, that the terminal have been disconnected,
* and usually results in client's death, unless it's process is a daemon or have been somehow else detached
* from the terminal (for example, by the "nohup" utility).
*/
void hangupProcessGroup() {
TermExec.sendSignal(-mProcId, 1);
}
}