/* * 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 com.adblog.term; import java.io.File; import java.io.FileDescriptor; import java.io.FileInputStream; import java.io.FileNotFoundException; import java.io.FileOutputStream; import java.io.UnsupportedEncodingException; import java.util.ArrayList; import com.adblog.compat.FileCompat; import com.adblog.util.TermSettings; import android.os.Handler; import android.os.Message; import android.util.Log; import jackpal.androidterm.emulatorview.ColorScheme; import jackpal.androidterm.emulatorview.TermSession; import jackpal.androidterm.emulatorview.UpdateCallback; /** * A terminal session, consisting of a TerminalEmulator, a TranscriptScreen, * the PID of the process attached to the session, and the I/O streams used to * talk to the process. */ public class ShellTermSession extends TermSession { //** Set to true to force into 80 x 24 for testing with vttest. */ private static final boolean VTTEST_MODE = false; private TermSettings mSettings; private int mProcId; private FileDescriptor mTermFd; private Thread mWatcherThread; // A cookie which uniquely identifies this session. private String mHandle; private String mInitialCommand; public static final int PROCESS_EXIT_FINISHES_SESSION = 0; public static final int PROCESS_EXIT_DISPLAYS_MESSAGE = 1; private int mProcessExitBehavior = PROCESS_EXIT_FINISHES_SESSION; private String mProcessExitMessage; 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); } } }; private UpdateCallback mUTF8ModeNotify = new UpdateCallback() { public void onUpdate() { Exec.setPtyUTF8Mode(mTermFd, getUTF8Mode()); } }; public ShellTermSession(TermSettings settings, String initialCommand) { super(); updatePrefs(settings); initializeSession(); mInitialCommand = initialCommand; mWatcherThread = new Thread() { @Override public void run() { Log.i(TermDebug.LOG_TAG, "waiting for: " + mProcId); int result = Exec.waitFor(mProcId); Log.i(TermDebug.LOG_TAG, "Subprocess exited: " + result); mMsgHandler.sendMessage(mMsgHandler.obtainMessage(PROCESS_EXITED, result)); } }; mWatcherThread.setName("Process watcher"); } public void updatePrefs(TermSettings settings) { mSettings = settings; setColorScheme(new ColorScheme(settings.getColorScheme())); setDefaultUTF8Mode(settings.defaultToUTF8Mode()); } private void initializeSession() { TermSettings settings = mSettings; int[] processId = new int[1]; 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(); createSubprocess(processId, settings.getShell(), env); mProcId = processId[0]; setTermOut(new FileOutputStream(mTermFd)); setTermIn(new FileInputStream(mTermFd)); } 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) { if (VTTEST_MODE) { columns = 80; rows = 24; } super.initializeEmulator(columns, rows); Exec.setPtyUTF8Mode(mTermFd, getUTF8Mode()); setUTF8ModeUpdateCallback(mUTF8ModeNotify); mWatcherThread.start(); sendInitialCommand(mInitialCommand); } private void sendInitialCommand(String initialCommand) { if (initialCommand.length() > 0) { write(initialCommand + '\r'); } } private void createSubprocess(int[] processId, String shell, String[] env) { 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]); } mTermFd = Exec.createSubprocess(arg0, args, env, processId); } 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; } @Override public void updateSize(int columns, int rows) { if (VTTEST_MODE) { columns = 80; rows = 24; } // Inform the attached pty of our new size: Exec.setPtyWindowSize(mTermFd, rows, columns, 0, 0); super.updateSize(columns, rows); } /* XXX We should really get this ourselves from the resource bundle, but we cannot hold a context */ public void setProcessExitMessage(String message) { mProcessExitMessage = message; } private void onProcessExit(int result) { if (mSettings.closeWindowOnProcessExit()) { finish(); } else if (mProcessExitMessage != null) { try { byte[] msg = ("\r\n[" + mProcessExitMessage + "]").getBytes("UTF-8"); appendToEmulator(msg, 0, msg.length); notifyUpdate(); } catch (UnsupportedEncodingException e) { // Never happens } } } @Override public void finish() { Exec.hangupProcessGroup(mProcId); Exec.close(mTermFd); super.finish(); } /** * Gets the terminal session's title. Unlike the superclass's getTitle(), * if the title is null or an empty string, the provided default title will * be returned instead. * * @param defaultTitle The default title to use if this session's title is * unset or an empty string. */ public String getTitle(String defaultTitle) { String title = super.getTitle(); if (title != null && title.length() > 0) { return title; } else { return defaultTitle; } } public void setHandle(String handle) { if (mHandle != null) { throw new IllegalStateException("Cannot change handle once set"); } mHandle = handle; } public String getHandle() { return mHandle; } }