mirror of
https://gitflic.ru/project/openide/openide.git
synced 2026-09-27 10:03:11 +07:00
Merge branch 'svn_18_2'
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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<WorkingCopyFormat, RuntimeException> {
|
||||
@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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
-1
@@ -44,7 +44,6 @@ public class CommandParametersResolutionModule extends BaseCommandRuntimeModule
|
||||
command.setWorkingDirectory(resolveWorkingDirectory(command));
|
||||
}
|
||||
command.setConfigDir(myAuthCallback.getSpecialConfigDir());
|
||||
command.put("--non-interactive");
|
||||
command.saveOriginalParameters();
|
||||
}
|
||||
|
||||
|
||||
@@ -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 <url>"
|
||||
@@ -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
|
||||
|
||||
+26
@@ -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);
|
||||
}
|
||||
+5
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<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);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void logCommand() {
|
||||
super.logCommand();
|
||||
|
||||
LOG.info("Terminal output " + ((TerminalProcessHandler) myHandler).getTerminalOutput());
|
||||
}
|
||||
|
||||
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,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<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
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -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<Integer> answer = new Ref<Integer>();
|
||||
|
||||
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<String> answer = new Ref<String>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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"));
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user