Repository: apache/hadoop
Pull request #7511: HADOOP-19488 fix the temporary directory creation on Windows
Author: login: sjlee
Labels: name: trunk
name: Common
Created: 2025-03-16T05:12:00Z | Merged: 2025-03-22T06:47:43Z

Use ACL-based permissions for Windows instead of POSIX permissions.

<!--
  Thanks for sending a pull request!
    1. If this is your first time, please read our contributor guidelines: https://cwiki.apache.org/confluence/display/HADOOP/How+To+Contribute
    2. Make sure your PR title starts with JIRA issue id, e.g., 'HADOOP-17799. Your PR title ...'.
-->

### Description of PR
Fix Hadoop RunJar for Windows. The Windows filesystem does not recognize the POSIX permissions, and it was causing the directory creation failure. If the filesystem does not support POSIX permissions, switch to using the user-based, ACL-based permissions that work on Windows.

### How was this patch tested?
I added a test to TestRunJar and ran it first to reproduce the bug on Windows. After I changed RunJar.java, I ran TestRunJar on Windows and confirmed it runs successfully.

### For code changes:

- [x] Does the title or this PR starts with the corresponding JIRA issue id (e.g. 'HADOOP-17799. Your PR title ...')?
- [ ] Object storage: have the integration tests been executed and the endpoint declared according to the connector-specific documentation?
- [ ] If adding new dependencies to the code, are these dependencies licensed in a way that is compatible for inclusion under [ASF 2.0](http://www.apache.org/legal/resolved.html#category-a)?
- [ ] If applicable, have you updated the `LICENSE`, `LICENSE-binary`, `NOTICE-binary` files?




Diff:
diff --git a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
index e527f602cdd31..881dd2defc86f 100644
--- a/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
+++ b/hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
@@ -27,12 +27,17 @@
 import java.net.MalformedURLException;
 import java.net.URL;
 import java.net.URLClassLoader;
+import java.nio.file.FileSystems;
 import java.nio.file.Files;
+import java.nio.file.attribute.AclEntry;
+import java.nio.file.attribute.AclEntryPermission;
 import java.nio.file.attribute.FileAttribute;
-import java.nio.file.attribute.PosixFilePermission;
 import java.nio.file.attribute.PosixFilePermissions;
+import java.nio.file.attribute.UserPrincipal;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Collections;
+import java.util.EnumSet;
 import java.util.Enumeration;
 import java.util.List;
 import java.util.Set;
@@ -51,6 +56,10 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import static java.nio.file.attribute.AclEntryFlag.DIRECTORY_INHERIT;
+import static java.nio.file.attribute.AclEntryFlag.FILE_INHERIT;
+import static java.nio.file.attribute.AclEntryType.ALLOW;
+
 /** Run a Hadoop job jar. */
 @InterfaceAudience.Private
 @InterfaceStability.Unstable
@@ -286,25 +295,19 @@ public void run(String[] args) throws Throwable {
     }
     mainClassName = mainClassName.replaceAll("/", ".");
 
-    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
-    ensureDirectory(tmpDir);
-
     final File workDir;
     try {
-      FileAttribute<Set<PosixFilePermission>> perms = PosixFilePermissions
-          .asFileAttribute(PosixFilePermissions.fromString("rwx------"));
-      workDir = Files.createTempDirectory(tmpDir.toPath(), "hadoop-unjar", perms).toFile();
+      workDir = createWorkDirectory();
     } catch (IOException | SecurityException e) {
       // If user has insufficient perms to write to tmpDir, default
       // "Permission denied" message doesn't specify a filename.
       System.err.println("Error creating temp dir in java.io.tmpdir "
-                         + tmpDir + " due to " + e.getMessage());
+                         + System.getProperty("java.io.tmpdir") + " due to "
+                         + e.getMessage());
       System.exit(-1);
       return;
     }
 
