From b8768d164c8190aab7ac9727043ea3518dc8a5fa Mon Sep 17 00:00:00 2001 From: Konstantin Kolosovsky Date: Mon, 4 Nov 2013 23:16:54 +0400 Subject: [PATCH] svn: Implemented basic svn+ssh authentication using terminal (under svn.use.terminal registry key) --- .../src/misc/registry.properties | 1 + .../idea/svn/commandLine/CommandExecutor.java | 6 + .../idea/svn/commandLine/CommandRuntime.java | 16 ++- .../InteractiveCommandListener.java | 26 ++++ .../svn/commandLine/TerminalExecutor.java | 97 ++++++++++++++ .../commandLine/TerminalProcessHandler.java | 123 ++++++++++++++++++ .../svn/commandLine/TerminalSshModule.java | 72 ++++++++++ plugins/svn4idea/svn4idea.iml | 3 + 8 files changed, 342 insertions(+), 2 deletions(-) create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/InteractiveCommandListener.java create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalExecutor.java create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalProcessHandler.java create mode 100644 plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalSshModule.java diff --git a/platform/platform-resources-en/src/misc/registry.properties b/platform/platform-resources-en/src/misc/registry.properties index e3581ee694ec..7f7b034a6c9d 100644 --- a/platform/platform-resources-en/src/misc/registry.properties +++ b/platform/platform-resources-en/src/misc/registry.properties @@ -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 diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java index 3cec1c5ecce8..2f96710693dc 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java @@ -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 diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandRuntime.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandRuntime.java index 4e366e857f1d..f7362854fe9d 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandRuntime.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandRuntime.java @@ -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 diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/InteractiveCommandListener.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/InteractiveCommandListener.java new file mode 100644 index 000000000000..a9dc33a7061e --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/InteractiveCommandListener.java @@ -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); +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalExecutor.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalExecutor.java new file mode 100644 index 000000000000..ea8544544bf1 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalExecutor.java @@ -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 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 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 escapeArguments(List collection) { + // TODO: Add additional checks like in java.lang.ProcessImpl constructor + return ContainerUtil.map(collection, new Function() { + @Override + public String fun(String s) { + return s.contains(" ") ? "\"" + s + "\"" : s; + } + }); + } +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalProcessHandler.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalProcessHandler.java new file mode 100644 index 000000000000..5f3a012f8409 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalProcessHandler.java @@ -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 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); + } + } +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalSshModule.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalSshModule.java new file mode 100644 index 000000000000..e985824ecbc9 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalSshModule.java @@ -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; + } +} diff --git a/plugins/svn4idea/svn4idea.iml b/plugins/svn4idea/svn4idea.iml index 53dd66166d64..1765d1aede98 100644 --- a/plugins/svn4idea/svn4idea.iml +++ b/plugins/svn4idea/svn4idea.iml @@ -76,6 +76,9 @@ + + +