diff --git a/platform/platform-resources-en/src/misc/registry.properties b/platform/platform-resources-en/src/misc/registry.properties index 67ef53eb3c80..731835d01003 100644 --- a/platform/platform-resources-en/src/misc/registry.properties +++ b/platform/platform-resources-en/src/misc/registry.properties @@ -309,6 +309,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/SvnUtil.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnUtil.java index 073bf57a48e3..ce734fd683fd 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnUtil.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/SvnUtil.java @@ -15,6 +15,7 @@ */ package org.jetbrains.idea.svn; +import com.intellij.notification.NotificationType; import com.intellij.openapi.application.ApplicationManager; import com.intellij.openapi.diagnostic.Logger; import com.intellij.openapi.progress.ProcessCanceledException; @@ -25,11 +26,13 @@ import com.intellij.openapi.util.Computable; import com.intellij.openapi.util.Pair; import com.intellij.openapi.util.Ref; import com.intellij.openapi.util.io.FileUtil; +import com.intellij.openapi.util.io.FileUtilRt; import com.intellij.openapi.util.text.StringUtil; import com.intellij.openapi.vcs.AbstractVcsHelper; import com.intellij.openapi.vcs.VcsException; import com.intellij.openapi.vcs.changes.Change; import com.intellij.openapi.vcs.changes.ChangesUtil; +import com.intellij.openapi.vcs.ui.VcsBalloonProblemNotifier; import com.intellij.openapi.vfs.LocalFileSystem; import com.intellij.openapi.vfs.VfsUtilCore; import com.intellij.openapi.vfs.VirtualFile; @@ -57,6 +60,7 @@ import org.tmatesoft.svn.core.wc2.SvnOperationFactory; import org.tmatesoft.svn.core.wc2.SvnTarget; import java.io.File; +import java.nio.channels.NonWritableChannelException; import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -369,30 +373,34 @@ public class SvnUtil { */ @NotNull public static WorkingCopyFormat getFormat(final File path) { - int format = 0; + WorkingCopyFormat result = null; File dbFile = resolveDatabase(path); if (dbFile != null) { - SqlJetDb db = null; - try { - db = SqlJetDb.open(dbFile, false); - format = db.getOptions().getUserVersion(); - } - catch (SqlJetException e) { - LOG.error(e); - } finally { - if (db != null) { - try { - db.close(); - } - catch (SqlJetException e) { - LOG.error(e); - } - } + result = FileUtilRt.doIOOperation(new WorkingCopyFormatOperation(dbFile)); + + if (result == null) { + notifyDatabaseError(); } } - return WorkingCopyFormat.getInstance(format); + return result != null ? result : WorkingCopyFormat.UNKNOWN; + } + + private static void close(@Nullable SqlJetDb db) { + if (db != null) { + try { + db.close(); + } + catch (SqlJetException e) { + notifyDatabaseError(); + } + } + } + + private static void notifyDatabaseError() { + VcsBalloonProblemNotifier.NOTIFICATION_GROUP + .createNotification("Some errors occurred while accessing svn working copy database.", NotificationType.ERROR).notify(null); } private static File resolveDatabase(final File path) { @@ -803,4 +811,40 @@ public class SvnUtil { // thrown when getting info from repository for non-existent item - like HEAD revision for deleted file SVNErrorCode.ILLEGAL_TARGET.equals(code); } + + private static class WorkingCopyFormatOperation implements FileUtilRt.RepeatableIOOperation { + @NotNull private final File myDbFile; + + public WorkingCopyFormatOperation(@NotNull File dbFile) { + myDbFile = dbFile; + } + + @Nullable + @Override + public WorkingCopyFormat execute(boolean lastAttempt) { + // TODO: rewrite it using sqlite jdbc driver + SqlJetDb db = null; + WorkingCopyFormat result = null; + try { + // "write" access is requested here for now as workaround - see some details + // in https://code.google.com/p/sqljet/issues/detail?id=25 and http://issues.tmatesoft.com/issue/SVNKIT-418. + // BUSY error is currently handled same way as others. + db = SqlJetDb.open(myDbFile, true); + result = WorkingCopyFormat.getInstance(db.getOptions().getUserVersion()); + } + catch (NonWritableChannelException e) { + // Such exceptions could be thrown when db is opened in "read" mode, but the db file is readonly (for instance, locked + // by other process). See links above for some details. + // handle this exception type separately - not to break execution flow + LOG.info(e); + } + catch (SqlJetException e) { + LOG.info(e); + } + finally { + close(db); + } + return result; + } + } } diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/Command.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/Command.java index e5d7dd6c4263..ea623f4d8050 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/Command.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/Command.java @@ -41,6 +41,12 @@ public class Command { myParameters.addAll(parameters); } + public void putIfNotPresent(@NotNull String parameter) { + if (!myParameters.contains(parameter)) { + myParameters.add(parameter); + } + } + @Nullable public File getConfigDir() { return myConfigDir; 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 c015d0259006..5779ee4a86fb 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandExecutor.java @@ -15,6 +15,7 @@ */ package org.jetbrains.idea.svn.commandLine; +import com.intellij.execution.ExecutionException; import com.intellij.execution.configurations.GeneralCommandLine; import com.intellij.execution.process.*; import com.intellij.openapi.diagnostic.Logger; @@ -25,6 +26,8 @@ import org.jetbrains.annotations.NonNls; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; +import java.io.IOException; +import java.io.OutputStreamWriter; import java.util.concurrent.atomic.AtomicBoolean; import java.util.concurrent.atomic.AtomicReference; @@ -40,9 +43,11 @@ public class CommandExecutor { private boolean myIsDestroyed; private boolean myNeedsDestroy; + private volatile String myDestroyReason; protected final GeneralCommandLine myCommandLine; - private Process myProcess; - private OSProcessHandler myHandler; + protected Process myProcess; + protected OSProcessHandler myHandler; + private OutputStreamWriter myProcessWriter; // TODO: Try to implement commands in a way that they manually indicate if they need full output - to prevent situations // TODO: when large amount of data needs to be stored instead of just sequential processing. private CapturingProcessAdapter outputAdapter; @@ -86,16 +91,21 @@ public class CommandExecutor { return myIsDestroyed; } + public String getDestroyReason() { + return myDestroyReason; + } + public void start() { synchronized (myLock) { checkNotStarted(); try { - myProcess = myCommandLine.createProcess(); + myProcess = createProcess(); if (LOG.isDebugEnabled()) { LOG.debug(myCommandLine.toString()); } - myHandler = new OSProcessHandler(myProcess, myCommandLine.getCommandLineString()); + myHandler = createProcessHandler(); + myProcessWriter = new OutputStreamWriter(myHandler.getProcessInput()); startHandlingStreams(); } catch (Throwable t) { listeners().startFailed(t); @@ -104,7 +114,17 @@ public class CommandExecutor { } } - private void startHandlingStreams() { + @NotNull + protected OSProcessHandler createProcessHandler() { + return new OSProcessHandler(myProcess, myCommandLine.getCommandLineString()); + } + + @NotNull + protected Process createProcess() throws ExecutionException { + return myCommandLine.createProcess(); + } + + protected void startHandlingStreams() { outputAdapter = new CapturingProcessAdapter(); myHandler.addProcessListener(outputAdapter); myHandler.addProcessListener(new ProcessTracker()); @@ -121,6 +141,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 @@ -183,6 +209,13 @@ public class CommandExecutor { } } + public void destroyProcess(@Nullable String destroyReason) { + synchronized (myLock) { + myDestroyReason = destroyReason; + myNeedsDestroy = true; + } + } + /** * ProcessHandler.destroyProcess() implementations could acquire read lock in its implementation - like OSProcessManager.getInstance(). * Some commands are called under write lock - which is generally bad idea, but such logic is not refactored yet. @@ -265,6 +298,23 @@ public class CommandExecutor { } } + public void write(String value) throws SvnBindException { + try { + synchronized (myLock) { + myProcessWriter.write(value); + myProcessWriter.flush(); + } + } + catch (IOException e) { + throw new SvnBindException(e); + } + } + + public void logCommand() { + LOG.info("Command text " + getCommandText()); + LOG.info("Command output " + getOutput()); + } + private class CommandCancelTracker extends LineCommandAdapter { @Override public void onLineAvailable(String line, Key outputType) { diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandParametersResolutionModule.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandParametersResolutionModule.java index a76f0ecb9c1b..2e9e42097495 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandParametersResolutionModule.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandParametersResolutionModule.java @@ -44,7 +44,6 @@ public class CommandParametersResolutionModule extends BaseCommandRuntimeModule command.setWorkingDirectory(resolveWorkingDirectory(command)); } command.setConfigDir(myAuthCallback.getSpecialConfigDir()); - command.put("--non-interactive"); command.saveOriginalParameters(); } 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 be097b65948f..67867eab36c0 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,8 @@ 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.openapi.util.text.StringUtil; import com.intellij.util.containers.ContainerUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -85,12 +87,20 @@ public class CommandRuntime { if (exitCode == null || exitCode != 0) { logNullExitCode(executor, exitCode); + if (executor.isManuallyDestroyed()) { + cleanup(executor, command.getWorkingDirectory()); + + String destroyReason = executor.getDestroyReason(); + if (!StringUtil.isEmpty(destroyReason)) { + throw new SvnBindException(destroyReason); + } + } + if (executor.getErrorOutput().length() > 0) { // handle authentication final String errText = executor.getErrorOutput().trim(); - final AuthCallbackCase callback = createCallback(errText, command.getRepositoryUrl()); + final AuthCallbackCase callback = executor instanceof TerminalExecutor ? null : createCallback(errText, command.getRepositoryUrl()); if (callback != null) { - cleanup(executor, command.getWorkingDirectory()); if (callback.getCredentials(errText)) { if (myAuthCallback.getSpecialConfigDir() != null) { command.setConfigDir(myAuthCallback.getSpecialConfigDir()); @@ -107,6 +117,9 @@ public class CommandRuntime { // no errors found in error stream => we treat null exitCode as successful, otherwise exception is thrown if (exitCode != null) { // here exitCode != null && exitCode != 0 + LOG.info("Command - " + executor.getCommandText()); + LOG.info("Command output - " + executor.getOutput()); + throw new SvnBindException("Svn process exited with error code: " + exitCode); } } @@ -148,7 +161,7 @@ public class CommandRuntime { } private void cleanup(@NotNull CommandExecutor executor, @NotNull File workingDirectory) throws SvnBindException { - if (executor.isManuallyDestroyed() && executor.getCommandName().isWriteable()) { + if (executor.getCommandName().isWriteable()) { File wcRoot = SvnUtil.getWorkingCopyRootNew(workingDirectory); // not all commands require cleanup - for instance, some commands operate only with repository - like "svn info " @@ -167,7 +180,19 @@ public class CommandRuntime { @NotNull private CommandExecutor newExecutor(@NotNull Command command) { - 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(this, executor)); + } + + 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/SvnCommandLineStatusClient.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/SvnCommandLineStatusClient.java index 48dcfa55abba..860e858c7a8e 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/SvnCommandLineStatusClient.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/SvnCommandLineStatusClient.java @@ -171,6 +171,11 @@ public class SvnCommandLineStatusClient implements SvnStatusClientI { throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e), e); } catch (SAXException e) { + // status parsing errors are logged separately as sometimes there are parsing errors connected to terminal output handling. + // these errors primarily occur when status output is rather large. + // and status output could be large, for instance, when working copy is locked (seems that each file is listed in status output). + command.logCommand(); + throw new SVNException(SVNErrorMessage.create(SVNErrorCode.IO_ERROR, e), e); } } 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..203b25cecf34 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalExecutor.java @@ -0,0 +1,104 @@ +/* + * 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); + } + } + + @Override + public void logCommand() { + super.logCommand(); + + LOG.info("Terminal output " + ((TerminalProcessHandler) myHandler).getTerminalOutput()); + } + + 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..86fd039ea43c --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalProcessHandler.java @@ -0,0 +1,153 @@ +/* + * 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 org.jetbrains.idea.svn.SvnUtil; + +import java.util.List; +import java.util.regex.Matcher; + +/** + * @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 + protected boolean processHasSeparateErrorStream() { + return false; + } + + @Override + protected void destroyProcessImpl() { + final Process process = getProcess(); + process.destroy(); + } + + @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, resolveOutputType(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; + } + + private static Key resolveOutputType(@NotNull String line, @NotNull Key outputType) { + Key result = outputType; + + if (!ProcessOutputTypes.SYSTEM.equals(outputType)) { + Matcher errorMatcher = SvnUtil.ERROR_PATTERN.matcher(line); + Matcher warningMatcher = SvnUtil.WARNING_PATTERN.matcher(line); + + result = errorMatcher.find() || warningMatcher.find() ? ProcessOutputTypes.STDERR : ProcessOutputTypes.STDOUT; + } + + return result; + } + + @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); + } + } + + public String getTerminalOutput() { + return terminalOutputCapturer.getOutput().getStdout(); + } +} 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..b957b9ad8004 --- /dev/null +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/TerminalSshModule.java @@ -0,0 +1,164 @@ +/* + * 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.project.Project; +import com.intellij.openapi.util.Key; +import com.intellij.openapi.util.Ref; +import com.intellij.util.WaitForProgressToShow; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.idea.svn.SvnBundle; +import org.jetbrains.idea.svn.dialogs.ServerSSHDialog; +import org.jetbrains.idea.svn.dialogs.SimpleCredentialsDialog; +import org.tmatesoft.svn.core.SVNURL; +import org.tmatesoft.svn.core.auth.ISVNAuthenticationProvider; + +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +/** + * @author Konstantin Kolosovsky. + */ +public class TerminalSshModule extends LineCommandAdapter implements CommandRuntimeModule, InteractiveCommandListener { + + private static final Logger LOG = Logger.getInstance(TerminalSshModule.class); + + private static final Pattern PASSPHRASE_PROMPT = Pattern.compile("Enter passphrase for key \\'(.*)\\':\\s?"); + private static final Pattern PASSWORD_PROMPT = Pattern.compile("(.*)\\'s password:\\s?"); + + private static final Pattern UNKNOWN_HOST_MESSAGE = + Pattern.compile("The authenticity of host \\'((.*) \\((.*)\\))\\' can\\'t be established\\.\\s?"); + private static final Pattern HOST_FINGERPRINT_MESSAGE = Pattern.compile("(\\w+) key fingerprint is (.*)\\.\\s?"); + private static final Pattern ACCEPT_HOST_PROMPT = Pattern.compile("Are you sure you want to continue connecting \\(yes/no\\)\\?\\s?"); + + @NotNull private final CommandRuntime myRuntime; + @NotNull private final CommandExecutor myExecutor; + + private String unknownHost; + private String fingerprintAlgorithm; + private String hostFingerprint; + + // TODO: Do not accept executor here and make it as command runtime module + public TerminalSshModule(@NotNull CommandRuntime runtime, @NotNull CommandExecutor executor) { + myExecutor = executor; + myRuntime = runtime; + } + + @Override + public void onStart(@NotNull Command command) throws SvnBindException { + } + + @Override + public boolean handlePrompt(String line, Key outputType) { + return checkPassphrase(line) || checkPassword(line) || checkUnknownHost(line); + } + + private boolean checkPassphrase(@NotNull String line) { + Matcher matcher = PASSPHRASE_PROMPT.matcher(line); + + return matcher.matches() && handleAuthPrompt(SimpleCredentialsDialog.Mode.SSH_PASSPHRASE, matcher.group(1)); + } + + private boolean checkPassword(@NotNull String line) { + Matcher matcher = PASSWORD_PROMPT.matcher(line); + + return matcher.matches() && handleAuthPrompt(SimpleCredentialsDialog.Mode.SSH_PASSWORD, matcher.group(1)); + } + + private boolean checkUnknownHost(@NotNull String line) { + Matcher unknownHostMatcher = UNKNOWN_HOST_MESSAGE.matcher(line); + Matcher hostFingerPrintMatcher = HOST_FINGERPRINT_MESSAGE.matcher(line); + Matcher acceptHostMatcher = ACCEPT_HOST_PROMPT.matcher(line); + + if (unknownHostMatcher.matches()) { + unknownHost = unknownHostMatcher.group(1); + } + else if (hostFingerPrintMatcher.matches()) { + fingerprintAlgorithm = hostFingerPrintMatcher.group(1); + hostFingerprint = hostFingerPrintMatcher.group(2); + } + else if (acceptHostMatcher.matches()) { + handleUnknownHost(); + } + + return unknownHostMatcher.matches() || hostFingerPrintMatcher.matches() || acceptHostMatcher.matches(); + } + + private void handleUnknownHost() { + final Project project = myRuntime.getVcs().getProject(); + final Ref answer = new Ref(); + + Runnable command = new Runnable() { + @Override + public void run() { + final ServerSSHDialog dialog = new ServerSSHDialog(project, true, unknownHost, fingerprintAlgorithm, hostFingerprint); + dialog.show(); + answer.set(dialog.getResult()); + } + }; + + WaitForProgressToShow.runOrInvokeAndWaitAboveProgress(command); + + unknownHost = null; + fingerprintAlgorithm = null; + hostFingerprint = null; + + sendAnswer(answer.get() == ISVNAuthenticationProvider.REJECTED ? "no" : "yes"); + } + + private boolean handleAuthPrompt(@NotNull final SimpleCredentialsDialog.Mode mode, @NotNull final String key) { + @NotNull final SVNURL repositoryUrl = myExecutor.getCommand().getRepositoryUrl(); + final Project project = myRuntime.getVcs().getProject(); + final Ref answer = new Ref(); + + Runnable command = new Runnable() { + public void run() { + SimpleCredentialsDialog dialog = new SimpleCredentialsDialog(project); + dialog.setup(mode, repositoryUrl.toDecodedString(), key, true); + dialog.setTitle(SvnBundle.message("dialog.title.authentication.required")); + dialog.show(); + if (dialog.isOK()) { + answer.set(dialog.getPassword()); + } + // TODO: Correctly handle "cancel" - kill the process + // TODO: and perform "cleanup" on working copy + } + }; + + WaitForProgressToShow.runOrInvokeAndWaitAboveProgress(command); + + if (!answer.isNull()) { + sendAnswer(answer.get()); + } else { + myExecutor.destroyProcess("Authentication canceled for repository: " + repositoryUrl); + } + + return !answer.isNull(); + } + + private boolean sendAnswer(@NotNull String answer) { + try { + myExecutor.write(answer + "\n"); + return true; + } + catch (SvnBindException e) { + // TODO: handle this more carefully + LOG.info(e); + } + return false; + } +} diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/ServerSSHDialog.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/ServerSSHDialog.java index 30ef5216614c..54e02658b03c 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/ServerSSHDialog.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/ServerSSHDialog.java @@ -17,6 +17,7 @@ package org.jetbrains.idea.svn.dialogs; import com.intellij.openapi.project.Project; import com.intellij.openapi.ui.DialogWrapper; +import com.intellij.openapi.util.text.StringUtil; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; import org.jetbrains.idea.svn.SvnBundle; @@ -41,12 +42,20 @@ public class ServerSSHDialog extends DialogWrapper { public ServerSSHDialog(Project project, boolean store, @NotNull final String host, @Nullable final String algorithm, @NotNull final byte[] fingerprints) { + this(project, store, host, algorithm, SVNSSLUtil.getFingerprint(fingerprints, "SHA1")); + } + + public ServerSSHDialog(Project project, + boolean store, + @NotNull final String host, + @Nullable final String algorithm, + @NotNull String fingerprints) { super(project, true); myStore = store; myHost = host; - myAlgorithm = algorithm == null ? "" : algorithm; + myAlgorithm = StringUtil.notNullize(algorithm); // todo ? - myFingerprints = SVNSSLUtil.getFingerprint(fingerprints, "SHA1"); + myFingerprints = fingerprints; myResult = ISVNAuthenticationProvider.REJECTED; setOKButtonText(SvnBundle.message("button.text.ssh.accept")); setCancelButtonText(SvnBundle.message("button.text.ssh.reject")); diff --git a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SimpleCredentialsDialog.java b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SimpleCredentialsDialog.java index 3c11acb0e14b..f779c0c359d5 100644 --- a/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SimpleCredentialsDialog.java +++ b/plugins/svn4idea/src/org/jetbrains/idea/svn/dialogs/SimpleCredentialsDialog.java @@ -34,6 +34,7 @@ import java.awt.*; public class SimpleCredentialsDialog extends DialogWrapper implements DocumentListener { private boolean myAllowSave; private String myUserName; + private Mode myMode; private String myRealm; private JTextField myUserNameText; @@ -42,18 +43,24 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi @NonNls private static final String HELP_ID = "vcs.subversion.authentication"; - protected SimpleCredentialsDialog(Project project) { + public SimpleCredentialsDialog(Project project) { super(project, true); setResizable(false); } public void setup(String realm, String userName, boolean allowSave) { + setup(Mode.DEFAULT, realm, userName, allowSave); + } + + public void setup(Mode mode, String realm, String userName, boolean allowSave) { + myMode = mode; myRealm = realm; myUserName = userName; myAllowSave = allowSave; getHelpAction().setEnabled(true); init(); } + protected void doHelpAction() { HelpManager.getInstance().invokeHelp(HELP_ID); } @@ -89,7 +96,7 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi gb.weightx = 0; gb.fill = GridBagConstraints.NONE; - label = new JLabel(SvnBundle.message("label.auth.user.name")); + label = new JLabel(SvnBundle.message(myMode.equals(Mode.SSH_PASSPHRASE) ? "label.ssh.key.file" : "label.auth.user.name")); panel.add(label, gb); // user name field @@ -106,6 +113,7 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi } myUserNameText.selectAll(); myUserNameText.getDocument().addDocumentListener(this); + myUserNameText.setEnabled(myMode.equals(Mode.DEFAULT)); gb.gridy += 1; gb.weightx = 0; @@ -113,7 +121,7 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi gb.fill = GridBagConstraints.NONE; gb.gridwidth = 1; - label = new JLabel(SvnBundle.message("label.auth.password")); + label = new JLabel(SvnBundle.message(myMode.equals(Mode.SSH_PASSPHRASE) ? "label.ssh.passphrase" : "label.auth.password")); panel.add(label, gb); // passworde field @@ -152,7 +160,7 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi } public JComponent getPreferredFocusedComponent() { - return myUserNameText; + return myUserNameText.isEnabled() ? myUserNameText : myPasswordText; } public boolean shouldCloseOnCross() { @@ -197,4 +205,10 @@ public class SimpleCredentialsDialog extends DialogWrapper implements DocumentLi private void updateOKButton() { getOKAction().setEnabled(isOKActionEnabled()); } + + public enum Mode { + SSH_PASSPHRASE, + SSH_PASSWORD, + DEFAULT + } } 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 @@ + + +