为什么 Linux 不执行具有 Java 执行权限的 bash 脚本?
Why won't Linux execute a bash script that has execution permission from Java?
有很多问题看起来与此类似,但其中 none 似乎完全相同,而且他们的所有解决方案都不适合我,所以这里是...
我有一个 Java 程序试图自动更新自身。它是这样工作的:
- 启动时,bash 脚本会检查特定位置是否存在 zip 文件。
- 如果它存在,而不是 运行 更新 "real" 主 class,它 运行 是更新程序 Java class。
- 此 class 将在临时位置解压缩 zip,然后从该临时位置在单独的进程中再次启动自身。
- 新进程启动后,旧进程退出。
- 新进程尝试再次将 zip 解压到以前的位置(现在什么都没有 运行ning,因此可以替换)。
- 成功后,它会删除 zip 文件并再次 运行 原始 bash 脚本。
如果一切正常,bash 脚本现在将启动 Java 主 class。
此列表中的所有内容实际上都有效,除了我尝试在最后执行 bash 脚本的最后一步。
bash 文件权限如下所示(在我 运行 自动更新程序之前和之后):
-rwxr-xr-x
据我所知,运行宁这个文件应该没有权限问题(事实上,我可以 运行 在进程崩溃后立即手动它)。
这是我得到的错误:
java.io.IOException: Cannot run program "/home/xxx/build/image/bin/run": error=13, Permission denied
at java.base/java.lang.ProcessBuilder.start(Unknown Source)
at java.base/java.lang.ProcessBuilder.start(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at my_mod/my.Main.main(Unknown Source)
我写了一个 "process bomb",它的工作原理类似,但确实有效:
- 从 运行 启动一个 bash 脚本开始 class
主要class使用以下代码再次启动bash脚本:
// DO NOT RUN THIS AT HOME!
// IT WILL START LOTS OF PROCESSES INFINITELY
// if you do want to try it, have "killall -9 java" ready!
try {
Runtime.getRuntime().exec( path )
System.exit(0);
} catch ( Exception e ) {
e.printStackTrace();
}
所以我认为这不是某种 Linux 防止像这样的进程炸弹的保护。
但我完全不明白为什么 Linux 在更新后仍然拒绝访问 运行ning 那个 bash 脚本。
我的 OS 是 Ubuntu 19.
我如何找出它拒绝访问的原因,更重要的是,我该如何解决它?
我终于找到了问题的根本原因:是文件权限问题,但是不是错误消息中提到的文件!
我解压新应用程序更新的方式是通过在 Java 中编写一个简单的 "unzipper",基本实现如下:
try (var zip = new ZipInputStream(
new BufferedInputStream(
new FileInputStream(newVersionZipFile), 4096))) {
var zipEntry = zip.getNextEntry();
if (zipEntry == null) {
throw new IllegalStateException("Expected at least one entry in the zip file: " + newVersionZipFile);
}
var topEntryName = zipEntry.getName();
zipEntry = zip.getNextEntry();
while (zipEntry != null) {
var file = fileFor(zipEntry, destinationDir, topEntryName);
if (isDirectory(zipEntry)) {
var ok = file.mkdir();
if (!ok) throw new IllegalStateException("Cannot create new directory: " + file);
} else {
Files.copy(zip, file.toPath());
}
zipEntry = zip.getNextEntry();
}
}
这在大多数情况下确实有效,但它有一个大问题:它没有考虑文件权限。我已经注意到这个问题并在解压后将bin
目录中的所有内容都设置为可执行文件,但我没有想到的是jlink创建的minimal-JVM中有更多可执行文件。而且因为该应用程序实际上大部分都能正常工作,所以我觉得一切都很好!
使用 tree
命令,我查看了使用 Linux 的 unzip
解压缩后的目录树,并使用我的自定义 Java 解压缩。我注意到,在 unzip
中,除了 bin
目录中的文件外,以下文件也具有执行权限:
├── [drwxrwxr-x] lib
│ ├── [-rw-rw-r--] classlist
│ ├── [-rw-rw-r--] javafx.properties
│ ├── [-rw-rw-r--] javafx-swt.jar
│ ├── [-rwxrwxr-x] jexec
│ ├── [-rw-rw-r--] jrt-fs.jar
│ ├── [-rwxrwxr-x] jspawnhelper
...
相比之下,Java 解压器解压后的树看起来像这样:
├── [drwxrwxr-x] lib
│ ├── [-rw-rw-r--] classlist
│ ├── [-rw-rw-r--] javafx.properties
│ ├── [-rw-rw-r--] javafx-swt.jar
│ ├── [-rw-rw-r--] jexec
│ ├── [-rw-rw-r--] jrt-fs.jar
│ ├── [-rw-rw-r--] jspawnhelper
...
注意到两个文件应该具有执行权限:jexec
和 jspawnhelper
,这两个文件的名称都强烈暗示它们与执行其他进程有关 :D
为了验证这个假设,我让解压器也将这两个文件的权限更改为可执行,现在一切正常!
我现在的挑战是如何在解压缩文件时真正保留所有文件的文件权限,这是另一个难题,因为没有标准方法从标准 zip 获取文件权限(这就是为什么 Java API for zips 没有该功能)。有大部分工作的外部库,所以我可能最终会使用其中一个...但这是另一个话题。
有很多问题看起来与此类似,但其中 none 似乎完全相同,而且他们的所有解决方案都不适合我,所以这里是...
我有一个 Java 程序试图自动更新自身。它是这样工作的:
- 启动时,bash 脚本会检查特定位置是否存在 zip 文件。
- 如果它存在,而不是 运行 更新 "real" 主 class,它 运行 是更新程序 Java class。
- 此 class 将在临时位置解压缩 zip,然后从该临时位置在单独的进程中再次启动自身。
- 新进程启动后,旧进程退出。
- 新进程尝试再次将 zip 解压到以前的位置(现在什么都没有 运行ning,因此可以替换)。
- 成功后,它会删除 zip 文件并再次 运行 原始 bash 脚本。
如果一切正常,bash 脚本现在将启动 Java 主 class。
此列表中的所有内容实际上都有效,除了我尝试在最后执行 bash 脚本的最后一步。
bash 文件权限如下所示(在我 运行 自动更新程序之前和之后):
-rwxr-xr-x
据我所知,运行宁这个文件应该没有权限问题(事实上,我可以 运行 在进程崩溃后立即手动它)。
这是我得到的错误:
java.io.IOException: Cannot run program "/home/xxx/build/image/bin/run": error=13, Permission denied
at java.base/java.lang.ProcessBuilder.start(Unknown Source)
at java.base/java.lang.ProcessBuilder.start(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at java.base/java.lang.Runtime.exec(Unknown Source)
at my_mod/my.Main.main(Unknown Source)
我写了一个 "process bomb",它的工作原理类似,但确实有效:
- 从 运行 启动一个 bash 脚本开始 class
主要class使用以下代码再次启动bash脚本:
// DO NOT RUN THIS AT HOME! // IT WILL START LOTS OF PROCESSES INFINITELY // if you do want to try it, have "killall -9 java" ready! try { Runtime.getRuntime().exec( path ) System.exit(0); } catch ( Exception e ) { e.printStackTrace(); }
所以我认为这不是某种 Linux 防止像这样的进程炸弹的保护。
但我完全不明白为什么 Linux 在更新后仍然拒绝访问 运行ning 那个 bash 脚本。
我的 OS 是 Ubuntu 19.
我如何找出它拒绝访问的原因,更重要的是,我该如何解决它?
我终于找到了问题的根本原因:是文件权限问题,但是不是错误消息中提到的文件!
我解压新应用程序更新的方式是通过在 Java 中编写一个简单的 "unzipper",基本实现如下:
try (var zip = new ZipInputStream(
new BufferedInputStream(
new FileInputStream(newVersionZipFile), 4096))) {
var zipEntry = zip.getNextEntry();
if (zipEntry == null) {
throw new IllegalStateException("Expected at least one entry in the zip file: " + newVersionZipFile);
}
var topEntryName = zipEntry.getName();
zipEntry = zip.getNextEntry();
while (zipEntry != null) {
var file = fileFor(zipEntry, destinationDir, topEntryName);
if (isDirectory(zipEntry)) {
var ok = file.mkdir();
if (!ok) throw new IllegalStateException("Cannot create new directory: " + file);
} else {
Files.copy(zip, file.toPath());
}
zipEntry = zip.getNextEntry();
}
}
这在大多数情况下确实有效,但它有一个大问题:它没有考虑文件权限。我已经注意到这个问题并在解压后将bin
目录中的所有内容都设置为可执行文件,但我没有想到的是jlink创建的minimal-JVM中有更多可执行文件。而且因为该应用程序实际上大部分都能正常工作,所以我觉得一切都很好!
使用 tree
命令,我查看了使用 Linux 的 unzip
解压缩后的目录树,并使用我的自定义 Java 解压缩。我注意到,在 unzip
中,除了 bin
目录中的文件外,以下文件也具有执行权限:
├── [drwxrwxr-x] lib
│ ├── [-rw-rw-r--] classlist
│ ├── [-rw-rw-r--] javafx.properties
│ ├── [-rw-rw-r--] javafx-swt.jar
│ ├── [-rwxrwxr-x] jexec
│ ├── [-rw-rw-r--] jrt-fs.jar
│ ├── [-rwxrwxr-x] jspawnhelper
...
相比之下,Java 解压器解压后的树看起来像这样:
├── [drwxrwxr-x] lib
│ ├── [-rw-rw-r--] classlist
│ ├── [-rw-rw-r--] javafx.properties
│ ├── [-rw-rw-r--] javafx-swt.jar
│ ├── [-rw-rw-r--] jexec
│ ├── [-rw-rw-r--] jrt-fs.jar
│ ├── [-rw-rw-r--] jspawnhelper
...
注意到两个文件应该具有执行权限:jexec
和 jspawnhelper
,这两个文件的名称都强烈暗示它们与执行其他进程有关 :D
为了验证这个假设,我让解压器也将这两个文件的权限更改为可执行,现在一切正常!
我现在的挑战是如何在解压缩文件时真正保留所有文件的文件权限,这是另一个难题,因为没有标准方法从标准 zip 获取文件权限(这就是为什么 Java API for zips 没有该功能)。有大部分工作的外部库,所以我可能最终会使用其中一个...但这是另一个话题。