Handle CTRL-C while editing

Fixes #22

Use ResultHandler for exits
Add unit test for CTRL-C capturing
This commit is contained in:
Eric Bottard
2017-05-17 17:15:50 +02:00
parent 929795c318
commit 3313140ecb
5 changed files with 152 additions and 15 deletions

View File

@@ -16,7 +16,6 @@
package org.springframework.shell2;
import java.io.Closeable;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
@@ -39,6 +38,7 @@ import org.jline.reader.Highlighter;
import org.jline.reader.LineReader;
import org.jline.reader.LineReaderBuilder;
import org.jline.reader.ParsedLine;
import org.jline.reader.UserInterruptException;
import org.jline.reader.impl.DefaultParser;
import org.jline.terminal.Terminal;
import org.jline.utils.AttributedString;
@@ -48,25 +48,30 @@ import org.jline.utils.AttributedStyle;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.shell2.result.ResultHandlers;
import org.springframework.stereotype.Component;
import org.springframework.util.ReflectionUtils;
/**
* Created by ericbottard on 26/11/15.
* Main component implementing a REPL using JLine.
*
* <p>Discovers {@link MethodTarget}s at startup and hands off execution of commands according
* to the parsed JLine buffer.</p>
*
* @author Eric Bottard
* @author Florent Biville
*/
@Component
public class JLineShell implements Shell {
@Autowired
private ResultHandlers resultHandlers;
ResultHandlers resultHandlers = new ResultHandlers();
@Autowired
private ApplicationContext applicationContext;
private Map<String, MethodTarget> methodTargets = new HashMap<>();
private LineReader lineReader;
LineReader lineReader;
@Autowired
private Terminal terminal;
@@ -123,7 +128,16 @@ public class JLineShell implements Shell {
public void run() throws IOException {
while (true) {
lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(terminal));
try {
lineReader.readLine(new AttributedString("shell:>", AttributedStyle.DEFAULT.foreground(AttributedStyle.YELLOW)).toAnsi(terminal));
}
catch (UserInterruptException e) {
if (e.getPartialLine().isEmpty()) {
resultHandlers.handleResult(new ExitRequest(1));
} else {
continue;
}
}
String separator = "";
StringBuilder candidateCommand = new StringBuilder();
MethodTarget methodTarget = null;
@@ -147,19 +161,12 @@ public class JLineShell implements Shell {
List<String> wordsForArgs = words.subList(wordsUsedForCommandKey, words.size());
Method method = methodTarget.getMethod();
Object result = null;
try {
Object[] args = resolveArgs(method, wordsForArgs);
validateArgs(args, methodTarget);
result = ReflectionUtils.invokeMethod(method, methodTarget.getBean(), args);
}
catch (ExitRequest e) {
if (applicationContext instanceof Closeable) {
((Closeable) applicationContext).close();
System.exit(e.status());
}
}
catch (Exception e) {
result = e;
}

View File

@@ -1,5 +1,5 @@
/*
* Copyright 2015 the original author or authors.
* Copyright 2017 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -14,7 +14,7 @@
* limitations under the License.
*/
package org.springframework.shell2.result;
package org.springframework.shell2;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
@@ -23,6 +23,7 @@ import java.util.Map;
import java.util.Set;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell2.result.ResultHandler;
import org.springframework.stereotype.Component;
/**

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2017 the original author or authors.
*
* 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 org.springframework.shell2.result;
import java.io.Closeable;
import java.io.IOException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.shell2.ExitRequest;
import org.springframework.stereotype.Component;
/**
* Intercepts {@link org.springframework.shell2.ExitRequest} exceptions and gracefully exits the running process.
*
* @author Eric Bottard
*/
@Component
public class ExitRequestResultHandler implements ResultHandler<ExitRequest> {
@Autowired
private ApplicationContext applicationContext;
@Override
public void handleResult(ExitRequest result) {
if (applicationContext instanceof Closeable) {
try {
((Closeable) applicationContext).close();
}
catch (IOException e) {
// ignore
}
System.exit(result.status());
}
}
}

View File

@@ -17,6 +17,7 @@
package org.springframework.shell2.result;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.shell2.ResultHandlers;
import org.springframework.stereotype.Component;
/**

View File

@@ -0,0 +1,77 @@
/*
* Copyright 2017 the original author or authors.
*
* 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 org.springframework.shell2;
import java.util.Collections;
import org.jline.reader.LineReader;
import org.jline.reader.UserInterruptException;
import org.junit.Test;
import org.mockito.Mockito;
import org.springframework.shell2.result.ResultHandler;
import static org.hamcrest.CoreMatchers.is;
import static org.junit.Assert.assertThat;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
/**
* Unit tests for {@link JLineShell}.
*
* @author Eric Bottard
*/
public class JLineShellTest {
private JLineShell shell = new JLineShell();
@Test
public void testCtrlCInterception() throws Exception {
shell.lineReader = mock(LineReader.class);
AssertingExitRequestResultHandler resultHandler = new AssertingExitRequestResultHandler();
shell.resultHandlers.setResultHandlers(Collections.singleton(resultHandler));
when(shell.lineReader.readLine(Mockito.<String>any())).thenThrow(
new UserInterruptException("some input"),
new UserInterruptException("")
);
try {
shell.run();
}
catch (BreakControlFlow breakControlFlow) {
}
assertThat(resultHandler.exited, is(true));
}
private static class AssertingExitRequestResultHandler implements ResultHandler<ExitRequest> {
private boolean exited;
@Override
public void handleResult(ExitRequest result) {
exited = true;
throw new BreakControlFlow();
}
}
private static class BreakControlFlow extends RuntimeException {
}
}