Create RemoteSession interface

The RemoteSession interface operates like a simplified version of
java.lang.Runtime with a single exec method (and a disconnect
method). It returns a java.lang.Process, which should begin execution
immediately. Note that this greatly simplifies the interface for
running commands. There is no longer a connect method, and most
implementations will contain the bulk of their code inside
Process.exec, or a constructor called by Process.exec. (See the
revised implementations of JschSession and ExtSession.)
Implementations can now configure their connections properly without
either ignoring the proper use of the interface or trying to adhere
to an overly strict interface with odd rules about what methods are
called first.  For example, Jsch needs to create the output stream
before executing, which it now does in the process constructor. These
changes should make it much easier to add alternate session
implementations in the future.

Also-by: John D Eblen <jdeblen@comcast.net>
Bug: 336749
CQ: 5004
Change-Id: Iece43632086afadf175af6638255041ccaf2bfbb
Signed-off-by: Chris Aniszczyk <caniszczyk@gmail.com>
This commit is contained in:
Roland Schulz 2011-03-04 19:50:14 -05:00 committed by Chris Aniszczyk
parent 770c733687
commit ccd3d83719
8 changed files with 449 additions and 284 deletions

View File

@ -58,7 +58,7 @@
* If user interactivity is required by SSH (e.g. to obtain a password), the * If user interactivity is required by SSH (e.g. to obtain a password), the
* connection will immediately fail. * connection will immediately fail.
*/ */
class DefaultSshSessionFactory extends SshConfigSessionFactory { class DefaultSshSessionFactory extends JschConfigSessionFactory {
protected void configure(final OpenSshConfig.Host hc, final Session session) { protected void configure(final OpenSshConfig.Host hc, final Session session) {
// No additional configuration required. // No additional configuration required.
} }

View File

@ -52,9 +52,13 @@
import java.io.FileInputStream; import java.io.FileInputStream;
import java.io.FileNotFoundException; import java.io.FileNotFoundException;
import java.io.IOException; import java.io.IOException;
import java.net.ConnectException;
import java.net.UnknownHostException;
import java.util.HashMap; import java.util.HashMap;
import java.util.Map; import java.util.Map;
import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import com.jcraft.jsch.JSch; import com.jcraft.jsch.JSch;
@ -74,7 +78,7 @@
* {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)} * {@link #configure(org.eclipse.jgit.transport.OpenSshConfig.Host, Session)}
* to supply appropriate {@link UserInfo} to the session. * to supply appropriate {@link UserInfo} to the session.
*/ */
public abstract class SshConfigSessionFactory extends SshSessionFactory { public abstract class JschConfigSessionFactory extends SshSessionFactory {
private final Map<String, JSch> byIdentityFile = new HashMap<String, JSch>(); private final Map<String, JSch> byIdentityFile = new HashMap<String, JSch>();
private JSch defaultJSch; private JSch defaultJSch;
@ -82,42 +86,63 @@ public abstract class SshConfigSessionFactory extends SshSessionFactory {
private OpenSshConfig config; private OpenSshConfig config;
@Override @Override
public synchronized Session getSession(String user, String pass, public synchronized RemoteSession getSession(URIish uri,
String host, int port, CredentialsProvider credentialsProvider, CredentialsProvider credentialsProvider, FS fs, int tms)
FS fs) throws JSchException { throws TransportException {
if (config == null)
config = OpenSshConfig.get(fs);
final OpenSshConfig.Host hc = config.lookup(host); String user = uri.getUser();
host = hc.getHostName(); final String pass = uri.getPass();
if (port <= 0) String host = uri.getHost();
port = hc.getPort(); int port = uri.getPort();
if (user == null)
user = hc.getUser();
final Session session = createSession(hc, user, host, port, fs); try {
if (pass != null) if (config == null)
session.setPassword(pass); config = OpenSshConfig.get(fs);
final String strictHostKeyCheckingPolicy = hc
.getStrictHostKeyChecking(); final OpenSshConfig.Host hc = config.lookup(host);
if (strictHostKeyCheckingPolicy != null) host = hc.getHostName();
session.setConfig("StrictHostKeyChecking", if (port <= 0)
strictHostKeyCheckingPolicy); port = hc.getPort();
final String pauth = hc.getPreferredAuthentications(); if (user == null)
if (pauth != null) user = hc.getUser();
session.setConfig("PreferredAuthentications", pauth);
if (credentialsProvider != null final Session session = createSession(hc, user, host, port, fs);
if (pass != null)
session.setPassword(pass);
final String strictHostKeyCheckingPolicy = hc
.getStrictHostKeyChecking();
if (strictHostKeyCheckingPolicy != null)
session.setConfig("StrictHostKeyChecking",
strictHostKeyCheckingPolicy);
final String pauth = hc.getPreferredAuthentications();
if (pauth != null)
session.setConfig("PreferredAuthentications", pauth);
if (credentialsProvider != null
&& (!hc.isBatchMode() || !credentialsProvider.isInteractive())) { && (!hc.isBatchMode() || !credentialsProvider.isInteractive())) {
session.setUserInfo(new CredentialsProviderUserInfo(session, session.setUserInfo(new CredentialsProviderUserInfo(session,
credentialsProvider)); credentialsProvider));
}
configure(hc, session);
if (!session.isConnected())
session.connect(tms);
return new JschSession(session, uri);
} catch (JSchException je) {
final Throwable c = je.getCause();
if (c instanceof UnknownHostException)
throw new TransportException(uri, JGitText.get().unknownHost);
if (c instanceof ConnectException)
throw new TransportException(uri, c.getMessage());
throw new TransportException(uri, je.getMessage(), je);
} }
configure(hc, session);
return session;
} }
/** /**
* Create a new JSch session for the requested address. * Create a new remote session for the requested address.
* *
* @param hc * @param hc
* host configuration * host configuration
* @param user * @param user
@ -165,15 +190,13 @@ protected Session createSession(final OpenSshConfig.Host hc,
protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException { protected JSch getJSch(final OpenSshConfig.Host hc, FS fs) throws JSchException {
if (defaultJSch == null) { if (defaultJSch == null) {
defaultJSch = createDefaultJSch(fs); defaultJSch = createDefaultJSch(fs);
for (Object name : defaultJSch.getIdentityNames()) { for (Object name : defaultJSch.getIdentityNames())
byIdentityFile.put((String) name, defaultJSch); byIdentityFile.put((String) name, defaultJSch);
}
} }
final File identityFile = hc.getIdentityFile(); final File identityFile = hc.getIdentityFile();
if (identityFile == null) { if (identityFile == null)
return defaultJSch; return defaultJSch;
}
final String identityKey = identityFile.getAbsolutePath(); final String identityKey = identityFile.getAbsolutePath();
JSch jsch = byIdentityFile.get(identityKey); JSch jsch = byIdentityFile.get(identityKey);

View File

@ -0,0 +1,237 @@
/*
* Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
* Copyright (C) 2008-2009, Google Inc.
* Copyright (C) 2009, Google, Inc.
* Copyright (C) 2009, JetBrains s.r.o.
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.transport;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.util.io.StreamCopyThread;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
/**
* Run remote commands using Jsch.
* <p>
* This class is the default session implementation using Jsch. Note that
* {@link JschConfigSessionFactory} is used to create the actual session passed
* to the constructor.
*/
public class JschSession implements RemoteSession {
private final Session sock;
private final URIish uri;
/**
* Create a new session object by passing the real Jsch session and the URI
* information.
*
* @param session
* the real Jsch session created elsewhere.
* @param uri
* the URI information for the remote connection
*/
public JschSession(final Session session, URIish uri) {
sock = session;
this.uri = uri;
}
public Process exec(String command, int timeout) throws IOException {
return new JschProcess(command, timeout);
}
public void disconnect() {
if (sock.isConnected())
sock.disconnect();
}
/**
* A kludge to allow {@link TransportSftp} to get an Sftp channel from Jsch.
* Ideally, this method would be generic, which would require implementing
* generic Sftp channel operations in the RemoteSession class.
*
* @return a channel suitable for Sftp operations.
* @throws JSchException
* on problems getting the channel.
*/
public Channel getSftpChannel() throws JSchException {
return sock.openChannel("sftp");
}
/**
* Implementation of Process for running a single command using Jsch.
* <p>
* Uses the Jsch session to do actual command execution and manage the
* execution.
*/
private class JschProcess extends Process {
private ChannelExec channel;
private final int timeout;
private InputStream inputStream;
private OutputStream outputStream;
private InputStream errStream;
/**
* Opens a channel on the session ("sock") for executing the given
* command, opens streams, and starts command execution.
*
* @param commandName
* the command to execute
* @param tms
* the timeout value, in seconds, for the command.
* @throws TransportException
* on problems opening a channel or connecting to the remote
* host
* @throws IOException
* on problems opening streams
*/
private JschProcess(final String commandName, int tms)
throws TransportException, IOException {
timeout = tms;
try {
channel = (ChannelExec) sock.openChannel("exec");
channel.setCommand(commandName);
setupStreams();
channel.connect(timeout > 0 ? timeout * 1000 : 0);
if (!channel.isConnected())
throw new TransportException(uri, "connection failed");
} catch (JSchException e) {
throw new TransportException(uri, e.getMessage(), e);
}
}
private void setupStreams() throws IOException {
inputStream = channel.getInputStream();
// JSch won't let us interrupt writes when we use our InterruptTimer
// to break out of a long-running write operation. To work around
// that we spawn a background thread to shuttle data through a pipe,
// as we can issue an interrupted write out of that. Its slower, so
// we only use this route if there is a timeout.
final OutputStream out = channel.getOutputStream();
if (timeout <= 0) {
outputStream = out;
} else {
final PipedInputStream pipeIn = new PipedInputStream();
final StreamCopyThread copier = new StreamCopyThread(pipeIn,
out);
final PipedOutputStream pipeOut = new PipedOutputStream(pipeIn) {
@Override
public void flush() throws IOException {
super.flush();
copier.flush();
}
@Override
public void close() throws IOException {
super.close();
try {
copier.join(timeout * 1000);
} catch (InterruptedException e) {
// Just wake early, the thread will terminate
// anyway.
}
}
};
copier.start();
outputStream = pipeOut;
}
errStream = channel.getErrStream();
}
@Override
public InputStream getInputStream() {
return inputStream;
}
@Override
public OutputStream getOutputStream() {
return outputStream;
}
@Override
public InputStream getErrorStream() {
return errStream;
}
@Override
public int exitValue() {
if (isRunning())
throw new IllegalStateException();
return channel.getExitStatus();
}
private boolean isRunning() {
return channel.getExitStatus() < 0 && channel.isConnected();
}
@Override
public void destroy() {
if (channel.isConnected())
channel.disconnect();
}
@Override
public int waitFor() throws InterruptedException {
while (isRunning())
Thread.sleep(100);
return exitValue();
}
}
}

View File

@ -0,0 +1,86 @@
/*
* Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
* Copyright (C) 2008-2009, Google Inc.
* Copyright (C) 2009, Google, Inc.
* Copyright (C) 2009, JetBrains s.r.o.
* Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
* Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org>
* and other copyright owners as documented in the project's IP log.
*
* This program and the accompanying materials are made available
* under the terms of the Eclipse Distribution License v1.0 which
* accompanies this distribution, is reproduced below, and is
* available at http://www.eclipse.org/org/documents/edl-v10.php
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or
* without modification, are permitted provided that the following
* conditions are met:
*
* - Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
*
* - Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following
* disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* - Neither the name of the Eclipse Foundation, Inc. nor the
* names of its contributors may be used to endorse or promote
* products derived from this software without specific prior
* written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND
* CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES,
* INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES
* OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
* ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR
* CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT
* NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
* STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
* ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF
* ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package org.eclipse.jgit.transport;
import java.io.IOException;
/**
* Create a remote "session" for executing remote commands.
* <p>
* Clients should subclass RemoteSession to create an alternate way for JGit to
* execute remote commands. (The client application may already have this
* functionality available.) Note that this class is just a factory for creating
* remote processes. If the application already has a persistent connection to
* the remote machine, RemoteSession may do nothing more than return a new
* RemoteProcess when exec is called.
*/
public interface RemoteSession {
/**
* Generate a new remote process to execute the given command. This function
* should also start execution and may need to create the streams prior to
* execution.
* @param commandName
* command to execute
* @param timeout
* timeout value, in seconds, for command execution
* @return a new remote process
* @throws IOException
* may be thrown in several cases. For example, on problems
* opening input or output streams or on problems connecting or
* communicating with the remote host. For the latter two cases,
* a TransportException may be thrown (a subclass of
* IOException).
*/
public Process exec(String commandName, int timeout) throws IOException;
/**
* Disconnect the remote session
*/
public void disconnect();
}

View File

@ -44,11 +44,9 @@
package org.eclipse.jgit.transport; package org.eclipse.jgit.transport;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.util.FS; import org.eclipse.jgit.util.FS;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
/** /**
* Creates and destroys SSH connections to a remote system. * Creates and destroys SSH connections to a remote system.
* <p> * <p>
@ -56,9 +54,9 @@
* communicating with the end-user as well as reading their personal SSH * communicating with the end-user as well as reading their personal SSH
* configuration settings, such as known hosts and private keys. * configuration settings, such as known hosts and private keys.
* <p> * <p>
* A {@link Session} must be returned to the factory that created it. Callers * A {@link RemoteSession} must be returned to the factory that created it.
* are encouraged to retain the SshSessionFactory for the duration of the period * Callers are encouraged to retain the SshSessionFactory for the duration of
* they are using the Session. * the period they are using the Session.
*/ */
public abstract class SshSessionFactory { public abstract class SshSessionFactory {
private static SshSessionFactory INSTANCE = new DefaultSshSessionFactory(); private static SshSessionFactory INSTANCE = new DefaultSshSessionFactory();
@ -68,7 +66,7 @@ public abstract class SshSessionFactory {
* <p> * <p>
* A factory is always available. By default the factory will read from the * A factory is always available. By default the factory will read from the
* user's <code>$HOME/.ssh</code> and assume OpenSSH compatibility. * user's <code>$HOME/.ssh</code> and assume OpenSSH compatibility.
* *
* @return factory the current factory for this JVM. * @return factory the current factory for this JVM.
*/ */
public static SshSessionFactory getInstance() { public static SshSessionFactory getInstance() {
@ -98,42 +96,32 @@ public static void setInstance(final SshSessionFactory newFactory) {
* The caller must connect the session by invoking <code>connect()</code> * The caller must connect the session by invoking <code>connect()</code>
* if it has not already been connected. * if it has not already been connected.
* *
* @param user * @param uri
* username to authenticate as. If null a reasonable default must * URI information about the remote host
* be selected by the implementation. This may be
* <code>System.getProperty("user.name")</code>.
* @param pass
* optional user account password or passphrase. If not null a
* UserInfo that supplies this value to the SSH library will be
* configured.
* @param host
* hostname (or IP address) to connect to. Must not be null.
* @param port
* port number the server is listening for connections on. May be <=
* 0 to indicate the IANA registered port of 22 should be used.
* @param credentialsProvider * @param credentialsProvider
* provider to support authentication, may be null. * provider to support authentication, may be null.
* @param fs * @param fs
* the file system abstraction which will be necessary to * the file system abstraction which will be necessary to
* perform certain file system operations. * perform certain file system operations.
* @param tms
* Timeout value, in milliseconds.
* @return a session that can contact the remote host. * @return a session that can contact the remote host.
* @throws JSchException * @throws TransportException
* the session could not be created. * the session could not be created.
*/ */
public abstract Session getSession(String user, String pass, String host, public abstract RemoteSession getSession(URIish uri,
int port, CredentialsProvider credentialsProvider, FS fs) CredentialsProvider credentialsProvider, FS fs, int tms)
throws JSchException; throws TransportException;
/** /**
* Close (or recycle) a session to a host. * Close (or recycle) a session to a host.
* *
* @param session * @param session
* a session previously obtained from this factory's * a session previously obtained from this factory's
* {@link #getSession(String,String, String, int, CredentialsProvider, FS)} * {@link #getSession(URIish, CredentialsProvider, FS, int)}
* method. * method.
*/ */
public void releaseSession(final Session session) { public void releaseSession(final RemoteSession session) {
if (session.isConnected()) session.disconnect();
session.disconnect();
} }
} }

View File

@ -47,16 +47,10 @@
package org.eclipse.jgit.transport; package org.eclipse.jgit.transport;
import java.net.ConnectException;
import java.net.UnknownHostException;
import org.eclipse.jgit.JGitText; import org.eclipse.jgit.JGitText;
import org.eclipse.jgit.errors.TransportException; import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.lib.Repository; import org.eclipse.jgit.lib.Repository;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
/** /**
* The base class for transports that use SSH protocol. This class allows * The base class for transports that use SSH protocol. This class allows
* customizing SSH connection settings. * customizing SSH connection settings.
@ -68,7 +62,7 @@ public abstract class SshTransport extends TcpTransport {
/** /**
* The open SSH session * The open SSH session
*/ */
protected Session sock; private RemoteSession sock;
/** /**
* Create a new transport instance. * Create a new transport instance.
@ -111,35 +105,22 @@ public SshSessionFactory getSshSessionFactory() {
return sch; return sch;
} }
/** /**
* Initialize SSH session * Get the default SSH session
* *
* @return a remote session
* @throws TransportException * @throws TransportException
* in case of error with opening SSH session * in case of error with opening SSH session
*/ */
protected void initSession() throws TransportException { protected RemoteSession getSession() throws TransportException {
if (sock != null) if (sock != null)
return; return sock;
final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
final String user = uri.getUser();
final String pass = uri.getPass(); sock = sch
final String host = uri.getHost(); .getSession(uri, getCredentialsProvider(), local.getFS(), tms);
final int port = uri.getPort(); return sock;
try {
sock = sch.getSession(user, pass, host, port,
getCredentialsProvider(), local.getFS());
if (!sock.isConnected())
sock.connect(tms);
} catch (JSchException je) {
final Throwable c = je.getCause();
if (c instanceof UnknownHostException)
throw new TransportException(uri, JGitText.get().unknownHost);
if (c instanceof ConnectException)
throw new TransportException(uri, c.getMessage());
throw new TransportException(uri, je.getMessage(), je);
}
} }
@Override @Override

View File

@ -48,9 +48,6 @@
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.io.OutputStream;
import java.io.PipedInputStream;
import java.io.PipedOutputStream;
import java.text.MessageFormat; import java.text.MessageFormat;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@ -70,9 +67,7 @@
import org.eclipse.jgit.util.SystemReader; import org.eclipse.jgit.util.SystemReader;
import org.eclipse.jgit.util.io.MessageWriter; import org.eclipse.jgit.util.io.MessageWriter;
import org.eclipse.jgit.util.io.StreamCopyThread; import org.eclipse.jgit.util.io.StreamCopyThread;
import org.eclipse.jgit.util.FS;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.JSchException;
/** /**
* Transport through an SSH tunnel. * Transport through an SSH tunnel.
@ -135,22 +130,26 @@ public Transport open(URIish uri, Repository local, String remoteName)
TransportGitSsh(final Repository local, final URIish uri) { TransportGitSsh(final Repository local, final URIish uri) {
super(local, uri); super(local, uri);
if (useExtSession()) {
setSshSessionFactory(new SshSessionFactory() {
@Override
public RemoteSession getSession(URIish uri2,
CredentialsProvider credentialsProvider, FS fs, int tms)
throws TransportException {
return new ExtSession();
}
});
}
} }
@Override @Override
public FetchConnection openFetch() throws TransportException { public FetchConnection openFetch() throws TransportException {
return new SshFetchConnection(newConnection()); return new SshFetchConnection();
} }
@Override @Override
public PushConnection openPush() throws TransportException { public PushConnection openPush() throws TransportException {
return new SshPushConnection(newConnection()); return new SshPushConnection();
}
private Connection newConnection() {
if (useExtConnection())
return new ExtConnection();
return new JschConnection();
} }
String commandFor(final String exe) { String commandFor(final String exe) {
@ -195,123 +194,13 @@ NoRemoteRepositoryException cleanNotFound(NoRemoteRepositoryException nf,
return new NoRemoteRepositoryException(uri, why); return new NoRemoteRepositoryException(uri, why);
} }
private abstract class Connection { private static boolean useExtSession() {
abstract void exec(String commandName) throws TransportException;
abstract void connect() throws TransportException;
abstract InputStream getInputStream() throws IOException;
abstract OutputStream getOutputStream() throws IOException;
abstract InputStream getErrorStream() throws IOException;
abstract int getExitStatus();
abstract void close();
}
private class JschConnection extends Connection {
private ChannelExec channel;
private int exitStatus;
@Override
void exec(String commandName) throws TransportException {
initSession();
try {
channel = (ChannelExec) sock.openChannel("exec");
channel.setCommand(commandFor(commandName));
} catch (JSchException je) {
throw new TransportException(uri, je.getMessage(), je);
}
}
@Override
void connect() throws TransportException {
try {
channel.connect(getTimeout() > 0 ? getTimeout() * 1000 : 0);
if (!channel.isConnected())
throw new TransportException(uri, "connection failed");
} catch (JSchException e) {
throw new TransportException(uri, e.getMessage(), e);
}
}
@Override
InputStream getInputStream() throws IOException {
return channel.getInputStream();
}
@Override
OutputStream getOutputStream() throws IOException {
// JSch won't let us interrupt writes when we use our InterruptTimer
// to break out of a long-running write operation. To work around
// that we spawn a background thread to shuttle data through a pipe,
// as we can issue an interrupted write out of that. Its slower, so
// we only use this route if there is a timeout.
//
final OutputStream out = channel.getOutputStream();
if (getTimeout() <= 0)
return out;
final PipedInputStream pipeIn = new PipedInputStream();
final StreamCopyThread copier = new StreamCopyThread(pipeIn, out);
final PipedOutputStream pipeOut = new PipedOutputStream(pipeIn) {
@Override
public void flush() throws IOException {
super.flush();
copier.flush();
}
@Override
public void close() throws IOException {
super.close();
try {
copier.join(getTimeout() * 1000);
} catch (InterruptedException e) {
// Just wake early, the thread will terminate anyway.
}
}
};
copier.start();
return pipeOut;
}
@Override
InputStream getErrorStream() throws IOException {
return channel.getErrStream();
}
@Override
int getExitStatus() {
return exitStatus;
}
@Override
void close() {
if (channel != null) {
try {
exitStatus = channel.getExitStatus();
if (channel.isConnected())
channel.disconnect();
} finally {
channel = null;
}
}
}
}
private static boolean useExtConnection() {
return SystemReader.getInstance().getenv("GIT_SSH") != null; return SystemReader.getInstance().getenv("GIT_SSH") != null;
} }
private class ExtConnection extends Connection { private class ExtSession implements RemoteSession {
private Process proc; public Process exec(String command, int timeout)
throws TransportException {
private int exitStatus;
@Override
void exec(String commandName) throws TransportException {
String ssh = SystemReader.getInstance().getenv("GIT_SSH"); String ssh = SystemReader.getInstance().getenv("GIT_SSH");
boolean putty = ssh.toLowerCase().contains("plink"); boolean putty = ssh.toLowerCase().contains("plink");
@ -327,7 +216,7 @@ void exec(String commandName) throws TransportException {
args.add(getURI().getUser() + "@" + getURI().getHost()); args.add(getURI().getUser() + "@" + getURI().getHost());
else else
args.add(getURI().getHost()); args.add(getURI().getHost());
args.add(commandFor(commandName)); args.add(command);
ProcessBuilder pb = new ProcessBuilder(); ProcessBuilder pb = new ProcessBuilder();
pb.command(args); pb.command(args);
@ -337,73 +226,35 @@ void exec(String commandName) throws TransportException {
local.getDirectory().getPath()); local.getDirectory().getPath());
try { try {
proc = pb.start(); return pb.start();
} catch (IOException err) { } catch (IOException err) {
throw new TransportException(uri, err.getMessage(), err); throw new TransportException(err.getMessage(), err);
} }
} }
@Override public void disconnect() {
void connect() throws TransportException { // Nothing to do
// Nothing to do, the process was already opened.
}
@Override
InputStream getInputStream() throws IOException {
return proc.getInputStream();
}
@Override
OutputStream getOutputStream() throws IOException {
return proc.getOutputStream();
}
@Override
InputStream getErrorStream() throws IOException {
return proc.getErrorStream();
}
@Override
int getExitStatus() {
return exitStatus;
}
@Override
void close() {
if (proc != null) {
try {
try {
exitStatus = proc.waitFor();
} catch (InterruptedException e) {
// Ignore the interrupt, but return immediately.
}
} finally {
proc = null;
}
}
} }
} }
class SshFetchConnection extends BasePackFetchConnection { class SshFetchConnection extends BasePackFetchConnection {
private Connection conn; private final Process process;
private StreamCopyThread errorThread; private StreamCopyThread errorThread;
SshFetchConnection(Connection conn) throws TransportException { SshFetchConnection() throws TransportException {
super(TransportGitSsh.this); super(TransportGitSsh.this);
this.conn = conn;
try { try {
process = getSession().exec(commandFor(getOptionUploadPack()),
getTimeout());
final MessageWriter msg = new MessageWriter(); final MessageWriter msg = new MessageWriter();
setMessageWriter(msg); setMessageWriter(msg);
conn.exec(getOptionUploadPack()); final InputStream upErr = process.getErrorStream();
final InputStream upErr = conn.getErrorStream();
errorThread = new StreamCopyThread(upErr, msg.getRawStream()); errorThread = new StreamCopyThread(upErr, msg.getRawStream());
errorThread.start(); errorThread.start();
init(conn.getInputStream(), conn.getOutputStream()); init(process.getInputStream(), process.getOutputStream());
conn.connect();
} catch (TransportException err) { } catch (TransportException err) {
close(); close();
@ -418,7 +269,7 @@ class SshFetchConnection extends BasePackFetchConnection {
readAdvertisedRefs(); readAdvertisedRefs();
} catch (NoRemoteRepositoryException notFound) { } catch (NoRemoteRepositoryException notFound) {
final String msgs = getMessages(); final String msgs = getMessages();
checkExecFailure(conn.getExitStatus(), getOptionUploadPack(), checkExecFailure(process.exitValue(), getOptionUploadPack(),
msgs); msgs);
throw cleanNotFound(notFound, msgs); throw cleanNotFound(notFound, msgs);
} }
@ -439,30 +290,28 @@ public void close() {
} }
super.close(); super.close();
conn.close(); process.destroy();
} }
} }
class SshPushConnection extends BasePackPushConnection { class SshPushConnection extends BasePackPushConnection {
private Connection conn; private final Process process;
private StreamCopyThread errorThread; private StreamCopyThread errorThread;
SshPushConnection(Connection conn) throws TransportException { SshPushConnection() throws TransportException {
super(TransportGitSsh.this); super(TransportGitSsh.this);
this.conn = conn;
try { try {
process = getSession().exec(commandFor(getOptionReceivePack()),
getTimeout());
final MessageWriter msg = new MessageWriter(); final MessageWriter msg = new MessageWriter();
setMessageWriter(msg); setMessageWriter(msg);
conn.exec(getOptionReceivePack()); final InputStream rpErr = process.getErrorStream();
final InputStream rpErr = conn.getErrorStream();
errorThread = new StreamCopyThread(rpErr, msg.getRawStream()); errorThread = new StreamCopyThread(rpErr, msg.getRawStream());
errorThread.start(); errorThread.start();
init(conn.getInputStream(), conn.getOutputStream()); init(process.getInputStream(), process.getOutputStream());
conn.connect();
} catch (TransportException err) { } catch (TransportException err) {
close(); close();
@ -477,7 +326,7 @@ class SshPushConnection extends BasePackPushConnection {
readAdvertisedRefs(); readAdvertisedRefs();
} catch (NoRemoteRepositoryException notFound) { } catch (NoRemoteRepositoryException notFound) {
final String msgs = getMessages(); final String msgs = getMessages();
checkExecFailure(conn.getExitStatus(), getOptionReceivePack(), checkExecFailure(process.exitValue(), getOptionReceivePack(),
msgs); msgs);
throw cleanNotFound(notFound, msgs); throw cleanNotFound(notFound, msgs);
} }
@ -498,7 +347,7 @@ public void close() {
} }
super.close(); super.close();
conn.close(); process.destroy();
} }
} }
} }

View File

@ -147,11 +147,12 @@ public PushConnection openPush() throws TransportException {
} }
ChannelSftp newSftp() throws TransportException { ChannelSftp newSftp() throws TransportException {
initSession();
final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0; final int tms = getTimeout() > 0 ? getTimeout() * 1000 : 0;
try { try {
final Channel channel = sock.openChannel("sftp"); // @TODO: Fix so that this operation is generic and casting to
// JschSession is no longer necessary.
final Channel channel = ((JschSession) getSession())
.getSftpChannel();
channel.connect(tms); channel.connect(tms);
return (ChannelSftp) channel; return (ChannelSftp) channel;
} catch (JSchException je) { } catch (JSchException je) {