blob: 5f36dadc04c7852d82b60a83ba15890d22eabe2a [file] [log] [blame]
/*
* 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 others
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Distribution License v. 1.0 which is available at
* https://www.eclipse.org/org/documents/edl-v10.php.
*
* SPDX-License-Identifier: BSD-3-Clause
*/
package org.eclipse.jgit.transport.ssh.jsch;
import java.io.BufferedOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.concurrent.Callable;
import java.util.concurrent.TimeUnit;
import org.eclipse.jgit.errors.TransportException;
import org.eclipse.jgit.internal.transport.ssh.jsch.JSchText;
import org.eclipse.jgit.transport.FtpChannel;
import org.eclipse.jgit.transport.RemoteSession2;
import org.eclipse.jgit.transport.URIish;
import org.eclipse.jgit.util.io.IsolatedOutputStream;
import com.jcraft.jsch.Channel;
import com.jcraft.jsch.ChannelExec;
import com.jcraft.jsch.ChannelSftp;
import com.jcraft.jsch.JSchException;
import com.jcraft.jsch.Session;
import com.jcraft.jsch.SftpException;
/**
* Run remote commands using Jsch.
* <p>
* This class is the default session implementation using Jsch. Note that
* {@link org.eclipse.jgit.transport.ssh.jsch.JschConfigSessionFactory} is used to
* create the actual session passed to the constructor.
*
* @since 6.0
*/
public class JschSession implements RemoteSession2 {
final Session sock;
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(Session session, URIish uri) {
sock = session;
this.uri = uri;
}
@Override
public Process exec(String command, int timeout) throws IOException {
return exec(command, Collections.emptyMap(), timeout);
}
@Override
public Process exec(String command, Map<String, String> environment,
int timeout) throws IOException {
return new JschProcess(command, environment, timeout);
}
@Override
public void disconnect() {
if (sock.isConnected())
sock.disconnect();
}
/**
* A kludge to allow {@link org.eclipse.jgit.transport.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 com.jcraft.jsch.JSchException
* on problems getting the channel.
* @deprecated since 5.2; use {@link #getFtpChannel()} instead
*/
@Deprecated
public Channel getSftpChannel() throws JSchException {
return sock.openChannel("sftp"); //$NON-NLS-1$
}
/**
* {@inheritDoc}
*
* @since 5.2
*/
@Override
public FtpChannel getFtpChannel() {
return new JschFtpChannel();
}
/**
* 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;
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 environment
* environment variables to pass on
* @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
*/
JschProcess(String commandName, Map<String, String> environment,
int tms) throws TransportException, IOException {
timeout = tms;
try {
channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$
if (environment != null) {
for (Map.Entry<String, String> envVar : environment
.entrySet()) {
channel.setEnv(envVar.getKey(), envVar.getValue());
}
}
channel.setCommand(commandName);
setupStreams();
channel.connect(timeout > 0 ? timeout * 1000 : 0);
if (!channel.isConnected()) {
closeOutputStream();
throw new TransportException(uri,
JSchText.get().connectionFailed);
}
} catch (JSchException e) {
closeOutputStream();
throw new TransportException(uri, e.getMessage(), e);
}
}
private void closeOutputStream() {
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ioe) {
// ignore
}
}
}
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.
OutputStream out = channel.getOutputStream();
if (timeout <= 0) {
outputStream = out;
} else {
IsolatedOutputStream i = new IsolatedOutputStream(out);
outputStream = new BufferedOutputStream(i, 16 * 1024);
}
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 IllegalThreadStateException();
return channel.getExitStatus();
}
private boolean isRunning() {
return channel.getExitStatus() < 0 && channel.isConnected();
}
@Override
public void destroy() {
if (channel.isConnected())
channel.disconnect();
closeOutputStream();
}
@Override
public int waitFor() throws InterruptedException {
while (isRunning())
Thread.sleep(100);
return exitValue();
}
}
private class JschFtpChannel implements FtpChannel {
private ChannelSftp ftp;
@Override
public void connect(int timeout, TimeUnit unit) throws IOException {
try {
ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$
ftp.connect((int) unit.toMillis(timeout));
} catch (JSchException e) {
ftp = null;
throw new IOException(e.getLocalizedMessage(), e);
}
}
@Override
public void disconnect() {
ftp.disconnect();
ftp = null;
}
private <T> T map(Callable<T> op) throws IOException {
try {
return op.call();
} catch (Exception e) {
if (e instanceof SftpException) {
throw new FtpChannel.FtpException(e.getLocalizedMessage(),
((SftpException) e).id, e);
}
throw new IOException(e.getLocalizedMessage(), e);
}
}
@Override
public boolean isConnected() {
return ftp != null && sock.isConnected();
}
@Override
public void cd(String path) throws IOException {
map(() -> {
ftp.cd(path);
return null;
});
}
@Override
public String pwd() throws IOException {
return map(() -> ftp.pwd());
}
@Override
public Collection<DirEntry> ls(String path) throws IOException {
return map(() -> {
List<DirEntry> result = new ArrayList<>();
for (Object e : ftp.ls(path)) {
ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e;
result.add(new DirEntry() {
@Override
public String getFilename() {
return entry.getFilename();
}
@Override
public long getModifiedTime() {
return entry.getAttrs().getMTime();
}
@Override
public boolean isDirectory() {
return entry.getAttrs().isDir();
}
});
}
return result;
});
}
@Override
public void rmdir(String path) throws IOException {
map(() -> {
ftp.rm(path);
return null;
});
}
@Override
public void mkdir(String path) throws IOException {
map(() -> {
ftp.mkdir(path);
return null;
});
}
@Override
public InputStream get(String path) throws IOException {
return map(() -> ftp.get(path));
}
@Override
public OutputStream put(String path) throws IOException {
return map(() -> ftp.put(path));
}
@Override
public void rm(String path) throws IOException {
map(() -> {
ftp.rm(path);
return null;
});
}
@Override
public void rename(String from, String to) throws IOException {
map(() -> {
// Plain FTP rename will fail if "to" exists. Jsch knows about
// the FTP extension "posix-rename@openssh.com", which will
// remove "to" first if it exists.
if (hasPosixRename()) {
ftp.rename(from, to);
} else if (!to.equals(from)) {
// Try to remove "to" first. With git, we typically get this
// when a lock file is moved over the file locked. Note that
// the check for to being equal to from may still fail in
// the general case, but for use with JGit's TransportSftp
// it should be good enough.
delete(to);
ftp.rename(from, to);
}
return null;
});
}
/**
* Determine whether the server has the posix-rename extension.
*
* @return {@code true} if it is supported, {@code false} otherwise
* @see <a href=
* "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD">OpenSSH
* deviations and extensions to the published SSH protocol</a>
* @see <a href=
* "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h:
* rename()</a>
*/
private boolean hasPosixRename() {
return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$
}
}
}