001: /*
002: * Shell.java
003: *
004: * Copyright (C) 1998-2004 Peter Graves
005: * $Id: Shell.java,v 1.33 2004/09/19 14:13:59 piso Exp $
006: *
007: * This program is free software; you can redistribute it and/or
008: * modify it under the terms of the GNU General Public License
009: * as published by the Free Software Foundation; either version 2
010: * of the License, or (at your option) any later version.
011: *
012: * This program is distributed in the hope that it will be useful,
013: * but WITHOUT ANY WARRANTY; without even the implied warranty of
014: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
015: * GNU General Public License for more details.
016: *
017: * You should have received a copy of the GNU General Public License
018: * along with this program; if not, write to the Free Software
019: * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA.
020: */
021:
022: package org.armedbear.j;
023:
024: import gnu.regexp.REMatch;
025: import java.io.IOException;
026: import java.io.OutputStreamWriter;
027: import java.util.List;
028: import java.util.StringTokenizer;
029: import javax.swing.SwingUtilities;
030: import javax.swing.undo.CompoundEdit;
031:
032: public class Shell extends CommandInterpreter implements Constants {
033: protected static final String JPTY_NOT_FOUND = "Unable to start shell process (jpty not found in PATH)";
034:
035: private Process process;
036: private String command; // First token on command line.
037: private boolean promptIsStderr = true;
038: private File oldDir;
039: private File currentDir;
040: private File initialDir;
041: private boolean cygnify;
042:
043: protected Shell() {
044: type = TYPE_SHELL;
045: mode = Editor.getModeList().getMode(SHELL_MODE);
046: formatter = mode.getFormatter(this );
047: setInitialized(true);
048: }
049:
050: protected Shell(String shellCommand) {
051: this ();
052: this .shellCommand = shellCommand;
053: if (shellCommand != null && shellCommand.indexOf("tcsh") >= 0)
054: promptIsStderr = false;
055: }
056:
057: protected Shell(String shellCommand, Mode mode) {
058: type = TYPE_SHELL;
059: this .shellCommand = shellCommand;
060: this .mode = mode;
061: formatter = mode.getFormatter(this );
062: setInitialized(true);
063: }
064:
065: protected synchronized Process getProcess() {
066: return process;
067: }
068:
069: protected synchronized void setProcess(Process p) {
070: process = p;
071: }
072:
073: private static final String getDefaultShellCommand() {
074: String s = Editor.preferences().getStringProperty(
075: Property.SHELL_FILE_NAME);
076: if (s != null && s.length() > 0)
077: return s;
078: return Platform.isPlatformWindows() ? "cmd.exe" : "bash -i";
079: }
080:
081: protected void initializeHistory() {
082: history = new History("shell.history", 30);
083: }
084:
085: private static Shell createShell(String shellCommand) {
086: if (shellCommand == null) {
087: Debug.bug();
088: return null;
089: }
090: Shell shell = new Shell(shellCommand);
091: shell.startProcess();
092: if (shell.getProcess() == null) {
093: Editor.getBufferList().remove(shell);
094: String message = "Unable to start shell process \""
095: + shell.shellCommand + "\"";
096: MessageDialog.showMessageDialog(message, "Error");
097: return null;
098: }
099: shell.needsRenumbering = true;
100: return shell;
101: }
102:
103: protected void startProcess() {
104: if (shellCommand == null) {
105: Debug.bug();
106: return;
107: }
108: if (Platform.isPlatformWindows())
109: if (shellCommand.toLowerCase().indexOf("cmd.exe") < 0)
110: cygnify = true;
111: // Only set initialDir the first time we run, so that if we restart
112: // this shell, it will start up in the same directory each time.
113: if (initialDir == null) {
114: initialDir = Editor.currentEditor().getCurrentDirectory();
115: if (initialDir == null || initialDir.isRemote())
116: initialDir = Directories.getUserHomeDirectory();
117: }
118: // Shell command may contain a space (e.g. "bash -i").
119: StringTokenizer st = new StringTokenizer(shellCommand);
120: String[] cmdArray;
121: int i = 0;
122: if (Utilities.haveJpty()) {
123: cmdArray = new String[st.countTokens() + 1];
124: cmdArray[i++] = "jpty";
125: } else
126: cmdArray = new String[st.countTokens()];
127: while (st.hasMoreTokens())
128: cmdArray[i++] = st.nextToken();
129: Process p = null;
130: try {
131: p = Runtime.getRuntime().exec(cmdArray, null,
132: new java.io.File(initialDir.canonicalPath()));
133: setProcess(p);
134: } catch (Throwable t) {
135: setProcess(null);
136: return;
137: }
138: currentDir = initialDir;
139: startWatcherThread();
140: // See if the process exits right away (meaning jpty couldn't launch
141: // the shell command).
142: try {
143: Thread.sleep(100);
144: } catch (InterruptedException e) {
145: Log.error(e);
146: }
147: // When the process exits, the watcher thread calls setProcess(null),
148: // so check the value of getProcess() here.
149: if (getProcess() == null)
150: return; // Process exited.
151: setPromptRE(Editor.preferences().getStringProperty(
152: Property.SHELL_PROMPT_PATTERN));
153: try {
154: stdin = new OutputStreamWriter(p.getOutputStream());
155: stdoutThread = new StdoutThread(p.getInputStream());
156: stderrThread = new StderrThread(p.getErrorStream());
157: stdoutThread.start();
158: stderrThread.start();
159: readOnly = false;
160: } catch (Throwable t) {
161: Log.error(t);
162: }
163: }
164:
165: public void dispose() {
166: if (!checkProcess()) {
167: Log.debug("checkProcess returned false");
168: return;
169: }
170: Thread thread = new Thread("shell dispose") {
171: public void run() {
172: try {
173: stdin.write(3);
174: stdin.flush();
175: stdin.write("exit\n");
176: stdin.flush();
177: stdin.close();
178: final Process p = getProcess();
179: if (p != null) {
180: p.destroy();
181: p.waitFor();
182: }
183: } catch (IOException e) {
184: Log.error(e);
185: } catch (InterruptedException e) {
186: Log.error(e);
187: }
188: }
189: };
190: thread.setDaemon(true);
191: thread.start();
192: }
193:
194: protected void enter(final String s) {
195: super .enter(s);
196: // If it's a local shell (i.e. not telnet or ssh), keep track of the
197: // current directory.
198: if (type == TYPE_SHELL) {
199: ShellTokenizer st = new ShellTokenizer(s);
200: command = s.trim();
201: String arg = null;
202: if (st.hasMoreTokens()) {
203: command = st.nextToken();
204: if (st.hasMoreTokens())
205: arg = st.nextToken();
206: }
207: if (command.equals("cd")) {
208: if (arg == null)
209: changeDirectory(Utilities.getUserHome());
210: else if (arg.equals("-")) {
211: if (oldDir != null)
212: changeDirectory(oldDir.canonicalPath());
213: } else
214: changeDirectory(arg);
215: } else if (command.equals("pushd"))
216: changeDirectory(arg);
217: }
218: }
219:
220: protected boolean checkProcess() {
221: Process p = getProcess();
222: if (p == null)
223: return false;
224: if (Utilities.isProcessAlive(p))
225: return true;
226: // Not alive.
227: setProcess(null);
228: readOnly = true;
229: resetUndo();
230: return false;
231: }
232:
233: protected void startWatcherThread() {
234: Thread thread = new Thread("shell watcher") {
235: public void run() {
236: try {
237: Process p = getProcess();
238: if (p != null)
239: p.waitFor();
240: setProcess(null);
241: if (stdoutThread != null)
242: stdoutThread.join();
243: if (stderrThread != null)
244: stderrThread.join();
245: } catch (InterruptedException e) {
246: Log.error(e);
247: }
248: Runnable processExitedRunnable = new Runnable() {
249: public void run() {
250: appendString("\nProcess exited\n");
251: setBusy(false);
252: updateDisplayInAllFrames();
253: }
254: };
255: if (stderrThread != null)
256: SwingUtilities.invokeLater(processExitedRunnable);
257: }
258: };
259: thread.setDaemon(true);
260: thread.start();
261: }
262:
263: private void tab() {
264: final Editor editor = Editor.currentEditor();
265: if (editor.getMark() != null)
266: return;
267: final Line dotLine = editor.getDotLine();
268: final String dotLineText = dotLine.getText();
269: final REMatch match = promptRE.getMatch(dotLineText);
270: if (match == null)
271: return; // Not at prompt.
272: final String prompt = match.toString();
273: final String userInput = dotLineText.substring(match
274: .getEndIndex());
275: if (userInput.length() == 0)
276: return; // Nothing to complete.
277: final ShellTokenizer st = new ShellTokenizer(userInput);
278: final String prefix = st.lastToken();
279: if (prefix == null)
280: return; // Nothing to complete.
281: final String toBeCompleted = unescape(prefix);
282: final Completion completion = new Completion(currentDir,
283: toBeCompleted, shellCommand);
284: final String toBeInserted = completion.toString();
285: if (!toBeInserted.equals(prefix)) {
286: CompoundEdit compoundEdit = beginCompoundEdit();
287: editor.addUndo(SimpleEdit.MOVE);
288: editor.getDot().setOffset(dotLineText.lastIndexOf(prefix));
289: // Remove prefix from line.
290: final String head = dotLine.substring(0, editor
291: .getDotOffset());
292: final String tail = dotLine.substring(editor.getDotOffset()
293: + prefix.length());
294: editor.addUndo(SimpleEdit.LINE_EDIT);
295: dotLine.setText(head.concat(tail));
296: // Insert completion.
297: editor.addUndo(SimpleEdit.INSERT_STRING);
298: editor.insertStringInternal(toBeInserted);
299: // Move dot past inserted string.
300: editor.moveCaretToDotCol();
301: endCompoundEdit(compoundEdit);
302: Editor.updateInAllEditors(dotLine);
303: } else {
304: final List completions = completion.getCompletions();
305: final int size = completions.size();
306: if (size > 0) {
307: dotLine.setFlags(STATE_INPUT);
308: editor.insertLineSeparator();
309: for (int i = 0; i < size; i++) {
310: String s = (String) completions.get(i);
311: s = unescape(s);
312: int index = s.lastIndexOf('/', s.length() - 2);
313: if (index >= 0)
314: s = s.substring(index + 1);
315: editor.insertStringInternal(s);
316: editor.getDotLine().setFlags(STATE_OUTPUT);
317: editor.insertLineSeparator();
318: }
319: if (prompt != null)
320: editor.insertStringInternal(prompt);
321: editor.insertStringInternal(userInput);
322: editor.getDotLine().setFlags(STATE_INPUT);
323: editor.eob();
324: editor.getDisplay().setReframe(-2);
325: resetUndo();
326: }
327: }
328: }
329:
330: private void updateDirectory(String output) {
331: if (command != null) {
332: String s = output;
333: int index = s.indexOf('\r');
334: if (index >= 0)
335: s = s.substring(0, index);
336: index = s.indexOf('\n');
337: if (index >= 0)
338: s = s.substring(0, index);
339: if (command.equals("pwd") || command.equals("cd")) {
340: changeDirectory(s);
341: } else if (command.equals("popd")) {
342: // BUG! Directory names with embedded spaces will not be
343: // handled correctly!
344: index = s.indexOf(' ');
345: if (index >= 0)
346: s = s.substring(0, index);
347: changeDirectory(s);
348: }
349: }
350: }
351:
352: private void changeDirectory(String s) {
353: if (s != null) {
354: s = unescape(s).trim();
355: if (s.length() > 0) {
356: char c = s.charAt(0);
357: if (c == '\'' || c == '"') {
358: s = s.substring(1);
359: final int length = s.length();
360: if (length > 0 && s.charAt(length - 1) == c)
361: s = s.substring(0, length - 1);
362: }
363: } else
364: s = Utilities.getUserHome();
365: if (cygnify) {
366: if (!s.startsWith(".."))
367: s = Utilities.uncygnify(s);
368: }
369: File dir = File.getInstance(currentDir, s);
370: if (dir != null && dir.isDirectory()) {
371: oldDir = currentDir;
372: currentDir = dir;
373: for (EditorIterator it = new EditorIterator(); it
374: .hasNext();) {
375: Editor ed = it.nextEditor();
376: if (ed.getBuffer() == this )
377: ed.updateLocation();
378: }
379: }
380: }
381: }
382:
383: private String unescape(String s) {
384: // Is '\' an escape character?
385: boolean backslashIsEscape = Platform.isPlatformUnix()
386: || cygnify;
387:
388: FastStringBuffer sb = new FastStringBuffer(s.length());
389: char quoteChar = 0;
390: final int limit = s.length();
391: for (int i = 0; i < limit; i++) {
392: char c = s.charAt(i);
393: if (quoteChar != 0) {
394: if (c == quoteChar)
395: quoteChar = 0;
396: else
397: sb.append(c);
398: } else if (c == '\'' || c == '"') {
399: quoteChar = c;
400: } else if (backslashIsEscape && c == '\\') {
401: if (i < limit - 1)
402: sb.append(s.charAt(++i));
403: } else
404: sb.append(c);
405: }
406: return sb.toString();
407: }
408:
409: public String getFileNameForDisplay() {
410: return (currentDir != null) ? currentDir.canonicalPath() : "";
411: }
412:
413: // For the buffer list.
414: public String toString() {
415: return shellCommand != null ? shellCommand : "";
416: }
417:
418: public File getCurrentDirectory() {
419: return currentDir;
420: }
421:
422: public File getCompletionDirectory() {
423: return currentDir;
424: }
425:
426: protected void appendString(String s) {
427: if (s.indexOf(0x1b) >= 0) {
428: // Strip escape sequences used for ls colorization.
429: int limit = s.length();
430: FastStringBuffer sb = new FastStringBuffer(limit);
431: int i = 0;
432: while (i < limit) {
433: char c = s.charAt(i++);
434: if (c == 0x1b) {
435: // Skip escaped chars through 'm'.
436: while (i < limit && s.charAt(i++) != 'm')
437: ;
438: } else
439: sb.append(c);
440: }
441: s = sb.toString();
442: }
443: super .appendString(s);
444: }
445:
446: protected void stdOutUpdate(final String s) {
447: Runnable r = new Runnable() {
448: public void run() {
449: if (s.length() > 0) {
450: updateDirectory(s);
451: appendString(s);
452: }
453: updateLineFlags();
454: updateDisplayInAllFrames();
455: resetUndo();
456: checkPasswordPrompt();
457: }
458: };
459: SwingUtilities.invokeLater(r);
460: }
461:
462: protected void stdErrUpdate(final String s) {
463: if (promptIsStderr) {
464: REMatch match = promptRE.getMatch(s);
465: if (match != null) {
466: // This looks like the prompt.
467: // Give stdout a chance to finish.
468: try {
469: Thread.sleep(100);
470: } catch (InterruptedException e) {
471: Log.error(e);
472: }
473: }
474: }
475: Runnable r = new Runnable() {
476: public void run() {
477: appendString(s);
478: updateLineFlags();
479: updateDisplayInAllFrames();
480: resetUndo();
481: }
482: };
483: SwingUtilities.invokeLater(r);
484: }
485:
486: protected void updateLineFlags() {
487: Debug.assertTrue(SwingUtilities.isEventDispatchThread());
488: Position endOfOutput = getEndOfOutput();
489: if (endOfOutput == null)
490: return;
491: final Line last = endOfOutput.getLine();
492: if (isPasswordPrompt(last)) {
493: last.setFlags(STATE_PASSWORD_PROMPT);
494: return;
495: }
496: if (last.flags() != STATE_INPUT)
497: last.setFlags(0);
498: // Look at the next-to-last line.
499: final Line nextToLast = last.previous();
500: // For now, this is a hard-coded test for Mikol's prompt. It should
501: // really use a configurable regexp.
502: if (nextToLast != null && nextToLast.getText().startsWith("| ")) {
503: // Next-to-last line looks like first line of 2-line prompt.
504: // See if the last line looks like the second line of the prompt.
505: REMatch match = promptRE.getMatch(last.getText());
506: if (match != null)
507: nextToLast.setFlags(STATE_PROMPT);
508: }
509: }
510:
511: private boolean isPasswordPrompt(Line line) {
512: final String text = line.trim();
513: if (text.startsWith("Enter passphrase") && text.endsWith(":"))
514: return true;
515: if (text.toLowerCase().endsWith("password:"))
516: return true;
517: if (text.equals("Response:"))
518: return true;
519: return false;
520: }
521:
522: protected void checkPasswordPrompt() {
523: Position endOfOutput = getEndOfOutput();
524: if (endOfOutput != null
525: && isPasswordPrompt(endOfOutput.getLine())) {
526: if (!Editor.getBufferList().contains(this ))
527: return;
528: String password = PasswordDialog.showPasswordDialog(Editor
529: .currentEditor(), "Password:", "Password");
530: if (password != null) {
531: try {
532: stdin.write(password + "\n");
533: stdin.flush();
534: setEndOfOutput(new Position(getEnd()));
535: } catch (IOException e) {
536: Log.error(e);
537: }
538: }
539: }
540: }
541:
542: public static void shell() {
543: shell(getDefaultShellCommand());
544: }
545:
546: public static void shell(String shellCommand) {
547: if (shellCommand == null) {
548: Debug.bug();
549: return;
550: }
551: if (!Editor.checkExperimental())
552: return;
553: if (Platform.isPlatformWindows()) {
554: if (!Platform.isPlatformWindows5())
555: return;
556: } else {
557: // Unix.
558: if (!Utilities.haveJpty()) {
559: MessageDialog
560: .showMessageDialog(JPTY_NOT_FOUND, "Error");
561: return;
562: }
563: }
564: // Look for existing shell buffer.
565: Buffer buf = null;
566: for (BufferIterator it = new BufferIterator(); it.hasNext();) {
567: Buffer b = it.nextBuffer();
568: if (b instanceof Shell) {
569: if (shellCommand.equals(((Shell) b).shellCommand)) {
570: buf = b;
571: break;
572: }
573: }
574: }
575: if (buf != null) {
576: Shell shell = (Shell) buf;
577: if (shell.getProcess() == null)
578: shell.startProcess();
579: } else
580: buf = createShell(shellCommand);
581: if (buf != null) {
582: final Editor editor = Editor.currentEditor();
583: editor.makeNext(buf);
584: editor.switchToBuffer(buf);
585: }
586: }
587:
588: public static void shellTab() {
589: final Buffer buffer = Editor.currentEditor().getBuffer();
590: if (buffer instanceof Shell && !(buffer instanceof RemoteShell))
591: ((Shell) buffer).tab();
592: }
593:
594: public static void shellInterrupt() {
595: final Buffer buffer = Editor.currentEditor().getBuffer();
596: if (buffer instanceof Shell)
597: ((Shell) buffer).sendChar(3);
598: }
599: }
|