-    ensureDirectory(workDir);
-
     ShutdownHookManager.get().addShutdownHook(
         new Runnable() {
           @Override
@@ -333,6 +336,55 @@ public void run() {
     }
   }
 
+  static File createWorkDirectory() throws IOException {
+    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+    ensureDirectory(tmpDir);
+
+    File workDir = Files.createTempDirectory(tmpDir.toPath(), "hadoop-unjar",
+        directoryPermissions()).toFile();
+    ensureDirectory(workDir);
+    return workDir;
+  }
+
+  private static FileAttribute<?> directoryPermissions() throws IOException {
+    Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
+    if (views.contains("posix")) {
+      return PosixFilePermissions
+          .asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+    } else if (views.contains("acl")) {
+      return userOnly();
+    } else {
+      throw new IOException("unrecognized FileSystem type " +
+          FileSystems.getDefault());
+    }
+  }
+
+  private static FileAttribute<?> userOnly() throws IOException {
+    UserPrincipal user =
+        FileSystems.getDefault()
+            .getUserPrincipalLookupService()
+            .lookupPrincipalByName(System.getProperty("user.name"));
+    List<AclEntry> acl =
+        Collections.singletonList(AclEntry.newBuilder()
+            .setType(ALLOW)
+            .setPrincipal(user)
+            .setPermissions(EnumSet.allOf(AclEntryPermission.class))
+            .setFlags(DIRECTORY_INHERIT, FILE_INHERIT)
+            .build());
+    return
+        new FileAttribute<List<AclEntry>>() {
+          @Override
+          public String name() {
+            return "acl:acl";
+          }
+
+          @Override
+          public List<AclEntry> value() {
+            return acl;
+          }
+        };
+  }
+
   /**
    * Creates a classloader based on the environment that was specified by the
    * user. If HADOOP_USE_CLIENT_CLASSLOADER is specified, it creates an
diff --git a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestRunJar.java b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestRunJar.java
index 7b1e031360a19..39a4e72e95847 100644
--- a/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestRunJar.java
+++ b/hadoop-common-project/hadoop-common/src/test/java/org/apache/hadoop/util/TestRunJar.java
@@ -20,6 +20,7 @@
 import static org.apache.hadoop.util.RunJar.MATCH_ANY;
 import static org.junit.jupiter.api.Assertions.assertEquals;
 import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import static org.junit.jupiter.api.Assertions.fail;
 import static org.mockito.ArgumentMatchers.any;
@@ -197,6 +198,24 @@ private File getUnjarDir(String dirName) {
     return unjarDir;
   }
 
+  /**
+   * Tests the creation of the temp working directory into which the jars are
+   * unjarred.
+   */
+  @Test
+  public void testCreateWorkDirectory() throws Exception {
+    File workDir = null;
+    try {
+      workDir = RunJar.createWorkDirectory();
+
+      assertNotNull(workDir, "Work directory should exist and not null");
+    } finally {
+      if (workDir != null) {
+        FileUtil.fullyDelete(workDir);
+      }
+    }
+  }
+
   /**
    * Tests the client classloader to verify the main class and its dependent
    * class are loaded correctly by the application classloader, and others are


Reviews:
user: login: GauthamBanasandra
state: COMMENTED
author_association: MEMBER
submitted_at: 2025-03-18T19:53:33Z
user: login: sjlee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T04:30:04Z
user: login: sjlee
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T14:54:41Z
user: login: GauthamBanasandra
body: LGTM. Thanks for the contribution @sjlee.
state: APPROVED
author_association: MEMBER
submitted_at: 2025-03-22T06:45:37Z

Review comments:
diff_hunk: @@ -333,6 +336,55 @@ public void run() {
     }
   }
 
+  static File createWorkDirectory() throws IOException {
+    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+    ensureDirectory(tmpDir);
+
+    File workDir = Files.createTempDirectory(tmpDir.toPath(), "hadoop-unjar",
+        directoryPermissions()).toFile();
+    ensureDirectory(workDir);
+    return workDir;
+  }
+
+  private static FileAttribute<?> directoryPermissions() throws IOException {
+    Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
+    if (views.contains("posix")) {
+      return PosixFilePermissions
+          .asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+    } else if (views.contains("acl")) {
+      return userOnly();
+    } else {
path: hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
user: login: GauthamBanasandra
body: Are we sure about the casing here? It's always `posix` and not `POSIX`?
created_at: 2025-03-18T19:51:32Z
author_association: MEMBER
diff_hunk: @@ -333,6 +336,55 @@ public void run() {
     }
   }
 
+  static File createWorkDirectory() throws IOException {
+    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+    ensureDirectory(tmpDir);
+
+    File workDir = Files.createTempDirectory(tmpDir.toPath(), "hadoop-unjar",
+        directoryPermissions()).toFile();
+    ensureDirectory(workDir);
+    return workDir;
+  }
+
+  private static FileAttribute<?> directoryPermissions() throws IOException {
+    Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
+    if (views.contains("posix")) {
+      return PosixFilePermissions
+          .asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+    } else if (views.contains("acl")) {
+      return userOnly();
+    } else {
path: hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
user: login: sjlee
body: Although I don't have the authoritative source code that confirms it, it appears that for all the ones I tested, the views appear to be in lowercase.

- Windows: owner dos acl basic user
- Linux (Ubuntu): owner dos basic posix user unix
- macOS: owner basic posix unix
created_at: 2025-03-19T04:30:04Z
author_association: CONTRIBUTOR
diff_hunk: @@ -333,6 +336,55 @@ public void run() {
     }
   }
 
+  static File createWorkDirectory() throws IOException {
+    File tmpDir = new File(System.getProperty("java.io.tmpdir"));
+    ensureDirectory(tmpDir);
+
+    File workDir = Files.createTempDirectory(tmpDir.toPath(), "hadoop-unjar",
+        directoryPermissions()).toFile();
+    ensureDirectory(workDir);
+    return workDir;
+  }
+
+  private static FileAttribute<?> directoryPermissions() throws IOException {
+    Set<String> views = FileSystems.getDefault().supportedFileAttributeViews();
+    if (views.contains("posix")) {
+      return PosixFilePermissions
+          .asFileAttribute(PosixFilePermissions.fromString("rwx------"));
+    } else if (views.contains("acl")) {
+      return userOnly();
+    } else {
path: hadoop-common-project/hadoop-common/src/main/java/org/apache/hadoop/util/RunJar.java
user: login: sjlee
body: One can also check that is the case at least from the OpenJDK source.
created_at: 2025-03-19T14:54:41Z
author_association: CONTRIBUTOR

PR comments:
user: login: hadoop-yetus
created_at: 2025-03-16T08:54:11Z
body: :confetti_ball: **+1 overall**






| Vote | Subsystem | Runtime |  Logfile | Comment |
|:----:|----------:|--------:|:--------:|:-------:|
| +0 :ok: |  reexec  |  17m 41s |  |  Docker mode activated.  |
|||| _ Prechecks _ |
| +1 :green_heart: |  dupname  |   0m  0s |  |  No case conflicting files found.  |
| +0 :ok: |  codespell  |   0m  0s |  |  codespell was not available.  |
| +0 :ok: |  detsecrets  |   0m  0s |  |  detect-secrets was not available.  |
| +1 :green_heart: |  @author  |   0m  0s |  |  The patch does not contain any @author tags.  |
| +1 :green_heart: |  test4tests  |   0m  0s |  |  The patch appears to include 1 new or modified test files.  |
|||| _ trunk Compile Tests _ |
| +1 :green_heart: |  mvninstall  |  35m 35s |  |  trunk passed  |
| +1 :green_heart: |  compile  |  15m 49s |  |  trunk passed with JDK Ubuntu-11.0.26+4-post-Ubuntu-1ubuntu120.04  |
| +1 :green_heart: |  compile  |  13m 57s |  |  trunk passed with JDK Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06  |
| +1 :green_heart: |  checkstyle  |   1m 16s |  |  trunk passed  |
| +1 :green_heart: |  mvnsite  |   1m 38s |  |  trunk passed  |
| +1 :green_heart: |  javadoc  |   1m 14s |  |  trunk passed with JDK Ubuntu-11.0.26+4-post-Ubuntu-1ubuntu120.04  |
| +1 :green_heart: |  javadoc  |   0m 52s |  |  trunk passed with JDK Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06  |
| +1 :green_heart: |  spotbugs  |   2m 34s |  |  trunk passed  |
| +1 :green_heart: |  shadedclient  |  36m 22s |  |  branch has no errors when building and testing our client artifacts.  |
|||| _ Patch Compile Tests _ |
| +1 :green_heart: |  mvninstall  |   0m 55s |  |  the patch passed  |
| +1 :green_heart: |  compile  |  15m  8s |  |  the patch passed with JDK Ubuntu-11.0.26+4-post-Ubuntu-1ubuntu120.04  |
| +1 :green_heart: |  javac  |  15m  8s |  |  the patch passed  |
| +1 :green_heart: |  compile  |  13m 57s |  |  the patch passed with JDK Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06  |
| +1 :green_heart: |  javac  |  13m 57s |  |  the patch passed  |
| +1 :green_heart: |  blanks  |   0m  0s |  |  The patch has no blanks issues.  |
| +1 :green_heart: |  checkstyle  |   1m 14s |  |  the patch passed  |
| +1 :green_heart: |  mvnsite  |   1m 36s |  |  the patch passed  |
| +1 :green_heart: |  javadoc  |   1m 11s |  |  the patch passed with JDK Ubuntu-11.0.26+4-post-Ubuntu-1ubuntu120.04  |
| +1 :green_heart: |  javadoc  |   0m 54s |  |  the patch passed with JDK Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06  |
| +1 :green_heart: |  spotbugs  |   2m 40s |  |  the patch passed  |
| +1 :green_heart: |  shadedclient  |  38m  9s |  |  patch has no errors when building and testing our client artifacts.  |
|||| _ Other Tests _ |
| +1 :green_heart: |  unit  |  16m  9s |  |  hadoop-common in the patch passed.  |
| +1 :green_heart: |  asflicense  |   0m 59s |  |  The patch does not generate ASF License warnings.  |
|  |   | 220m 49s |  |  |


| Subsystem | Report/Notes |
|----------:|:-------------|
| Docker | ClientAPI=1.48 ServerAPI=1.48 base: https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-7511/1/artifact/out/Dockerfile |
| GITHUB PR | https://github.com/apache/hadoop/pull/7511 |
| Optional Tests | dupname asflicense compile javac javadoc mvninstall mvnsite unit shadedclient spotbugs checkstyle codespell detsecrets |
| uname | Linux 0c598f5bd0ea 5.15.0-130-generic #140-Ubuntu SMP Wed Dec 18 17:59:53 UTC 2024 x86_64 x86_64 x86_64 GNU/Linux |
| Build tool | maven |
| Personality | dev-support/bin/hadoop.sh |
| git revision | trunk / c70b276a33c6a4cbc7ab7951b005cadd0353aa52 |
| Default Java | Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06 |
| Multi-JDK versions | /usr/lib/jvm/java-11-openjdk-amd64:Ubuntu-11.0.26+4-post-Ubuntu-1ubuntu120.04 /usr/lib/jvm/java-8-openjdk-amd64:Private Build-1.8.0_442-8u442-b06~us1-0ubuntu1~20.04-b06 |
|  Test Results | https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-7511/1/testReport/ |
| Max. process+thread count | 2151 (vs. ulimit of 5500) |
| modules | C: hadoop-common-project/hadoop-common U: hadoop-common-project/hadoop-common |
| Console output | https://ci-hadoop.apache.org/job/hadoop-multibranch/job/PR-7511/1/console |
| versions | git=2.25.1 maven=3.6.3 spotbugs=4.2.2 |
| Powered by | Apache Yetus 0.14.0 https://yetus.apache.org |


This message was automatically generated.


author_association: NONE
user: login: sjlee
created_at: 2025-03-21T16:40:38Z
body: Any additional feedback or comments? Thanks.
author_association: CONTRIBUTOR
user: login: GauthamBanasandra
created_at: 2025-03-22T06:44:55Z
body: The Jenkins CI run looks good -

![image](https://github.com/user-attachments/assets/73804b6b-79bf-4c75-89c5-064c0b227edc)

Logs - [archive.zip](https://github.com/user-attachments/files/19401272/archive.zip)
author_association: MEMBER
user: login: slfan1989
created_at: 2025-03-22T08:34:54Z
body: @sjlee @GauthamBanasandra Thanks for your contribution! Can we backport this pr to branch-3.4?
author_association: CONTRIBUTOR