blob: 4902891163d63d175d5a790a10f4fa9899f911ac [file] [log] [blame]
/*
* 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.Condition;
import com.intellij.openapi.util.SystemInfo;
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;
import org.jetbrains.idea.svn.*;
import org.jetbrains.idea.svn.auth.AuthenticationService;
import org.jetbrains.idea.svn.auth.SvnAuthenticationManager;
import org.tmatesoft.svn.core.SVNURL;
import java.io.File;
import java.util.List;
/**
* @author Konstantin Kolosovsky.
*/
public class CommandRuntime {
private static final Logger LOG = Logger.getInstance(CommandRuntime.class);
@NotNull private final AuthenticationService myAuthenticationService;
@NotNull private final SvnVcs myVcs;
@NotNull private final List<CommandRuntimeModule> myModules;
private final String exePath;
public CommandRuntime(@NotNull SvnVcs vcs, @NotNull AuthenticationService authenticationService) {
myVcs = vcs;
myAuthenticationService = authenticationService;
exePath = SvnApplicationSettings.getInstance().getCommandLinePath();
myModules = ContainerUtil.newArrayList();
myModules.add(new CommandParametersResolutionModule(this));
myModules.add(new ProxyModule(this));
}
@NotNull
public CommandExecutor runWithAuthenticationAttempt(@NotNull Command command) throws SvnBindException {
try {
onStart(command);
boolean repeat = true;
CommandExecutor executor = null;
while (repeat) {
executor = newExecutor(command);
executor.run();
repeat = onAfterCommand(executor, command);
}
return executor;
} finally {
onFinish();
}
}
private void onStart(@NotNull Command command) throws SvnBindException {
// TODO: Actually command handler should be used as canceller, but currently all handlers use same cancel logic -
// TODO: - just check progress indicator
command.setCanceller(new SvnProgressCanceller());
for (CommandRuntimeModule module : myModules) {
module.onStart(command);
}
}
private boolean onAfterCommand(@NotNull CommandExecutor executor, @NotNull Command command) throws SvnBindException {
boolean repeat = false;
// TODO: synchronization does not work well in all cases - sometimes exit code is not yet set and null returned - fix synchronization
// here we treat null exit code as some non-zero exit code
final Integer exitCode = executor.getExitCodeReference();
if (exitCode == null || exitCode != 0) {
logNullExitCode(executor, exitCode);
cleanupManualDestroy(executor, command);
repeat = !StringUtil.isEmpty(executor.getErrorOutput()) ? handleErrorText(executor, command) : handleErrorCode(executor);
}
else {
handleSuccess(executor);
}
return repeat;
}
private static void handleSuccess(CommandExecutor executor) {
// could be situations when exit code = 0, but there is info "warning" in error stream for instance, for "svn status"
// on non-working copy folder
if (executor.getErrorOutput().length() > 0) {
// here exitCode == 0, but some warnings are in error stream
LOG.info("Detected warning - " + executor.getErrorOutput());
}
}
private static boolean handleErrorCode(CommandExecutor executor) throws SvnBindException {
// no errors found in error stream => we treat null exitCode as successful, otherwise exception is thrown
Integer exitCode = executor.getExitCodeReference();
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);
}
return false;
}
private boolean handleErrorText(CommandExecutor executor, Command command) throws SvnBindException {
final String errText = executor.getErrorOutput().trim();
final AuthCallbackCase callback = executor instanceof TerminalExecutor ? null : createCallback(errText, command.getRepositoryUrl());
// do not handle possible authentication errors if command was manually cancelled
// force checking if command is cancelled and not just use corresponding value from executor - as there could be cases when command
// finishes quickly but with some auth error - this way checkCancelled() is not called by executor itself and so command is repeated
// "infinite" times despite it was cancelled.
if (!executor.checkCancelled() && callback != null) {
if (callback.getCredentials(errText)) {
if (myAuthenticationService.getSpecialConfigDir() != null) {
command.setConfigDir(myAuthenticationService.getSpecialConfigDir());
}
callback.updateParameters(command);
return true;
}
}
throw new SvnBindException(errText);
}
private void cleanupManualDestroy(CommandExecutor executor, Command command) throws SvnBindException {
if (executor.isManuallyDestroyed()) {
cleanup(executor, command.getWorkingDirectory());
String destroyReason = executor.getDestroyReason();
if (!StringUtil.isEmpty(destroyReason)) {
throw new SvnBindException(destroyReason);
}
}
}
private void onFinish() {
myAuthenticationService.reset();
}
private static void logNullExitCode(@NotNull CommandExecutor executor, @Nullable Integer exitCode) {
if (exitCode == null) {
LOG.info("Null exit code returned, but not errors detected " + executor.getCommandText());
}
}
@Nullable
private AuthCallbackCase createCallback(@NotNull final String errText, @Nullable final SVNURL url) {
List<AuthCallbackCase> authCases = ContainerUtil.newArrayList();
authCases.add(new CertificateCallbackCase(myAuthenticationService, url));
authCases.add(new CredentialsCallback(myAuthenticationService, url));
authCases.add(new PassphraseCallback(myAuthenticationService, url));
authCases.add(new ProxyCallback(myAuthenticationService, url));
authCases.add(new TwoWaySslCallback(myAuthenticationService, url));
authCases.add(new UsernamePasswordCallback(myAuthenticationService, url));
return ContainerUtil.find(authCases, new Condition<AuthCallbackCase>() {
@Override
public boolean value(AuthCallbackCase authCase) {
return authCase.canHandle(errText);
}
});
}
private void cleanup(@NotNull CommandExecutor executor, @NotNull File workingDirectory) throws SvnBindException {
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>"
// TODO: check if we could "configure" commands (or make command to explicitly ask) if cleanup is required - not to search
// TODO: working copy root each time
if (wcRoot != null) {
Command cleanupCommand = new Command(SvnCommandName.cleanup);
cleanupCommand.setWorkingDirectory(wcRoot);
newExecutor(cleanupCommand).run();
} else {
LOG.info("Could not execute cleanup for command " + executor.getCommandText());
}
}
}
@NotNull
private CommandExecutor newExecutor(@NotNull Command command) {
final CommandExecutor executor;
if (!(Registry.is("svn.use.terminal") && isForSshRepository(command)) || isLocal(command)) {
command.putIfNotPresent("--non-interactive");
executor = new CommandExecutor(exePath, command);
}
else {
// do not explicitly specify "--force-interactive" as it is not supported in svn 1.7 - commands will be interactive by default as
// running under terminal
executor = newTerminalExecutor(command);
((TerminalExecutor)executor).addInteractiveListener(new TerminalSshModule(this, executor));
}
return executor;
}
@NotNull
private TerminalExecutor newTerminalExecutor(@NotNull Command command) {
return SystemInfo.isWindows ? new WinTerminalExecutor(exePath, command) : new TerminalExecutor(exePath, command);
}
private static boolean isLocal(@NotNull Command command) {
return SvnCommandName.version.equals(command.getName()) ||
SvnCommandName.cleanup.equals(command.getName()) ||
SvnCommandName.add.equals(command.getName()) ||
// currently "svn delete" is only applied to local files
SvnCommandName.delete.equals(command.getName()) ||
SvnCommandName.revert.equals(command.getName()) ||
SvnCommandName.resolve.equals(command.getName()) ||
SvnCommandName.upgrade.equals(command.getName()) ||
SvnCommandName.changelist.equals(command.getName()) ||
command.isLocalInfo() || command.isLocalStatus() || command.isLocalProperty() || command.isLocalCat();
}
private static boolean isForSshRepository(@NotNull Command command) {
SVNURL url = command.getRepositoryUrl();
return url != null && StringUtil.equalsIgnoreCase(SvnAuthenticationManager.SVN_SSH, url.getProtocol());
}
@NotNull
public AuthenticationService getAuthenticationService() {
return myAuthenticationService;
}
@NotNull
public SvnVcs getVcs() {
return myVcs;
}
}