svn: Implemented basic svn+ssh authentication using terminal (under svn.use.terminal registry key)

This commit is contained in:
Konstantin Kolosovsky
2013-11-04 23:16:54 +04:00
parent 13a217b643
commit b8768d164c
8 changed files with 342 additions and 2 deletions
@@ -304,6 +304,7 @@ testng_sm_runner=false
show.flex.debug.design.view=false
git.new.log=true
svn.use.terminal=false
completion.enable.relevant.method.chain.suggestions=false
ide.mac.message.sheets.java.emulation=false
@@ -136,6 +136,12 @@ public class CommandExecutor {
return outputAdapter.getOutput().getStderr();
}
// TODO: Carefully here - do not modify command from threads other than the one started command execution
@NotNull
public Command getCommand() {
return myCommand;
}
/**
* Wait for process termination
* @param timeout
@@ -17,6 +17,7 @@ package org.jetbrains.idea.svn.commandLine;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Condition;
import com.intellij.openapi.util.registry.Registry;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
@@ -170,8 +171,19 @@ public class CommandRuntime {
@NotNull
private CommandExecutor newExecutor(@NotNull Command command) {
command.putIfNotPresent("--non-interactive");
return new CommandExecutor(exePath, command);
final CommandExecutor executor;
if (!Registry.is("svn.use.terminal")) {
command.putIfNotPresent("--non-interactive");
executor = new CommandExecutor(exePath, command);
}
else {
command.put("--force-interactive");
executor = new TerminalExecutor(exePath, command);
((TerminalExecutor)executor).addInteractiveListener(new TerminalSshModule(executor, myAuthCallback));
}
return executor;
}
@NotNull
@@ -0,0 +1,26 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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.jetbrains.idea.svn.commandLine;
import com.intellij.openapi.util.Key;
/**
* @author Konstantin Kolosovsky.
*/
public interface InteractiveCommandListener {
boolean handlePrompt(String line, Key outputType);
}
@@ -0,0 +1,97 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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.jetbrains.idea.svn.commandLine;
import com.intellij.execution.CommandLineUtil;
import com.intellij.execution.ExecutionException;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.openapi.util.SystemInfo;
import com.intellij.util.ArrayUtil;
import com.intellij.util.Function;
import com.intellij.util.containers.ContainerUtil;
import com.pty4j.PtyProcess;
import org.jetbrains.annotations.NonNls;
import org.jetbrains.annotations.NotNull;
import java.io.IOException;
import java.util.List;
/**
* @author Konstantin Kolosovsky.
*/
public class TerminalExecutor extends CommandExecutor {
// max available value is 480
// if greater value is provided than the default value of 80 will be assumed
// this could provide unnecessary line breaks and thus could break parsing logic
private static final int TERMINAL_WINDOW_MAX_COLUMNS = 480;
static {
if (SystemInfo.isWindows) {
System.setProperty("win.pty.cols", String.valueOf(TERMINAL_WINDOW_MAX_COLUMNS));
}
}
private final List<InteractiveCommandListener> myInteractiveListeners = ContainerUtil.createLockFreeCopyOnWriteList();
public TerminalExecutor(@NotNull @NonNls String exePath, @NotNull Command command) {
super(exePath, command);
}
public void addInteractiveListener(@NotNull InteractiveCommandListener listener) {
myInteractiveListeners.add(listener);
}
@Override
protected void startHandlingStreams() {
for (InteractiveCommandListener listener : myInteractiveListeners) {
((TerminalProcessHandler)myHandler).addInteractiveListener(listener);
}
super.startHandlingStreams();
}
@NotNull
@Override
protected OSProcessHandler createProcessHandler() {
return new TerminalProcessHandler(myProcess);
}
@NotNull
@Override
protected Process createProcess() throws ExecutionException {
List<String> parameters =
escapeArguments(CommandLineUtil.toCommandLine(myCommandLine.getExePath(), myCommandLine.getParametersList().getList()));
try {
return PtyProcess
.exec(ArrayUtil.toStringArray(parameters), myCommandLine.getEnvironment(), myCommandLine.getWorkDirectory().getAbsolutePath());
}
catch (IOException e) {
throw new ExecutionException(e);
}
}
private static List<String> escapeArguments(List<String> collection) {
// TODO: Add additional checks like in java.lang.ProcessImpl constructor
return ContainerUtil.map(collection, new Function<String, String>() {
@Override
public String fun(String s) {
return s.contains(" ") ? "\"" + s + "\"" : s;
}
});
}
}
@@ -0,0 +1,123 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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.jetbrains.idea.svn.commandLine;
import com.intellij.execution.process.CapturingProcessAdapter;
import com.intellij.execution.process.OSProcessHandler;
import com.intellij.execution.process.ProcessEvent;
import com.intellij.execution.process.ProcessOutputTypes;
import com.intellij.openapi.util.Key;
import com.intellij.openapi.util.text.StringUtil;
import com.intellij.util.containers.ContainerUtil;
import org.jetbrains.annotations.NotNull;
import java.util.List;
/**
* @author Konstantin Kolosovsky.
*/
public class TerminalProcessHandler extends OSProcessHandler {
// see http://en.wikipedia.org/wiki/ANSI_escape_code
private static final String NON_CSI_ESCAPE_CODE = "\u001B.[@-_]";
private static final String CSI_ESCAPE_CODE = "\u001B\\[(.*?)[@-~]";
private final List<InteractiveCommandListener> myInteractiveListeners = ContainerUtil.createLockFreeCopyOnWriteList();
private final CapturingProcessAdapter terminalOutputCapturer = new CapturingProcessAdapter();
private final StringBuilder outputLine = new StringBuilder();
private final StringBuilder errorLine = new StringBuilder();
public TerminalProcessHandler(@NotNull Process process) {
super(process);
}
public void addInteractiveListener(@NotNull InteractiveCommandListener listener) {
myInteractiveListeners.add(listener);
}
@Override
public void notifyTextAvailable(String text, Key outputType) {
terminalOutputCapturer.onTextAvailable(new ProcessEvent(this, text), outputType);
// filter terminal escape codes - they are presented in the output for windows platform
String filteredText = text.replaceAll(CSI_ESCAPE_CODE, "").replaceAll(NON_CSI_ESCAPE_CODE, "");
// trim leading '\r' symbols - as they break xml parsing logic
filteredText = StringUtil.trimLeading(filteredText);
if (!StringUtil.isEmpty(filteredText)) {
StringBuilder lastLine = getLastLineFor(outputType);
String currentLine = lastLine.append(filteredText).toString();
lastLine.setLength(0);
// check if current line presents some interactive output
boolean handled = false;
for (InteractiveCommandListener listener : myInteractiveListeners) {
handled |= listener.handlePrompt(currentLine, outputType);
}
if (!handled) {
notify(currentLine, outputType, lastLine);
}
}
}
private void notify(@NotNull String text, @NotNull Key outputType, @NotNull StringBuilder lastLine) {
// for windows platform output is assumed in format suitable for terminal emulator
// for instance, same text could be returned twice with '\r' symbol in between (so in emulator output we'll still see correct
// text without duplication)
// because of this we manually process '\r' occurrences to get correct output
text = removeAllBeforeCaretReturn(text);
// text is not more than one line - either one line or part of the line
if (StringUtil.endsWith(text, "\n")) {
// we have full line - notify listeners
super.notifyTextAvailable(text, outputType);
}
else {
// save line part to lastLine
lastLine.append(text);
}
}
private static String removeAllBeforeCaretReturn(@NotNull String line) {
int caretReturn = line.lastIndexOf("\r");
while (caretReturn >= 0) {
if (caretReturn + 1 < line.length() && line.charAt(caretReturn + 1) != '\n') {
// next symbol is not '\n' - we should not treat text before found caret return symbol
line = line.substring(caretReturn + 1);
break;
}
caretReturn = line.lastIndexOf("\r", caretReturn - 1);
}
return line;
}
@NotNull
private StringBuilder getLastLineFor(Key outputType) {
if (ProcessOutputTypes.STDERR.equals(outputType)) {
return errorLine;
}
else if (ProcessOutputTypes.STDOUT.equals(outputType)) {
return outputLine;
}
else {
throw new IllegalArgumentException("Unknown process output type " + outputType);
}
}
}
@@ -0,0 +1,72 @@
/*
* Copyright 2000-2013 JetBrains s.r.o.
*
* 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.jetbrains.idea.svn.commandLine;
import com.intellij.openapi.diagnostic.Logger;
import com.intellij.openapi.util.Key;
import org.jetbrains.annotations.NotNull;
import org.tmatesoft.svn.core.auth.ISVNAuthenticationManager;
import org.tmatesoft.svn.core.auth.SVNAuthentication;
import org.tmatesoft.svn.core.auth.SVNSSHAuthentication;
/**
* @author Konstantin Kolosovsky.
*/
public class TerminalSshModule extends LineCommandAdapter implements CommandRuntimeModule, InteractiveCommandListener {
private static final Logger LOG = Logger.getInstance(TerminalSshModule.class);
@NotNull private final CommandExecutor myExecutor;
@NotNull private final AuthenticationCallback myAuthCallback;
// TODO: Do not accept executor here and make it as command runtime module
public TerminalSshModule(@NotNull CommandExecutor executor, @NotNull AuthenticationCallback authCallback) {
myExecutor = executor;
myAuthCallback = authCallback;
}
@Override
public void onStart(@NotNull Command command) throws SvnBindException {
}
@Override
public boolean handlePrompt(String line, Key outputType) {
boolean result = false;
if (line.toLowerCase().contains("enter passphrase for key")) {
result = handlePassphrase();
}
return result;
}
private boolean handlePassphrase() {
SVNAuthentication authentication =
myAuthCallback.requestCredentials(myExecutor.getCommand().getRepositoryUrl(), ISVNAuthenticationManager.SSH);
if (authentication != null && authentication instanceof SVNSSHAuthentication) {
try {
myExecutor.write(String.format("%s\n", ((SVNSSHAuthentication)authentication).getPassphrase()));
return true;
}
catch (SvnBindException e) {
// TODO: handle this more carefully
LOG.info(e);
}
}
return false;
}
}
+3
View File
@@ -76,6 +76,9 @@
</SOURCES>
</library>
</orderEntry>
<orderEntry type="library" name="jna" level="project" />
<orderEntry type="library" name="pty4j" level="project" />
<orderEntry type="library" name="purejavacomm" level="project" />
</component>
</module>