/******************************************************************************* * Copyright (c) 2011, 2012 Wind River Systems, Inc. and others. * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * which accompanies this distribution, and is available at * http://www.eclipse.org/legal/epl-v10.html * * Contributors: * Wind River Systems - initial API and implementation *******************************************************************************/ package org.eclipse.tcf.internal.debug.tests; import java.util.ArrayList; import java.util.Collection; import java.util.HashSet; import java.util.Iterator; import java.util.List; import java.util.Map; import java.util.Random; import org.eclipse.tcf.protocol.IChannel; import org.eclipse.tcf.protocol.IToken; import org.eclipse.tcf.protocol.Protocol; import org.eclipse.tcf.services.IProcesses; import org.eclipse.tcf.services.IStreams; import org.eclipse.tcf.services.ITerminals; import org.eclipse.tcf.services.ITerminals.TerminalContext; class TestTerminals implements ITCFTest { private final TCFTestSuite test_suite; private final ITerminals terminals; private final IProcesses processes; private final IStreams streams; private final Random rnd = new Random(); private final HashSet<String> stream_ids = new HashSet<String>(); private final StringBuffer stdout_buf = new StringBuffer(); private final StringBuffer stderr_buf = new StringBuffer(); private final HashSet<IToken> disconnect_cmds = new HashSet<IToken>(); private final List<String> echo_tx = new ArrayList<String>(); private final List<Integer> echo_rx = new ArrayList<Integer>(); private final int echo_cnt = 50; private IStreams.StreamsListener streams_listener; private IStreams.DoneRead stdout_read; private IStreams.DoneRead stderr_read; private String encoding; private TerminalContext terminal; private TerminalContext get_ctx; private Map<String,String> environment; private boolean delay_done; private Collection<Map<String,Object>> signal_list; private IToken get_signals_cmd; private IToken signal_cmd; private boolean signal_sent; private IToken unsubscribe_cmd; private boolean unsubscribe_done; private boolean exited; private boolean stdout_eos; private int time_out = 0; private final ITerminals.TerminalsListener listener = new ITerminals.TerminalsListener() { public void exited(String id, int exit_code) { if (terminal != null && id.equals(terminal.getID())) { exited = true; if (!signal_sent) { exit(new Exception("Terminal exited with code " + exit_code)); } else { run(); } } } public void winSizeChanged(String id, int w, int h) { } }; private final IStreams.DoneDisconnect disconnect_done = new IStreams.DoneDisconnect() { public void doneDisconnect(IToken token, Exception error) { assert disconnect_cmds.contains(token); disconnect_cmds.remove(token); if (error != null) exit(error); if (disconnect_cmds.size() == 0 && unsubscribe_done) exit(null); } }; TestTerminals(TCFTestSuite test_suite, IChannel channel) { this.test_suite = test_suite; terminals = channel.getRemoteService(ITerminals.class); processes = channel.getRemoteService(IProcesses.class); streams = channel.getRemoteService(IStreams.class); } public void start() { if (terminals == null || streams == null) { test_suite.done(this, null); } else { terminals.addListener(listener); run(); } } private void run() { if (environment == null && processes != null) { processes.getEnvironment(new IProcesses.DoneGetEnvironment() { public void doneGetEnvironment(IToken token, Exception error, Map<String, String> environment) { if (error != null) { exit(error); } else if (environment == null) { exit(new Exception("Default process environment must not be null")); } else { TestTerminals.this.environment = environment; run(); } } }); return; } if (streams_listener == null) { final IStreams.StreamsListener l = new IStreams.StreamsListener() { public void created(String stream_type, String stream_id, String context_id) { if (!terminals.getName().equals(stream_type)) { exit(new Exception("Invalid stream type in Streams.created event: " + stream_type)); } else if (stream_id == null || stream_id.length() == 0 || stream_ids.contains(stream_id)) { exit(new Exception("Invalid stream ID in Streams.created event: " + stream_id)); } else if (terminal != null) { if (stream_id.equals(terminal.getStdInID()) || stream_id.equals(terminal.getStdOutID()) || stream_id.equals(terminal.getStdErrID())) { exit(new Exception("Invalid stream ID in Streams.created event: " + stream_id)); } else { disconnect_cmds.add(streams.disconnect(stream_id, disconnect_done)); } } else { stream_ids.add(stream_id); } } public void disposed(String stream_type, String stream_id) { } }; streams.subscribe(terminals.getName(), l, new IStreams.DoneSubscribe() { public void doneSubscribe(IToken token, Exception error) { if (error != null) { exit(error); } else { streams_listener = l; run(); } } }); return; } if (terminal == null) { String[] types = { "ansi", "vt100", null }; String[] langs = { "en_US", "en_US.UTF-8", null }; String[] env = null; if (environment != null && rnd.nextBoolean()) { int i = 0; env = new String[environment.size() + 1]; for (String s : environment.keySet()) { env[i++] = s + "=" + environment.get(s); } env[i++] = "TCF_FOO=BAR"; } terminals.launch(types[rnd.nextInt(types.length)], langs[rnd.nextInt(langs.length)], env, new ITerminals.DoneLaunch() { public void doneLaunch(IToken token, Exception error, final TerminalContext terminal) { if (error != null) { exit(error); } else if (terminal == null) { exit(new Exception("Terminal context must not be null")); } else if (terminal.getID() == null) { exit(new Exception("Terminal context ID must not be null")); } else { TestTerminals.this.terminal = terminal; for (Iterator<String> i = stream_ids.iterator(); i.hasNext();) { String stream_id = i.next(); if (stream_id.equals(terminal.getStdInID()) || stream_id.equals(terminal.getStdOutID()) || stream_id.equals(terminal.getStdErrID())) { // keep connected } else { i.remove(); disconnect_cmds.add(streams.disconnect(stream_id, disconnect_done)); } } Protocol.invokeLater(100, new Runnable() { public void run() { if (!test_suite.isActive(TestTerminals.this)) return; time_out++; if (test_suite.cancel) { exit(null); } else if (time_out < 200) { Protocol.invokeLater(100, this); } else if (!signal_sent) { exit(new Error("Timeout waiting for terminal reply. Context: " + terminal.getID())); } else if (!exited) { exit(new Error("Timeout waiting for 'Terminals.exited' event. Context: " + terminal.getID())); } else { exit(new Error("Timeout waiting for end-of-stream. Context: " + terminal.getID())); } } }); run(); } } }); return; } if (get_ctx == null) { terminals.getContext(terminal.getID(), new ITerminals.DoneGetContext() { public void doneGetContext(IToken token, Exception error, TerminalContext terminal) { if (error != null) { exit(error); } else if (terminal == null) { exit(new Exception("Terminal context must not be null")); } else if (terminal.getID() == null) { exit(new Exception("Terminal context ID must not be null")); } else if (!TestTerminals.this.terminal.getProperties().equals(terminal.getProperties())) { exit(new Exception("Invalid result of Terminal.getContext")); } else { TestTerminals.this.get_ctx = terminal; run(); } } }); return; } if (signal_list == null && processes != null && terminal.getProcessID() != null) { assert get_signals_cmd == null; get_signals_cmd = processes.getSignalList(terminal.getProcessID(), new IProcesses.DoneGetSignalList() { public void doneGetSignalList(IToken token, Exception error, Collection<Map<String,Object>> list) { assert get_signals_cmd == token; get_signals_cmd = null; if (error != null) { exit(error); } else if (list == null) { exit(new Exception("Signal list must not be null")); } else { signal_list = list; run(); } } }); return; } if (encoding == null) { String lang = terminal.getEncoding(); if (lang == null && environment != null) lang = environment.get("LC_ALL"); if (lang == null && environment != null) lang = environment.get("LANG"); if (lang == null) lang = "en_US.UTF-8"; int i = lang.indexOf('.'); int j = lang.indexOf('@'); if (i < 0) { encoding = "UTF-8"; } else if (j < i) { encoding = lang.substring(i + 1); } else { encoding = lang.substring(i + 1, j); } } if (stdout_read == null) { final String id = terminal.getStdOutID(); if (id == null) { exit(new Exception("stdout stream ID is null")); return; } stdout_read = new IStreams.DoneRead() { public void doneRead(IToken token, Exception error, int lost_size, byte[] data, boolean eos) { if (error != null) { exit(error); } else if (lost_size > 0) { exit(new Exception("Lost bytes in terminal stream")); } else { try { boolean run = false; if (data != null) { stdout_buf.append(new String(data, encoding)); if (echo_tx.size() > echo_rx.size()) { String s = echo_tx.get(echo_rx.size()); String p = "\n" + s.substring(0, 12); int n = 0; if (echo_rx.size() > 0) n = echo_rx.get(echo_rx.size() - 1); int i = stdout_buf.indexOf(p, n); if (i >= 0 && stdout_buf.length() >= i + s.length() + 4) { time_out = 0; echo_rx.add(i + 1); run = true; } } } if (!eos) { streams.read(id, 0x1000, this); } else { stdout_eos = true; run = true; } if (run) run(); } catch (Exception x) { exit(x); } } } }; streams.read(id, 0x1000, stdout_read); } if (stderr_read == null && terminal.getStdErrID() != null) { final String id = terminal.getStdErrID(); stderr_read = new IStreams.DoneRead() { public void doneRead(IToken token, Exception error, int lost_size, byte[] data, boolean eos) { if (error != null) { exit(error); } else if (lost_size > 0) { exit(new Exception("Lost bytes in terminal stream")); } else { try { if (data != null) stderr_buf.append(new String(data, encoding)); if (!eos) streams.read(id, 0x1000, this); } catch (Exception x) { exit(x); } } } }; int n = rnd.nextInt(4) + 1; for (int i = 0; i < n; i++) { streams.read(id, 0x1000, stderr_read); } } if (!delay_done) { final int n = stdout_buf.length(); Protocol.invokeLater(rnd.nextInt(250), new Runnable() { public void run() { if (n > 1 && stdout_buf.length() == n && stdout_buf.charAt(n - 1) != '\n') delay_done = true; TestTerminals.this.run(); } }); return; } if (echo_tx.size() < echo_cnt && echo_rx.size() == echo_tx.size()) { try { StringBuffer bf = new StringBuffer(); for (int i = 0; i < 0x100; i++) { bf.append((char)('A' + rnd.nextInt('Z' - 'A' + 1))); bf.append((char)('a' + rnd.nextInt('Z' - 'A' + 1))); } String s = bf.toString(); echo_tx.add(s); s = "echo " + s + '\n'; byte[] buf = s.getBytes(encoding); streams.write(terminal.getStdInID(), buf, 0, buf.length, new IStreams.DoneWrite() { public void doneWrite(IToken token, Exception error) { if (error != null) { exit(error); } else { run(); } } }); } catch (Exception x) { exit(x); } return; } if (!exited && !stdout_eos && echo_rx.size() < echo_cnt) return; if (!signal_sent) { assert !exited; if (signal_cmd == null) { int code = 0; if (signal_list != null && rnd.nextBoolean()) { for (Map<String,Object> m : signal_list) { String nm = (String)m.get(IProcesses.SIG_NAME); if (nm != null && nm.equals("SIGKILL")) { Number n = (Number)m.get(IProcesses.SIG_CODE); if (n != null) code = n.intValue(); } } if (code == 0) { for (Map<String,Object> m : signal_list) { String nm = (String)m.get(IProcesses.SIG_NAME); if (nm != null && nm.equals("SIGTERM")) { Number n = (Number)m.get(IProcesses.SIG_CODE); if (n != null) code = n.intValue(); } } } } if (code > 0) { signal_cmd = processes.signal(terminal.getProcessID(), code, new IProcesses.DoneCommand() { public void doneCommand(IToken token, Exception error) { assert signal_cmd == token; signal_cmd = null; if (error != null) { exit(error); } else { signal_sent = true; run(); } } }); } else { signal_cmd = terminals.exit(terminal.getID(), new ITerminals.DoneCommand() { public void doneCommand(IToken token, Exception error) { assert signal_cmd == token; signal_cmd = null; if (error != null) { exit(error); } else { signal_sent = true; run(); } } }); } } return; } if (exited && stdout_eos) { if (!unsubscribe_done) { if (unsubscribe_cmd == null) { unsubscribe_cmd = streams.unsubscribe(terminals.getName(), streams_listener, new IStreams.DoneUnsubscribe() { public void doneUnsubscribe(IToken token, Exception error) { unsubscribe_done = true; if (error != null) exit(error); else run(); } }); } return; } else { for (String stream_id : stream_ids) { disconnect_cmds.add(streams.disconnect(stream_id, disconnect_done)); } stream_ids.clear(); checkTerminalOutput(stdout_buf); checkTerminalOutput(stderr_buf); if (echo_rx.size() < echo_cnt) { exit(new Exception("Terminal exited before test finished")); } else { int n = 0; for (int i : echo_rx) { String s = echo_tx.get(n++); String r = stdout_buf.substring(i, i + s.length()); if (!s.equals(r)) { exit(new Exception("Invalid reply: " + r + "\nExpected: " + s)); } } } } } } private void checkTerminalOutput(StringBuffer bf) { if (bf.indexOf("Cannot start") >= 0) { exit(new Exception("Unexpected terminal output:\n" + bf)); } } private void exit(Throwable x) { if (!test_suite.isActive(this)) return; if (terminals != null) terminals.removeListener(listener); test_suite.done(this, x); } public boolean canResume(String id) { return true; } }