diff --git a/core/src/main/java/hudson/Util.java b/core/src/main/java/hudson/Util.java index 9127a26a0d..16eed1aafc 100644 --- a/core/src/main/java/hudson/Util.java +++ b/core/src/main/java/hudson/Util.java @@ -90,6 +90,8 @@ import java.util.regex.Pattern; import java.nio.charset.Charset; import com.sun.jna.Native; +import com.sun.jna.Memory; +import com.sun.jna.NativeLong; /** * Various utility methods that don't have more proper home. @@ -958,39 +960,77 @@ public class Util { * If there's a prior symlink at baseDir+symlinkPath, it will be overwritten. */ public static void createSymlink(File baseDir, String targetPath, String symlinkPath, TaskListener listener) throws InterruptedException { - if(!isWindows() && !NO_SYMLINK) { - try { - // if a file or a directory exists here, delete it first. - // try simple delete first (whether exists() or not, as it may be symlink pointing - // to non-existent target), but fallback to "rm -rf" to delete non-empty dir. - File symlinkFile = new File(baseDir, symlinkPath); - if (!symlinkFile.delete() && symlinkFile.exists()) - // ignore a failure. - new LocalProc(new String[]{"rm","-rf", symlinkPath},new String[0],listener.getLogger(), baseDir).join(); + if(isWindows() || NO_SYMLINK) return; - int r; - if (!SYMLINK_ESCAPEHATCH) { - try { - r = GNUCLibrary.LIBC.symlink(targetPath,symlinkFile.getAbsolutePath()); - if (r!=0) - r = Native.getLastError(); - } catch (LinkageError e) { - // if JNA is unavailable, fall back. - // we still prefer to try JNA first as PosixAPI supports even smaller platforms. - r = PosixAPI.get().symlink(targetPath,symlinkFile.getAbsolutePath()); - } - } else // escape hatch, until we know that the above works well. - r = new LocalProc(new String[]{ - "ln","-s", targetPath, symlinkPath}, - new String[0],listener.getLogger(), baseDir).join(); - if(r!=0) - listener.getLogger().println("ln failed: "+r); - } catch (IOException e) { - PrintStream log = listener.getLogger(); - log.println("ln failed"); - Util.displayIOException(e,listener); - e.printStackTrace( log ); + try { + // if a file or a directory exists here, delete it first. + // try simple delete first (whether exists() or not, as it may be symlink pointing + // to non-existent target), but fallback to "rm -rf" to delete non-empty dir. + File symlinkFile = new File(baseDir, symlinkPath); + if (!symlinkFile.delete() && symlinkFile.exists()) + // ignore a failure. + new LocalProc(new String[]{"rm","-rf", symlinkPath},new String[0],listener.getLogger(), baseDir).join(); + + int r; + if (!SYMLINK_ESCAPEHATCH) { + try { + r = GNUCLibrary.LIBC.symlink(targetPath,symlinkFile.getAbsolutePath()); + if (r!=0) + r = Native.getLastError(); + } catch (LinkageError e) { + // if JNA is unavailable, fall back. + // we still prefer to try JNA first as PosixAPI supports even smaller platforms. + r = PosixAPI.get().symlink(targetPath,symlinkFile.getAbsolutePath()); + } + } else // escape hatch, until we know that the above works well. + r = new LocalProc(new String[]{ + "ln","-s", targetPath, symlinkPath}, + new String[0],listener.getLogger(), baseDir).join(); + if(r!=0) + listener.getLogger().println("ln failed: "+r); + } catch (IOException e) { + PrintStream log = listener.getLogger(); + log.println("ln failed"); + Util.displayIOException(e,listener); + e.printStackTrace( log ); + } + } + + /** + * Resolves symlink, if the given file is a symlink. Otherwise return null. + *
+ * If the resolution fails, report an error. + * + * @param listener + * If we rely on an external command to resolve symlink, this is it. + * (TODO: try readlink(1) available on some platforms) + */ + public static String resolveSymlink(File link, TaskListener listener) throws InterruptedException, IOException { + if(isWindows()) return null; + + String filename = link.getAbsolutePath(); + try { + for (int sz=512; sz < 65536; sz*=2) { + Memory m = new Memory(sz); + int r = GNUCLibrary.LIBC.readlink(filename,m,new NativeLong(sz)); + if (r<0) { + if (r==22/*EINVAL --- but is this really portable?*/) + return null; // this means it's not a symlink + throw new IOException("Failed to readlink "+link+" error="+Native.getLastError()); + } + if (r==sz) + continue; // buffer too small + + byte[] buf = new byte[r]; + m.read(0,buf,0,r); + return new String(buf); } + // something is wrong. It can't be this long! + throw new IOException("Symlink too long: "+link); + } catch (LinkageError e) { + // if JNA is unavailable, fall back. + // we still prefer to try JNA first as PosixAPI supports even smaller platforms. + return PosixAPI.get().readlink(filename); } } diff --git a/core/src/main/java/hudson/util/jna/GNUCLibrary.java b/core/src/main/java/hudson/util/jna/GNUCLibrary.java index 7353339738..8562034e29 100644 --- a/core/src/main/java/hudson/util/jna/GNUCLibrary.java +++ b/core/src/main/java/hudson/util/jna/GNUCLibrary.java @@ -27,6 +27,8 @@ import com.sun.jna.Library; import com.sun.jna.StringArray; import com.sun.jna.Pointer; import com.sun.jna.Native; +import com.sun.jna.Memory; +import com.sun.jna.NativeLong; import com.sun.jna.ptr.IntByReference; import org.jvnet.libpam.impl.CLibrary.passwd; @@ -87,5 +89,15 @@ public interface GNUCLibrary extends Library { */ int symlink(String oldname, String newname); + /** + * Read a symlink. The name will be copied into the specified memory, and returns the number of + * bytes copied. The string is not null-terminated. + * + * @return + * if the return value equals size, the caller needs to retry with a bigger buffer. + * If -1, error. + */ + int readlink(String filename, Memory buffer, NativeLong size); + public static final GNUCLibrary LIBC = (GNUCLibrary) Native.loadLibrary("c",GNUCLibrary.class); } diff --git a/core/src/test/java/hudson/UtilTest.java b/core/src/test/java/hudson/UtilTest.java index 6ef039f63e..7dfe978de4 100644 --- a/core/src/test/java/hudson/UtilTest.java +++ b/core/src/test/java/hudson/UtilTest.java @@ -28,6 +28,10 @@ import junit.framework.TestCase; import java.util.Map; import java.util.HashMap; import java.util.Locale; +import java.io.File; + +import hudson.model.Hudson; +import hudson.util.StreamTaskListener; /** * @author Kohsuke Kawaguchi @@ -138,4 +142,25 @@ public class UtilTest extends TestCase { assertEquals("Parsing empty string did not return the default value", 10, Util.tryParseNumber("", 10).intValue()); assertEquals("Parsing null string did not return the default value", 10, Util.tryParseNumber(null, 10).intValue()); } + + public void testSymlink() throws Exception { + if (Hudson.isWindows()) return; + + StreamTaskListener l = new StreamTaskListener(System.out); + File d = Util.createTempDir(); + try { + new FilePath(new File(d, "a")).touch(0); + Util.createSymlink(d,"a","x", l); + assertEquals("a",Util.resolveSymlink(new File(d,"x"),l)); + + // test a long name + StringBuilder buf = new StringBuilder(768); + for( int i=0; i<768; i++) + buf.append('0'+(i%10)); + Util.createSymlink(d,buf.toString(),"x", l); + assertEquals(buf.toString(),Util.resolveSymlink(new File(d,"x"),l)); + } finally { + Util.deleteRecursive(d); + } + } }