/* * Copyright 2015 Red Hat, Inc. * * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. * * * Copyright (c) 2015 The original author or authors * ------------------------------------------------------ * All rights reserved. This program and the accompanying materials * are made available under the terms of the Eclipse Public License v1.0 * and Apache License v2.0 which accompanies this distribution. * * The Eclipse Public License is available at * http://www.eclipse.org/legal/epl-v10.html * * The Apache License v2.0 is available at * http://www.opensource.org/licenses/apache2.0.php * * You may elect to redistribute this code under either of these licenses. * */ package io.vertx.ext.shell; import io.termd.core.readline.Keymap; import io.termd.core.tty.TtyEvent; import io.vertx.core.Context; import io.vertx.core.Vertx; import io.vertx.ext.shell.command.Command; import io.vertx.ext.shell.command.CommandBuilder; import io.vertx.ext.shell.command.CommandProcess; import io.vertx.ext.shell.command.base.Sleep; import io.vertx.ext.shell.support.TestCommands; import io.vertx.ext.shell.system.impl.InternalCommandManager; import io.vertx.ext.shell.system.Job; import io.vertx.ext.shell.system.ExecStatus; import io.vertx.ext.shell.impl.ShellImpl; import io.vertx.ext.shell.support.TestTtyConnection; import io.vertx.ext.shell.system.impl.JobImpl; import io.vertx.ext.shell.term.impl.TermImpl; import io.vertx.ext.unit.Async; import io.vertx.ext.unit.TestContext; import io.vertx.ext.unit.junit.Repeat; import io.vertx.ext.unit.junit.RepeatRule; import io.vertx.ext.unit.junit.VertxUnitRunner; import org.junit.After; import org.junit.Before; import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import java.util.Arrays; import java.util.Collections; import java.util.concurrent.CountDownLatch; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicReference; /** * @author <a href="mailto:julien@julienviet.com">Julien Viet</a> */ @RunWith(VertxUnitRunner.class) public class ShellTest { Vertx vertx; TestCommands commands; ShellServer server; @Before public void before() { vertx = Vertx.vertx(); commands = new TestCommands(vertx); server = ShellServer.create(vertx).registerCommandResolver(commands); } @After public void after(TestContext context) { vertx.close(context.asyncAssertSuccess()); } private ShellImpl createShell(TestTtyConnection conn) { return new ShellImpl(new TermImpl(vertx, conn), new InternalCommandManager(commands)); } @Test public void testVertx(TestContext context) { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async done = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { context.assertEquals(vertx, process.vertx()); done.complete(); })); conn.read("foo\r"); } @Test public void testExecuteProcess(TestContext context) { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); context.assertNull(shell.jobController().foregroundJob()); context.assertEquals(Collections.emptySet(), shell.jobController().jobs()); Async async = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { context.assertEquals(1, shell.jobController().jobs().size()); Job job = shell.jobController().getJob(1); context.assertEquals(job, shell.jobController().foregroundJob()); context.assertEquals("foo", job.line()); context.assertEquals(ExecStatus.RUNNING, job.status()); async.complete(); })); conn.read("foo\r"); } @Test public void testHandleReadlineBuffered(TestContext context) { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async async = context.async(); commands.add(CommandBuilder.command("_not_consumed").processHandler(process -> { async.complete(); })); commands.add(CommandBuilder.command("read").processHandler(process -> { StringBuilder buffer = new StringBuilder(); process.stdinHandler(line -> { buffer.append(line); if (buffer.toString().equals("the_line")) { process.end(); } }); })); conn.read("read\rthe_line_not_consumed\r"); } @Test public void testExecuteReadlineBuffered(TestContext context) { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async async = context.async(); AtomicInteger count = new AtomicInteger(); commands.add(CommandBuilder.command("read").processHandler(process -> { if (count.incrementAndGet() == 2) { async.complete(); } process.end(0); })); conn.read("read\rread\r"); } @Test public void testSuspendProcess(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async done = context.async(); Async latch2 = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { Job job = shell.jobController().getJob(1); process.suspendHandler(v -> { context.assertEquals(ExecStatus.STOPPED, job.status()); context.assertNull(shell.jobController().foregroundJob()); done.complete(); }); latch2.complete(); })); conn.read("foo\r"); latch2.awaitSuccess(10000); conn.sendEvent(TtyEvent.SUSP); } @Test public void testSuspendedProcessDisconnectedFromTty(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async done = context.async(); Async latch1 = context.async(); Async latch2 = context.async(); Async latch3 = context.async(); commands.add(CommandBuilder.command("read").processHandler(process -> { process.stdinHandler(line -> { context.fail("Should not process line " + line); }); process.suspendHandler(v -> { try { process.write(""); context.fail(); } catch (IllegalStateException ignore) { } latch2.countDown(); }); latch1.countDown(); })); commands.add(CommandBuilder.command("wait").processHandler(process -> { // Do nothing, this command is used to escape from readline and make // sure that the read data is not sent to the stopped command latch3.countDown(); process.suspendHandler(v -> { process.end(0); }); })); commands.add(CommandBuilder.command("end").processHandler(process -> { done.complete(); })); conn.read("read\r"); latch1.awaitSuccess(10000); conn.sendEvent(TtyEvent.SUSP); latch2.awaitSuccess(10000); conn.read("wait\r"); latch3.awaitSuccess(10000); conn.sendEvent(TtyEvent.SUSP); conn.read("end\r"); } @Test public void testResumeProcessToForeground(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); CountDownLatch latch1 = new CountDownLatch(1); CountDownLatch latch2 = new CountDownLatch(1); CountDownLatch latch3 = new CountDownLatch(1); CountDownLatch latch4 = new CountDownLatch(1); commands.add(CommandBuilder.command("foo").processHandler(process -> { Job job = shell.jobController().getJob(1); context.assertTrue(process.isForeground()); process.suspendHandler(v -> { context.assertFalse(process.isForeground()); context.assertEquals(0L, latch1.getCount()); try { process.write(""); context.fail(); } catch (IllegalStateException ignore) { } latch2.countDown(); }); process.resumeHandler(v -> { context.assertTrue(process.isForeground()); context.assertEquals(0L, latch2.getCount()); context.assertEquals(ExecStatus.RUNNING, job.status()); process.write(""); context.assertEquals(job, shell.jobController().foregroundJob()); conn.out().setLength(0); process.write("resumed"); latch3.countDown(); }); process.stdinHandler(txt -> { context.assertEquals(0L, latch3.getCount()); context.assertEquals("hello", txt); latch4.countDown(); }); latch1.countDown(); })); conn.read("foo\r"); latch1.await(10, TimeUnit.SECONDS); conn.sendEvent(TtyEvent.SUSP); latch2.await(10, TimeUnit.SECONDS); conn.read("fg\r"); latch3.await(10, TimeUnit.SECONDS); conn.read("hello"); latch4.await(10, TimeUnit.SECONDS); conn.assertWritten("resumed"); } @Test public void testResumeProcessToBackground(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async latch1 = context.async(); Async latch2 = context.async(); Async latch3 = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { Job job = shell.jobController().getJob(1); context.assertTrue(process.isForeground()); process.suspendHandler(v -> { context.assertFalse(process.isForeground()); context.assertEquals(0, latch1.count()); try { process.write(""); context.fail(); } catch (IllegalStateException ignore) { } latch2.countDown(); }); process.resumeHandler(v -> { context.assertFalse(process.isForeground()); context.assertEquals(0, latch2.count()); context.assertEquals(ExecStatus.RUNNING, job.status()); process.write(""); context.assertNull(shell.jobController().foregroundJob()); latch3.awaitSuccess(2000); process.write("resumed"); }); process.stdinHandler(txt -> { context.fail(); }); latch1.countDown(); })); conn.read("foo\r"); latch1.awaitSuccess(10000); conn.sendEvent(TtyEvent.SUSP); latch2.awaitSuccess(10000); conn.out().setLength(0); conn.read("bg\r"); conn.assertWritten("bg\n[1]+ Running foo\n% "); latch3.countDown(); conn.assertWritten("resumed"); conn.read("hello"); conn.assertWritten("hello"); } @Test public void backgroundToForeground(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async latch1 = context.async(); Async latch2 = context.async(); Async latch3 = context.async(); Async async = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { process.suspendHandler(v -> { context.assertFalse(process.isForeground()); context.assertEquals(1, latch2.count()); latch2.countDown(); }); process.resumeHandler(v -> { context.assertFalse(process.isForeground()); }); process.foregroundHandler(v -> { context.assertTrue(process.isForeground()); latch3.countDown(); }); process.stdinHandler(line -> { async.complete(); }); latch1.countDown(); })); conn.read("foo\r"); latch1.awaitSuccess(2000); conn.sendEvent(TtyEvent.SUSP); latch2.awaitSuccess(2000); conn.read("bg\r"); context.assertNull(shell.jobController().foregroundJob()); conn.read("fg\r"); latch3.await(); context.assertNotNull(shell.jobController().foregroundJob()); context.assertEquals(shell.jobController().getJob(1), shell.jobController().foregroundJob()); conn.read("whatever"); } @Test public void testExecuteBufferedCommand(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl adapter = createShell(conn); adapter.init().readline(); CountDownLatch latch = new CountDownLatch(1); Async done = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { context.assertEquals(null, conn.checkWritten("% foo\n")); conn.read("bar"); process.end(); latch.countDown(); })); commands.add(CommandBuilder.command("bar").processHandler(process -> { context.assertEquals(null, conn.checkWritten("\n")); done.complete(); })); conn.read("foo\r"); latch.await(10, TimeUnit.SECONDS); context.assertNull(conn.checkWritten("bar")); context.assertNull(conn.checkWritten("% bar")); conn.read("\r"); } @Test public void testEchoCharsDuringExecute(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); Async async = context.async(); commands.add(CommandBuilder.command("foo").processHandler(process -> { context.assertEquals(null, conn.checkWritten("% foo\n")); conn.read("\u0007"); context.assertNull(conn.checkWritten("^G")); conn.read("A"); context.assertNull(conn.checkWritten("A")); conn.read("\r"); context.assertNull(conn.checkWritten("\n")); conn.read("\003"); context.assertNull(conn.checkWritten("^C")); conn.read("\004"); context.assertNull(conn.checkWritten("^D")); async.complete(); })); conn.read("foo\r"); } public void testExit(TestContext context) throws Exception { commands.add(Command.create(vertx, Sleep.class)); for (String cmd : Arrays.asList("exit", "logout")) { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl adapter = createShell(conn); adapter.init().readline(); conn.read("sleep 10000\r"); long now = System.currentTimeMillis(); while (adapter.jobController().jobs().size() == 0 || ((JobImpl) adapter.jobController().jobs().iterator().next()).actualStatus() != ExecStatus.RUNNING) { context.assertTrue(System.currentTimeMillis() - now < 2000); Thread.sleep(1); } conn.sendEvent(TtyEvent.SUSP); conn.read("bg\r"); conn.read(cmd + "\r"); context.assertTrue(conn.isClosed()); now = System.currentTimeMillis(); while (adapter.jobController().jobs().size() > 0) { context.assertTrue((System.currentTimeMillis() - now) < 2000); Thread.sleep(10); } } } @Test public void testEOF(TestContext context) throws Exception { TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); conn.read("\u0004"); context.assertTrue(conn.getCloseLatch().await(2, TimeUnit.SECONDS)); } @Test public void testAbc(TestContext context) throws Exception { Async barLatch = context.async(); Async endLatch = context.async(); commands.add(CommandBuilder. command("foo"). processHandler(process -> { process.stdinHandler(cp -> { context.fail(); }); process.endHandler(v -> { barLatch.complete(); } ); process.end(); }). build(vertx)); commands.add(CommandBuilder. command("bar"). processHandler(process -> { process.endHandler(v -> { endLatch.complete(); }); process.end(); }). build(vertx)); TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); conn.read("foo\r"); barLatch.awaitSuccess(2000); conn.read("bar\r"); } @Test public void testSetStdinOnResumeToForeground(TestContext context) throws Exception { Async fooRunning = context.async(); Async fooSusp = context.async(); Async fooResumed = context.async(); Async readLatch = context.async(); commands.add( CommandBuilder.command("foo").processHandler(process -> { process.suspendHandler(v -> fooSusp.complete()); process.resumeHandler(v -> fooResumed.complete()); process.stdinHandler(line -> { context.assertEquals("foo_msg", line); readLatch.complete(); }); fooRunning.complete(); })); TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); conn.read("foo\r"); fooRunning.awaitSuccess(2000); conn.sendEvent(TtyEvent.SUSP); fooSusp.awaitSuccess(2000); conn.read("fg\r"); fooResumed.awaitSuccess(2000); conn.read("foo_msg"); } @Test public void testSetStdinOnBackgroundToForeground(TestContext context) throws Exception { Async fooRunning = context.async(); Async fooSusp = context.async(); Async fooResumed = context.async(); Async fooToForeground = context.async(); Async readLatch = context.async(); commands.add( CommandBuilder.command("foo").processHandler(process -> { process.suspendHandler(v -> fooSusp.complete()); process.resumeHandler(v -> fooResumed.complete()); process.foregroundHandler(v -> fooToForeground.complete()); process.stdinHandler(line -> { context.assertEquals("foo_msg", line); readLatch.complete(); }); fooRunning.complete(); })); TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); conn.read("foo\r"); fooRunning.awaitSuccess(2000); conn.sendEvent(TtyEvent.SUSP); fooSusp.awaitSuccess(2000); conn.read("bg\r"); fooResumed.awaitSuccess(2000); conn.read("fg\r"); fooToForeground.awaitSuccess(20000000); conn.read("foo_msg"); } @Test public void testEndInBackground(TestContext context) throws Exception { Async fooRunning = context.async(); Async fooSusp = context.async(); Async fooResumed = context.async(); Async endLatch = context.async(); AtomicReference<CommandProcess> cmdProcess = new AtomicReference<>(); AtomicReference<Context> cmdContext = new AtomicReference<>(); commands.add( CommandBuilder.command("foo").processHandler(process -> { cmdProcess.set(process); cmdContext.set(Vertx.currentContext()); process.suspendHandler(v -> fooSusp.complete()); process.resumeHandler(v -> fooResumed.complete()); process.endHandler(v -> { endLatch.complete(); }); fooRunning.complete(); })); TestTtyConnection conn = new TestTtyConnection(vertx); ShellImpl shell = createShell(conn); shell.init().readline(); conn.read("foo\r"); fooRunning.awaitSuccess(2000); conn.sendEvent(TtyEvent.SUSP); fooSusp.awaitSuccess(2000); conn.read("bg\r"); fooResumed.awaitSuccess(2000); cmdContext.get().runOnContext(v -> { cmdProcess.get().end(); }); long now = System.currentTimeMillis(); while (shell.jobController().jobs().size() > 0) { context.assertTrue(System.currentTimeMillis() - now < 2000); Thread.sleep(1); } conn.read("exit\r"); conn.getCloseLatch().await(2, TimeUnit.SECONDS); } }