18d2d6836SMatthias Sohn /* 28d2d6836SMatthias Sohn * Copyright (C) 2009, Constantine Plotnikov <constantine.plotnikov@gmail.com> 38d2d6836SMatthias Sohn * Copyright (C) 2008-2009, Google Inc. 48d2d6836SMatthias Sohn * Copyright (C) 2009, Google, Inc. 58d2d6836SMatthias Sohn * Copyright (C) 2009, JetBrains s.r.o. 68d2d6836SMatthias Sohn * Copyright (C) 2008, Robin Rosenberg <robin.rosenberg@dewire.com> 78d2d6836SMatthias Sohn * Copyright (C) 2008, Shawn O. Pearce <spearce@spearce.org> and others 88d2d6836SMatthias Sohn * 98d2d6836SMatthias Sohn * This program and the accompanying materials are made available under the 108d2d6836SMatthias Sohn * terms of the Eclipse Distribution License v. 1.0 which is available at 118d2d6836SMatthias Sohn * https://www.eclipse.org/org/documents/edl-v10.php. 128d2d6836SMatthias Sohn * 138d2d6836SMatthias Sohn * SPDX-License-Identifier: BSD-3-Clause 148d2d6836SMatthias Sohn */ 158d2d6836SMatthias Sohn 168d2d6836SMatthias Sohn //TODO(ms): move to org.eclipse.jgit.ssh.jsch in 6.0 178d2d6836SMatthias Sohn package org.eclipse.jgit.transport; 188d2d6836SMatthias Sohn 198d2d6836SMatthias Sohn import java.io.BufferedOutputStream; 208d2d6836SMatthias Sohn import java.io.IOException; 218d2d6836SMatthias Sohn import java.io.InputStream; 228d2d6836SMatthias Sohn import java.io.OutputStream; 238d2d6836SMatthias Sohn import java.util.ArrayList; 248d2d6836SMatthias Sohn import java.util.Collection; 25*0853a241SThomas Wolf import java.util.Collections; 268d2d6836SMatthias Sohn import java.util.List; 27*0853a241SThomas Wolf import java.util.Map; 288d2d6836SMatthias Sohn import java.util.concurrent.Callable; 298d2d6836SMatthias Sohn import java.util.concurrent.TimeUnit; 308d2d6836SMatthias Sohn 318d2d6836SMatthias Sohn import org.eclipse.jgit.errors.TransportException; 328d2d6836SMatthias Sohn import org.eclipse.jgit.internal.transport.jsch.JSchText; 338d2d6836SMatthias Sohn import org.eclipse.jgit.util.io.IsolatedOutputStream; 348d2d6836SMatthias Sohn 358d2d6836SMatthias Sohn import com.jcraft.jsch.Channel; 368d2d6836SMatthias Sohn import com.jcraft.jsch.ChannelExec; 378d2d6836SMatthias Sohn import com.jcraft.jsch.ChannelSftp; 388d2d6836SMatthias Sohn import com.jcraft.jsch.JSchException; 398d2d6836SMatthias Sohn import com.jcraft.jsch.Session; 408d2d6836SMatthias Sohn import com.jcraft.jsch.SftpException; 418d2d6836SMatthias Sohn 428d2d6836SMatthias Sohn /** 438d2d6836SMatthias Sohn * Run remote commands using Jsch. 448d2d6836SMatthias Sohn * <p> 458d2d6836SMatthias Sohn * This class is the default session implementation using Jsch. Note that 468d2d6836SMatthias Sohn * {@link org.eclipse.jgit.transport.JschConfigSessionFactory} is used to create 478d2d6836SMatthias Sohn * the actual session passed to the constructor. 488d2d6836SMatthias Sohn */ 49*0853a241SThomas Wolf public class JschSession implements RemoteSession2 { 508d2d6836SMatthias Sohn final Session sock; 518d2d6836SMatthias Sohn final URIish uri; 528d2d6836SMatthias Sohn 538d2d6836SMatthias Sohn /** 548d2d6836SMatthias Sohn * Create a new session object by passing the real Jsch session and the URI 558d2d6836SMatthias Sohn * information. 568d2d6836SMatthias Sohn * 578d2d6836SMatthias Sohn * @param session 588d2d6836SMatthias Sohn * the real Jsch session created elsewhere. 598d2d6836SMatthias Sohn * @param uri 608d2d6836SMatthias Sohn * the URI information for the remote connection 618d2d6836SMatthias Sohn */ JschSession(Session session, URIish uri)628d2d6836SMatthias Sohn public JschSession(Session session, URIish uri) { 638d2d6836SMatthias Sohn sock = session; 648d2d6836SMatthias Sohn this.uri = uri; 658d2d6836SMatthias Sohn } 668d2d6836SMatthias Sohn 678d2d6836SMatthias Sohn /** {@inheritDoc} */ 688d2d6836SMatthias Sohn @Override exec(String command, int timeout)698d2d6836SMatthias Sohn public Process exec(String command, int timeout) throws IOException { 70*0853a241SThomas Wolf return exec(command, Collections.emptyMap(), timeout); 71*0853a241SThomas Wolf } 72*0853a241SThomas Wolf 73*0853a241SThomas Wolf /** {@inheritDoc} */ 74*0853a241SThomas Wolf @Override exec(String command, Map<String, String> environment, int timeout)75*0853a241SThomas Wolf public Process exec(String command, Map<String, String> environment, 76*0853a241SThomas Wolf int timeout) throws IOException { 77*0853a241SThomas Wolf return new JschProcess(command, environment, timeout); 788d2d6836SMatthias Sohn } 798d2d6836SMatthias Sohn 808d2d6836SMatthias Sohn /** {@inheritDoc} */ 818d2d6836SMatthias Sohn @Override disconnect()828d2d6836SMatthias Sohn public void disconnect() { 838d2d6836SMatthias Sohn if (sock.isConnected()) 848d2d6836SMatthias Sohn sock.disconnect(); 858d2d6836SMatthias Sohn } 868d2d6836SMatthias Sohn 878d2d6836SMatthias Sohn /** 888d2d6836SMatthias Sohn * A kludge to allow {@link org.eclipse.jgit.transport.TransportSftp} to get 898d2d6836SMatthias Sohn * an Sftp channel from Jsch. Ideally, this method would be generic, which 908d2d6836SMatthias Sohn * would require implementing generic Sftp channel operations in the 918d2d6836SMatthias Sohn * RemoteSession class. 928d2d6836SMatthias Sohn * 938d2d6836SMatthias Sohn * @return a channel suitable for Sftp operations. 948d2d6836SMatthias Sohn * @throws com.jcraft.jsch.JSchException 958d2d6836SMatthias Sohn * on problems getting the channel. 968d2d6836SMatthias Sohn * @deprecated since 5.2; use {@link #getFtpChannel()} instead 978d2d6836SMatthias Sohn */ 988d2d6836SMatthias Sohn @Deprecated getSftpChannel()998d2d6836SMatthias Sohn public Channel getSftpChannel() throws JSchException { 1008d2d6836SMatthias Sohn return sock.openChannel("sftp"); //$NON-NLS-1$ 1018d2d6836SMatthias Sohn } 1028d2d6836SMatthias Sohn 1038d2d6836SMatthias Sohn /** 1048d2d6836SMatthias Sohn * {@inheritDoc} 1058d2d6836SMatthias Sohn * 1068d2d6836SMatthias Sohn * @since 5.2 1078d2d6836SMatthias Sohn */ 1088d2d6836SMatthias Sohn @Override getFtpChannel()1098d2d6836SMatthias Sohn public FtpChannel getFtpChannel() { 1108d2d6836SMatthias Sohn return new JschFtpChannel(); 1118d2d6836SMatthias Sohn } 1128d2d6836SMatthias Sohn 1138d2d6836SMatthias Sohn /** 1148d2d6836SMatthias Sohn * Implementation of Process for running a single command using Jsch. 1158d2d6836SMatthias Sohn * <p> 1168d2d6836SMatthias Sohn * Uses the Jsch session to do actual command execution and manage the 1178d2d6836SMatthias Sohn * execution. 1188d2d6836SMatthias Sohn */ 1198d2d6836SMatthias Sohn private class JschProcess extends Process { 1208d2d6836SMatthias Sohn private ChannelExec channel; 1218d2d6836SMatthias Sohn 1228d2d6836SMatthias Sohn final int timeout; 1238d2d6836SMatthias Sohn 1248d2d6836SMatthias Sohn private InputStream inputStream; 1258d2d6836SMatthias Sohn 1268d2d6836SMatthias Sohn private OutputStream outputStream; 1278d2d6836SMatthias Sohn 1288d2d6836SMatthias Sohn private InputStream errStream; 1298d2d6836SMatthias Sohn 1308d2d6836SMatthias Sohn /** 1318d2d6836SMatthias Sohn * Opens a channel on the session ("sock") for executing the given 1328d2d6836SMatthias Sohn * command, opens streams, and starts command execution. 1338d2d6836SMatthias Sohn * 1348d2d6836SMatthias Sohn * @param commandName 1358d2d6836SMatthias Sohn * the command to execute 136*0853a241SThomas Wolf * @param environment 137*0853a241SThomas Wolf * environment variables to pass on 1388d2d6836SMatthias Sohn * @param tms 1398d2d6836SMatthias Sohn * the timeout value, in seconds, for the command. 1408d2d6836SMatthias Sohn * @throws TransportException 1418d2d6836SMatthias Sohn * on problems opening a channel or connecting to the remote 1428d2d6836SMatthias Sohn * host 1438d2d6836SMatthias Sohn * @throws IOException 1448d2d6836SMatthias Sohn * on problems opening streams 1458d2d6836SMatthias Sohn */ JschProcess(String commandName, Map<String, String> environment, int tms)146*0853a241SThomas Wolf JschProcess(String commandName, Map<String, String> environment, 147*0853a241SThomas Wolf int tms) throws TransportException, IOException { 1488d2d6836SMatthias Sohn timeout = tms; 1498d2d6836SMatthias Sohn try { 1508d2d6836SMatthias Sohn channel = (ChannelExec) sock.openChannel("exec"); //$NON-NLS-1$ 151*0853a241SThomas Wolf if (environment != null) { 152*0853a241SThomas Wolf for (Map.Entry<String, String> envVar : environment 153*0853a241SThomas Wolf .entrySet()) { 154*0853a241SThomas Wolf channel.setEnv(envVar.getKey(), envVar.getValue()); 155*0853a241SThomas Wolf } 156*0853a241SThomas Wolf } 1578d2d6836SMatthias Sohn channel.setCommand(commandName); 1588d2d6836SMatthias Sohn setupStreams(); 1598d2d6836SMatthias Sohn channel.connect(timeout > 0 ? timeout * 1000 : 0); 1608d2d6836SMatthias Sohn if (!channel.isConnected()) { 1618d2d6836SMatthias Sohn closeOutputStream(); 1628d2d6836SMatthias Sohn throw new TransportException(uri, 1638d2d6836SMatthias Sohn JSchText.get().connectionFailed); 1648d2d6836SMatthias Sohn } 1658d2d6836SMatthias Sohn } catch (JSchException e) { 1668d2d6836SMatthias Sohn closeOutputStream(); 1678d2d6836SMatthias Sohn throw new TransportException(uri, e.getMessage(), e); 1688d2d6836SMatthias Sohn } 1698d2d6836SMatthias Sohn } 1708d2d6836SMatthias Sohn closeOutputStream()1718d2d6836SMatthias Sohn private void closeOutputStream() { 1728d2d6836SMatthias Sohn if (outputStream != null) { 1738d2d6836SMatthias Sohn try { 1748d2d6836SMatthias Sohn outputStream.close(); 1758d2d6836SMatthias Sohn } catch (IOException ioe) { 1768d2d6836SMatthias Sohn // ignore 1778d2d6836SMatthias Sohn } 1788d2d6836SMatthias Sohn } 1798d2d6836SMatthias Sohn } 1808d2d6836SMatthias Sohn setupStreams()1818d2d6836SMatthias Sohn private void setupStreams() throws IOException { 1828d2d6836SMatthias Sohn inputStream = channel.getInputStream(); 1838d2d6836SMatthias Sohn 1848d2d6836SMatthias Sohn // JSch won't let us interrupt writes when we use our InterruptTimer 1858d2d6836SMatthias Sohn // to break out of a long-running write operation. To work around 1868d2d6836SMatthias Sohn // that we spawn a background thread to shuttle data through a pipe, 1878d2d6836SMatthias Sohn // as we can issue an interrupted write out of that. Its slower, so 1888d2d6836SMatthias Sohn // we only use this route if there is a timeout. 1898d2d6836SMatthias Sohn OutputStream out = channel.getOutputStream(); 1908d2d6836SMatthias Sohn if (timeout <= 0) { 1918d2d6836SMatthias Sohn outputStream = out; 1928d2d6836SMatthias Sohn } else { 1938d2d6836SMatthias Sohn IsolatedOutputStream i = new IsolatedOutputStream(out); 1948d2d6836SMatthias Sohn outputStream = new BufferedOutputStream(i, 16 * 1024); 1958d2d6836SMatthias Sohn } 1968d2d6836SMatthias Sohn 1978d2d6836SMatthias Sohn errStream = channel.getErrStream(); 1988d2d6836SMatthias Sohn } 1998d2d6836SMatthias Sohn 2008d2d6836SMatthias Sohn @Override getInputStream()2018d2d6836SMatthias Sohn public InputStream getInputStream() { 2028d2d6836SMatthias Sohn return inputStream; 2038d2d6836SMatthias Sohn } 2048d2d6836SMatthias Sohn 2058d2d6836SMatthias Sohn @Override getOutputStream()2068d2d6836SMatthias Sohn public OutputStream getOutputStream() { 2078d2d6836SMatthias Sohn return outputStream; 2088d2d6836SMatthias Sohn } 2098d2d6836SMatthias Sohn 2108d2d6836SMatthias Sohn @Override getErrorStream()2118d2d6836SMatthias Sohn public InputStream getErrorStream() { 2128d2d6836SMatthias Sohn return errStream; 2138d2d6836SMatthias Sohn } 2148d2d6836SMatthias Sohn 2158d2d6836SMatthias Sohn @Override exitValue()2168d2d6836SMatthias Sohn public int exitValue() { 2178d2d6836SMatthias Sohn if (isRunning()) 21824fdc1d0SThomas Wolf throw new IllegalThreadStateException(); 2198d2d6836SMatthias Sohn return channel.getExitStatus(); 2208d2d6836SMatthias Sohn } 2218d2d6836SMatthias Sohn isRunning()2228d2d6836SMatthias Sohn private boolean isRunning() { 2238d2d6836SMatthias Sohn return channel.getExitStatus() < 0 && channel.isConnected(); 2248d2d6836SMatthias Sohn } 2258d2d6836SMatthias Sohn 2268d2d6836SMatthias Sohn @Override destroy()2278d2d6836SMatthias Sohn public void destroy() { 2288d2d6836SMatthias Sohn if (channel.isConnected()) 2298d2d6836SMatthias Sohn channel.disconnect(); 2308d2d6836SMatthias Sohn closeOutputStream(); 2318d2d6836SMatthias Sohn } 2328d2d6836SMatthias Sohn 2338d2d6836SMatthias Sohn @Override waitFor()2348d2d6836SMatthias Sohn public int waitFor() throws InterruptedException { 2358d2d6836SMatthias Sohn while (isRunning()) 2368d2d6836SMatthias Sohn Thread.sleep(100); 2378d2d6836SMatthias Sohn return exitValue(); 2388d2d6836SMatthias Sohn } 2398d2d6836SMatthias Sohn } 2408d2d6836SMatthias Sohn 2418d2d6836SMatthias Sohn private class JschFtpChannel implements FtpChannel { 2428d2d6836SMatthias Sohn 2438d2d6836SMatthias Sohn private ChannelSftp ftp; 2448d2d6836SMatthias Sohn 2458d2d6836SMatthias Sohn @Override connect(int timeout, TimeUnit unit)2468d2d6836SMatthias Sohn public void connect(int timeout, TimeUnit unit) throws IOException { 2478d2d6836SMatthias Sohn try { 2488d2d6836SMatthias Sohn ftp = (ChannelSftp) sock.openChannel("sftp"); //$NON-NLS-1$ 2498d2d6836SMatthias Sohn ftp.connect((int) unit.toMillis(timeout)); 2508d2d6836SMatthias Sohn } catch (JSchException e) { 2518d2d6836SMatthias Sohn ftp = null; 2528d2d6836SMatthias Sohn throw new IOException(e.getLocalizedMessage(), e); 2538d2d6836SMatthias Sohn } 2548d2d6836SMatthias Sohn } 2558d2d6836SMatthias Sohn 2568d2d6836SMatthias Sohn @Override disconnect()2578d2d6836SMatthias Sohn public void disconnect() { 2588d2d6836SMatthias Sohn ftp.disconnect(); 2598d2d6836SMatthias Sohn ftp = null; 2608d2d6836SMatthias Sohn } 2618d2d6836SMatthias Sohn map(Callable<T> op)2628d2d6836SMatthias Sohn private <T> T map(Callable<T> op) throws IOException { 2638d2d6836SMatthias Sohn try { 2648d2d6836SMatthias Sohn return op.call(); 2658d2d6836SMatthias Sohn } catch (Exception e) { 2668d2d6836SMatthias Sohn if (e instanceof SftpException) { 2678d2d6836SMatthias Sohn throw new FtpChannel.FtpException(e.getLocalizedMessage(), 2688d2d6836SMatthias Sohn ((SftpException) e).id, e); 2698d2d6836SMatthias Sohn } 2708d2d6836SMatthias Sohn throw new IOException(e.getLocalizedMessage(), e); 2718d2d6836SMatthias Sohn } 2728d2d6836SMatthias Sohn } 2738d2d6836SMatthias Sohn 2748d2d6836SMatthias Sohn @Override isConnected()2758d2d6836SMatthias Sohn public boolean isConnected() { 2768d2d6836SMatthias Sohn return ftp != null && sock.isConnected(); 2778d2d6836SMatthias Sohn } 2788d2d6836SMatthias Sohn 2798d2d6836SMatthias Sohn @Override cd(String path)2808d2d6836SMatthias Sohn public void cd(String path) throws IOException { 2818d2d6836SMatthias Sohn map(() -> { 2828d2d6836SMatthias Sohn ftp.cd(path); 2838d2d6836SMatthias Sohn return null; 2848d2d6836SMatthias Sohn }); 2858d2d6836SMatthias Sohn } 2868d2d6836SMatthias Sohn 2878d2d6836SMatthias Sohn @Override pwd()2888d2d6836SMatthias Sohn public String pwd() throws IOException { 2898d2d6836SMatthias Sohn return map(() -> ftp.pwd()); 2908d2d6836SMatthias Sohn } 2918d2d6836SMatthias Sohn 2928d2d6836SMatthias Sohn @Override ls(String path)2938d2d6836SMatthias Sohn public Collection<DirEntry> ls(String path) throws IOException { 2948d2d6836SMatthias Sohn return map(() -> { 2958d2d6836SMatthias Sohn List<DirEntry> result = new ArrayList<>(); 2968d2d6836SMatthias Sohn for (Object e : ftp.ls(path)) { 2978d2d6836SMatthias Sohn ChannelSftp.LsEntry entry = (ChannelSftp.LsEntry) e; 2988d2d6836SMatthias Sohn result.add(new DirEntry() { 2998d2d6836SMatthias Sohn 3008d2d6836SMatthias Sohn @Override 3018d2d6836SMatthias Sohn public String getFilename() { 3028d2d6836SMatthias Sohn return entry.getFilename(); 3038d2d6836SMatthias Sohn } 3048d2d6836SMatthias Sohn 3058d2d6836SMatthias Sohn @Override 3068d2d6836SMatthias Sohn public long getModifiedTime() { 3078d2d6836SMatthias Sohn return entry.getAttrs().getMTime(); 3088d2d6836SMatthias Sohn } 3098d2d6836SMatthias Sohn 3108d2d6836SMatthias Sohn @Override 3118d2d6836SMatthias Sohn public boolean isDirectory() { 3128d2d6836SMatthias Sohn return entry.getAttrs().isDir(); 3138d2d6836SMatthias Sohn } 3148d2d6836SMatthias Sohn }); 3158d2d6836SMatthias Sohn } 3168d2d6836SMatthias Sohn return result; 3178d2d6836SMatthias Sohn }); 3188d2d6836SMatthias Sohn } 3198d2d6836SMatthias Sohn 3208d2d6836SMatthias Sohn @Override rmdir(String path)3218d2d6836SMatthias Sohn public void rmdir(String path) throws IOException { 3228d2d6836SMatthias Sohn map(() -> { 3238d2d6836SMatthias Sohn ftp.rm(path); 3248d2d6836SMatthias Sohn return null; 3258d2d6836SMatthias Sohn }); 3268d2d6836SMatthias Sohn } 3278d2d6836SMatthias Sohn 3288d2d6836SMatthias Sohn @Override mkdir(String path)3298d2d6836SMatthias Sohn public void mkdir(String path) throws IOException { 3308d2d6836SMatthias Sohn map(() -> { 3318d2d6836SMatthias Sohn ftp.mkdir(path); 3328d2d6836SMatthias Sohn return null; 3338d2d6836SMatthias Sohn }); 3348d2d6836SMatthias Sohn } 3358d2d6836SMatthias Sohn 3368d2d6836SMatthias Sohn @Override get(String path)3378d2d6836SMatthias Sohn public InputStream get(String path) throws IOException { 3388d2d6836SMatthias Sohn return map(() -> ftp.get(path)); 3398d2d6836SMatthias Sohn } 3408d2d6836SMatthias Sohn 3418d2d6836SMatthias Sohn @Override put(String path)3428d2d6836SMatthias Sohn public OutputStream put(String path) throws IOException { 3438d2d6836SMatthias Sohn return map(() -> ftp.put(path)); 3448d2d6836SMatthias Sohn } 3458d2d6836SMatthias Sohn 3468d2d6836SMatthias Sohn @Override rm(String path)3478d2d6836SMatthias Sohn public void rm(String path) throws IOException { 3488d2d6836SMatthias Sohn map(() -> { 3498d2d6836SMatthias Sohn ftp.rm(path); 3508d2d6836SMatthias Sohn return null; 3518d2d6836SMatthias Sohn }); 3528d2d6836SMatthias Sohn } 3538d2d6836SMatthias Sohn 3548d2d6836SMatthias Sohn @Override rename(String from, String to)3558d2d6836SMatthias Sohn public void rename(String from, String to) throws IOException { 3568d2d6836SMatthias Sohn map(() -> { 3578d2d6836SMatthias Sohn // Plain FTP rename will fail if "to" exists. Jsch knows about 3588d2d6836SMatthias Sohn // the FTP extension "posix-rename@openssh.com", which will 3598d2d6836SMatthias Sohn // remove "to" first if it exists. 3608d2d6836SMatthias Sohn if (hasPosixRename()) { 3618d2d6836SMatthias Sohn ftp.rename(from, to); 3628d2d6836SMatthias Sohn } else if (!to.equals(from)) { 3638d2d6836SMatthias Sohn // Try to remove "to" first. With git, we typically get this 3648d2d6836SMatthias Sohn // when a lock file is moved over the file locked. Note that 3658d2d6836SMatthias Sohn // the check for to being equal to from may still fail in 3668d2d6836SMatthias Sohn // the general case, but for use with JGit's TransportSftp 3678d2d6836SMatthias Sohn // it should be good enough. 3688d2d6836SMatthias Sohn delete(to); 3698d2d6836SMatthias Sohn ftp.rename(from, to); 3708d2d6836SMatthias Sohn } 3718d2d6836SMatthias Sohn return null; 3728d2d6836SMatthias Sohn }); 3738d2d6836SMatthias Sohn } 3748d2d6836SMatthias Sohn 3758d2d6836SMatthias Sohn /** 3768d2d6836SMatthias Sohn * Determine whether the server has the posix-rename extension. 3778d2d6836SMatthias Sohn * 3788d2d6836SMatthias Sohn * @return {@code true} if it is supported, {@code false} otherwise 3798d2d6836SMatthias Sohn * @see <a href= 3808d2d6836SMatthias Sohn * "https://cvsweb.openbsd.org/src/usr.bin/ssh/PROTOCOL?annotate=HEAD">OpenSSH 3818d2d6836SMatthias Sohn * deviations and extensions to the published SSH protocol</a> 3828d2d6836SMatthias Sohn * @see <a href= 3838d2d6836SMatthias Sohn * "http://pubs.opengroup.org/onlinepubs/9699919799/functions/rename.html">stdio.h: 3848d2d6836SMatthias Sohn * rename()</a> 3858d2d6836SMatthias Sohn */ hasPosixRename()3868d2d6836SMatthias Sohn private boolean hasPosixRename() { 3878d2d6836SMatthias Sohn return "1".equals(ftp.getExtension("posix-rename@openssh.com")); //$NON-NLS-1$//$NON-NLS-2$ 3888d2d6836SMatthias Sohn } 3898d2d6836SMatthias Sohn } 3908d2d6836SMatthias Sohn } 391