summaryrefslogtreecommitdiff
path: root/plugins/svn4idea/src/org/jetbrains/idea/svn/commandLine/CommandRuntime.java
blob: 5cc66e0a3983bbb01bf05242166ff03d4826d4ef (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
/*
 * 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.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 AuthenticationCallback myAuthCallback;
  @NotNull private final SvnVcs myVcs;
  @NotNull private final List<CommandRuntimeModule> myModules;
  private final String exePath;

  public CommandRuntime(@NotNull SvnVcs vcs, @NotNull AuthenticationCallback authCallback) {
    myVcs = vcs;
    myAuthCallback = authCallback;
    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 (myAuthCallback.getSpecialConfigDir() != null) {
          command.setConfigDir(myAuthCallback.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() {
    myAuthCallback.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(myAuthCallback, url));
    authCases.add(new CredentialsCallback(myAuthCallback, url));
    authCases.add(new PassphraseCallback(myAuthCallback, url));
    authCases.add(new ProxyCallback(myAuthCallback, url));
    authCases.add(new TwoWaySslCallback(myAuthCallback, url));
    authCases.add(new UsernamePasswordCallback(myAuthCallback, 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 AuthenticationCallback getAuthCallback() {
    return myAuthCallback;
  }

  @NotNull
  public SvnVcs getVcs() {
    return myVcs;
  }
}