View Javadoc
1   /*
2    * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com>
3    * Copyright (C) 2008-2009, Google Inc.
4    * Copyright (C) 2009, Google, Inc.
5    * Copyright (C) 2009, JetBrains s.r.o.
6    * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com>
7    * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others
8    *
9    * This program and the accompanying materials are made available under the
10   * terms of the Eclipse Distribution License v. 1.0 which is available at
11   * https://www.eclipse.org/org/documents/edl-v10.php.
12   *
13   * SPDX-License-Identifier: BSD-3-Clause
14   */
15  
16  package org.eclipse.jgit.transport;
17  
18  import java.io.BufferedOutputStream;
19  import java.io.IOException;
20  import java.io.InputStream;
21  import java.io.OutputStream;
22  import java.util.ArrayList;
23  import java.util.Collection;
24  import java.util.List;
25  import java.util.concurrent.Callable;
26  import java.util.concurrent.TimeUnit;
27  
28  import org.eclipse.jgit.errors.TransportException;
29  import org.eclipse.jgit.internal.JGitText;
30  import org.eclipse.jgit.util.io.IsolatedOutputStream;
31  
32  import com.jcraft.jsch.Channel;
33  import com.jcraft.jsch.ChannelExec;
34  import com.jcraft.jsch.ChannelSftp;
35  import com.jcraft.jsch.JSchException;
36  import com.jcraft.jsch.Session;
37  import com.jcraft.jsch.SftpException;
38  
39  /**
40   * Run remote commands using Jsch.
41   * <p>
42   * This class is the default session implementation using Jsch. Note that
43   * {@link org.eclipse.jgit.transport.JschConfigSessionFactory} is used to create
44   * the actual session passed to the constructor.
45   */
46  public class JschSession implements RemoteSession {
47  	final Session sock;
48  	final URIish uri;
49  
50  	/**
51  	 * Create a new session object by passing the real Jsch session and the URI
52  	 * information.
53  	 *
54  	 * @param session
55  	 *            the real Jsch session created elsewhere.
56  	 * @param uri
57  	 *            the URI information for the remote connection
58  	 */
59  	public JschSession(Session session, URIish uri) {
60  		sock = session;
61  		this.uri = uri;
62  	}
63  
64  	/** {@inheritDoc} */
65  	@Override
66  	public Process exec(String command, int timeout) throws IOException {
67  		return new JschProcess(command, timeout);
68  	}
69  
70  	/** {@inheritDoc} */
71  	@Override
72  	public void disconnect() {
73  		if (sock.isConnected())
74  			sock.disconnect();
75  	}
76  
77  	/**
78  	 * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get
79  	 * an Sftp channel from Jsch. Ideally, this method would be generic, which
80  	 * would require implementing generic Sftp channel operations in the
81  	 * RemoteSession class.
82  	 *
83  	 * @return a channel suitable for Sftp operations.
84  	 * @throws com.jcraft.jsch.JSchException
85  	 *             on problems getting the channel.
86  	 * @deprecated since 5.2; use {@link #getFtpChannel()} instead
87  	 */
88  	@Deprecated
89  	public Channel getSftpChannel() throws JSchException {
90  		return sock.openChannel("sftp"); //$NON-NLS-1$
91  	}
92  
93  	/**
94  	 * {@inheritDoc}
95  	 *
96  	 * @since 5.2
97  	 */
98  	@Override
99  	public FtpChannel getFtpChannel() {
100 		return new JschFtpChannel();
101 	}
102 
103 	/**
104 	 * Implementation of Process for running a single command using Jsch.
105 	 * <p>
106 	 * Uses the Jsch session to do actual command execution and manage the
107 	 * execution.
108 	 */
109 	private class JschProcess extends Process {
110 		private ChannelExec channel;
111 
112 		final int timeout;
113 
114 		private InputStream inputStream;
115 
116 		private OutputStream outputStream;
117 
118 		private InputStream errStream;
119 
120 		/**
121 		 * Opens a channel on the session ("sock") for executing the given
122 		 * command, opens streams, and starts command execution.
123 		 *
124 		 * @param commandName
125 		 *            the command to execute
126 		 * @param tms
127 		 *            the timeout value, in seconds, for the command.
128 		 * @throws TransportException
129 		 *             on problems opening a channel or connecting to the remote
130 		 *             host
131 		 * @throws IOException
132 		 *             on problems opening streams
133 		 */
134 		JschProcess(String commandName, int tms)
135 				throws TransportException, IOException {
136 			timeout = tms;
137 			try {
138 				channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$
139 				channel.setCommand(commandName);
140 				setupStreams();
141 				channel.connect(timeout > 0 ? timeout * 1000 : 0);
142 				if (!channel.isConnected()) {
143 					closeOutputStream();
144 					throw new TransportException(uri,
145 							JGitText.get().connectionFailed);
146 				}
147 			} catch (JSchException e) {
148 				closeOutputStream();
149 				throw new TransportException(uri, e.getMessage(), e);
150 			}
151 		}
152 
153 		private void closeOutputStream() {
154 			if (outputStream != null) {
155 				try {
156 					outputStream.close();
157 				} catch (IOException ioe) {
158 					// ignore
159 				}
160 			}
161 		}
162 
163 		private void setupStreams() throws IOException {
164 			inputStream = channel.getInputStream();
165 
166 			// JSch won't let us interrupt writes when we use our InterruptTimer
167 			// to break out of a long-running write operation. To work around
168 			// that we spawn a background thread to shuttle data through a pipe,
169 			// as we can issue an interrupted write out of that. Its slower, so
170 			// we only use this route if there is a timeout.
171 			OutputStream out = channel.getOutputStream();
172 			if (timeout <= 0) {
173 				outputStream = out;
174 			} else {
175 				IsolatedOutputStream i = new IsolatedOutputStream(out);
176 				outputStream = new BufferedOutputStream(i, 16 * 1024);
177 			}
178 
179 			errStream = channel.getErrStream();
180 		}
181 
182 		@Override
183 		public InputStream getInputStream() {
184 			return inputStream;
185 		}
186 
187 		@Override
188 		public OutputStream getOutputStream() {
189 			return outputStream;
190 		}
191 
192 		@Override
193 		public InputStream getErrorStream() {
194 			return errStream;
195 		}
196 
197 		@Override
198 		public int exitValue() {
199 			if (isRunning())
200 				throw new IllegalStateException();
201 			return channel.getExitStatus();
202 		}
203 
204 		private boolean isRunning() {
205 			return channel.getExitStatus() < 0 && channel.isConnected();
206 		}
207 
208 		@Override
209 		public void destroy() {
210 			if (channel.isConnected())
211 				channel.disconnect();
212 			closeOutputStream();
213 		}
214 
215 		@Override
216 		public int waitFor() throws InterruptedException {
217 			while (isRunning())
218 				Thread.sleep(100);
219 			return exitValue();
220 		}
221 	}
222 
223 	private class JschFtpChannel implements FtpChannel {
224 
225 		private ChannelSftp ftp;
226 
227 		@Override
228 		public void connect(int timeout, TimeUnit unit) throws IOException {
229 			try {
230 				ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$
231 				ftp.connect((int) unit.toMillis(timeout));
232 			} catch (JSchException e) {
233 				ftp = null;
234 				throw new IOException(e.getLocalizedMessage(), e);
235 			}
236 		}
237 
238 		@Override
239 		public void disconnect() {
240 			ftp.disconnect();
241 			ftp = null;
242 		}
243 
244 		private <T> T map(Callable<T> op) throws IOException {
245 			try {
246 				return op.call();
247 			} catch (Exception e) {
248 				if (e instanceof SftpException) {
249 					throw new FtpChannel.FtpException(e.getLocalizedMessage(),
250 							((SftpException) e).id, e);
251 				}
252 				throw new IOException(e.getLocalizedMessage(), e);
253 			}
254 		}
255 
256 		@Override
257 		public boolean isConnected() {
258 			return ftp != null && sock.isConnected();
259 		}
260 
261 		@Override
262 		public void cd(String path) throws IOException {
263 			map(() -> {
264 				ftp.cd(path);
265 				return null;
266 			});
267 		}
268 
269 		@Override
270 		public String pwd() throws IOException {
271 			return map(() -> ftp.pwd());
272 		}
273 
274 		@Override
275 		public Collection<DirEntry> ls(String path) throws IOException {
276 			return map(() -> {
277 				List<DirEntry> result = new ArrayList<>();
278 				for (Object e : ftp.ls(path)) {
279 					ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e;
280 					result.add(new DirEntry() {
281 
282 						@Override
283 						public String getFilename() {
284 							return entry.getFilename();
285 						}
286 
287 						@Override
288 						public long getModifiedTime() {
289 							return entry.getAttrs().getMTime();
290 						}
291 
292 						@Override
293 						public boolean isDirectory() {
294 							return entry.getAttrs().isDir();
295 						}
296 					});
297 				}
298 				return result;
299 			});
300 		}
301 
302 		@Override
303 		public void rmdir(String path) throws IOException {
304 			map(() -> {
305 				ftp.rm(path);
306 				return null;
307 			});
308 		}
309 
310 		@Override
311 		public void mkdir(String path) throws IOException {
312 			map(() -> {
313 				ftp.mkdir(path);
314 				return null;
315 			});
316 		}
317 
318 		@Override
319 		public InputStream get(String path) throws IOException {
320 			return map(() -> ftp.get(path));
321 		}
322 
323 		@Override
324 		public OutputStream put(String path) throws IOException {
325 			return map(() -> ftp.put(path));
326 		}
327 
328 		@Override
329 		public void rm(String path) throws IOException {
330 			map(() -> {
331 				ftp.rm(path);
332 				return null;
333 			});
334 		}
335 
336 		@Override
337 		public void rename(String from, String to) throws IOException {
338 			map(() -> {
339 				// Plain FTP rename will fail if "to" exists. Jsch knows about
340 				// the FTP extension "posix-rename@openssh.com", which will
341 				// remove "to" first if it exists.
342 				if (hasPosixRename()) {
343 					ftp.rename(from, to);
344 				} else if (!to.equals(from)) {
345 					// Try to remove "to" first. With git, we typically get this
346 					// when a lock file is moved over the file locked. Note that
347 					// the check for to being equal to from may still fail in
348 					// the general case, but for use with JGit's TransportSftp
349 					// it should be good enough.
350 					delete(to);
351 					ftp.rename(from, to);
352 				}
353 				return null;
354 			});
355 		}
356 
357 		/**
358 		 * Determine whether the server has the posix-rename extension.
359 		 *
360 		 * @return {@code true} if it is supported, {@code false} otherwise
361 		 * @see <a href=
362 		 *      "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD">OpenSSH
363 		 *      deviations and extensions to the published SSH protocol</a>
364 		 * @see <a href=
365 		 *      "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h:
366 		 *      rename()</a>
367 		 */
368 		private boolean hasPosixRename() {
369 			return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$
370 		}
371 	}
372 }