Skip to content

Commit 4e55954

Browse files
authored
fix: avoid replacing active Windows command launcher (jbangdev#2617)
Signed-off-by: Arnab Nandy <arnab_nandy7@yahoo.com>
1 parent f4e7d6d commit 4e55954

6 files changed

Lines changed: 186 additions & 2 deletions

File tree

docs/modules/ROOT/pages/installation.adoc

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,17 @@ For example `iex "& { $(iwr -useb https://ps.jbang.dev) } properties@jbangdev"`
7474

7575
Unzip the https://github.com/jbangdev/jbang/releases/latest[latest binary release], add the `jbang-<version>/bin` folder to your `$PATH` and you are set.
7676

77+
=== Updating a JBang-managed installation
78+
79+
If JBang was installed using the universal installation script, update it with:
80+
81+
[source,shell]
82+
----
83+
jbang version --update
84+
----
85+
86+
When JBang was installed using a package manager such as SDKMAN, Homebrew, Chocolatey, or Scoop, use that package manager's upgrade command instead.
87+
7788
=== Wrapper install
7889

7990
If you would like to have `jbang` available in a local directory and committed into a source code repository (akin to Maven and Gradle wrappers) you can use the `jbang wrapper` command.
@@ -623,4 +634,4 @@ The following variables are set *by* the scripts for use by JBang's Java code an
623634
| `JBANG_LAUNCH_CMD`
624635
| Path to the script that was invoked.
625636

626-
|===
637+
|===

src/main/java/dev/jbang/cli/App.java

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,14 +253,18 @@ public static boolean installJBang(boolean force) throws IOException {
253253
return true;
254254
}
255255

256-
private static void copyJBangFiles(Path from, Path to) throws IOException {
256+
static void copyJBangFiles(Path from, Path to) throws IOException {
257257
to.toFile().mkdirs();
258258
Stream.of("jbang", "jbang.cmd", "jbang.ps1", "jbang.jar")
259259
.map(Paths::get)
260260
.forEach(f -> {
261261
try {
262262
Path fromp = from.resolve(f);
263263
Path top = to.resolve(f);
264+
if (Util.isWindows() && f.endsWith("jbang.cmd") && Files.isRegularFile(top)) {
265+
top = top.resolveSibling(top.getFileName() + ".new");
266+
Util.verboseMsg("Staging the Windows command launcher update: " + top);
267+
}
264268
if (f.endsWith("jbang.jar")) {
265269
if (!Files.isReadable(fromp)) {
266270
fromp = from.resolve(".jbang/jbang.jar");
@@ -275,6 +279,32 @@ private static void copyJBangFiles(Path from, Path to) throws IOException {
275279
throw new ExitException(EXIT_GENERIC_ERROR, "Could not copy " + f.toString(), e);
276280
}
277281
});
282+
if (Util.isWindows() && Util.getShell() != Util.Shell.cmd) {
283+
replaceStagedCmdLauncher(to);
284+
}
285+
}
286+
287+
static String cmdLauncherUpdateCommand() {
288+
if (!Util.isWindows() || Util.getShell() != Util.Shell.cmd) {
289+
return null;
290+
}
291+
Path launcher = Settings.getConfigBinDir().resolve("jbang.cmd");
292+
Path launcherUpdate = launcher.resolveSibling(launcher.getFileName() + ".new");
293+
if (!Files.isRegularFile(launcherUpdate)) {
294+
return null;
295+
}
296+
String moveCommand = CommandBuffer.of("move", "/y", launcherUpdate.toString(), launcher.toString())
297+
.shell(Util.Shell.cmd)
298+
.asCommandLine();
299+
return moveCommand + " > nul 2>&1 && exit /b 0 || exit /b 1";
300+
}
301+
302+
private static void replaceStagedCmdLauncher(Path binDir) throws IOException {
303+
Path launcher = binDir.resolve("jbang.cmd");
304+
Path launcherUpdate = launcher.resolveSibling(launcher.getFileName() + ".new");
305+
if (Files.isRegularFile(launcherUpdate)) {
306+
Files.move(launcherUpdate, launcher, StandardCopyOption.REPLACE_EXISTING);
307+
}
278308
}
279309
}
280310

src/main/java/dev/jbang/cli/Version.java

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,14 @@ public Integer doCall() {
5050
System.err.println("Native Image: " + JavaUtil.inNativeImage());
5151
}
5252

53+
if (update) {
54+
String cmdLauncherUpdate = App.AppInstall.cmdLauncherUpdateCommand();
55+
if (cmdLauncherUpdate != null) {
56+
System.out.println(cmdLauncherUpdate);
57+
return EXIT_EXECUTE;
58+
}
59+
}
60+
5361
return EXIT_OK;
5462
}
5563
}

src/main/java/dev/jbang/util/Util.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ public class Util {
8484

8585
public static final String JBANG_JDK_VENDOR = "JBANG_JDK_VENDOR";
8686
public static final String JBANG_RUNTIME_SHELL = "JBANG_RUNTIME_SHELL";
87+
public static final String JBANG_LAUNCH_CMD = "JBANG_LAUNCH_CMD";
8788
public static final String JBANG_STDIN_NOTTY = "JBANG_STDIN_NOTTY";
8889
public static final String JBANG_PREFER_GUI = "JBANG_PREFER_GUI";
8990
private static final String JBANG_DOWNLOAD_SOURCES = "JBANG_DOWNLOAD_SOURCES";

src/test/java/dev/jbang/cli/TestApp.java

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616
import org.junit.Assert;
1717
import org.junit.jupiter.api.Test;
1818
import org.junit.jupiter.api.Timeout;
19+
import org.junit.jupiter.api.condition.EnabledOnOs;
20+
import org.junit.jupiter.api.condition.OS;
1921
import org.junit.jupiter.api.io.TempDir;
2022

2123
import dev.jbang.BaseTest;
@@ -60,6 +62,55 @@ void testHasJBangSetup(@TempDir Path tempDir) throws IOException {
6062
assertThat(App.AppSetup.hasJBangSetup(rcFile), is(true));
6163
}
6264

65+
@Test
66+
@EnabledOnOs(OS.WINDOWS)
67+
void testUpdateStagesWindowsCmdLauncher(@TempDir Path tempDir) throws IOException, InterruptedException {
68+
Path source = Files.createDirectory(tempDir.resolve("source"));
69+
Path target = Settings.getConfigBinDir();
70+
Files.createDirectories(target);
71+
Files.writeString(source.resolve("jbang"), "new sh");
72+
Files.writeString(source.resolve("jbang.cmd"), "new cmd");
73+
Files.writeString(source.resolve("jbang.ps1"), "new ps1");
74+
Files.writeString(source.resolve("jbang.jar"), "new jar");
75+
Files.writeString(target.resolve("jbang.cmd"), "running cmd");
76+
Files.writeString(target.resolve("jbang.jar"), "running jar");
77+
environmentVariables.set(Util.JBANG_RUNTIME_SHELL, "cmd");
78+
79+
App.AppInstall.copyJBangFiles(source, target);
80+
81+
assertThat(Files.readString(target.resolve("jbang.cmd")), is("running cmd"));
82+
assertThat(Files.readString(target.resolve("jbang.cmd.new")), is("new cmd"));
83+
assertThat(Files.readString(target.resolve("jbang")), is("new sh"));
84+
assertThat(Files.readString(target.resolve("jbang.ps1")), is("new ps1"));
85+
assertThat(Files.readString(target.resolve("jbang.jar")), is("running jar"));
86+
assertThat(Files.readString(target.resolve("jbang.jar.new")), is("new jar"));
87+
88+
String updateCommand = App.AppInstall.cmdLauncherUpdateCommand();
89+
assertThat(updateCommand, notNullValue());
90+
Process process = new ProcessBuilder("cmd", "/d", "/c", updateCommand).start();
91+
assertThat(process.waitFor(), is(0));
92+
assertThat(Files.readString(target.resolve("jbang.cmd")), is("new cmd"));
93+
assertThat(target.resolve("jbang.cmd.new").toFile(), not(anExistingFile()));
94+
}
95+
96+
@Test
97+
@EnabledOnOs(OS.WINDOWS)
98+
void testUpdateReplacesStagedCmdLauncherOutsideCmd(@TempDir Path tempDir) throws IOException {
99+
Path source = Files.createDirectory(tempDir.resolve("source"));
100+
Path target = Files.createDirectory(tempDir.resolve("target"));
101+
Files.writeString(source.resolve("jbang"), "new sh");
102+
Files.writeString(source.resolve("jbang.cmd"), "new cmd");
103+
Files.writeString(source.resolve("jbang.ps1"), "new ps1");
104+
Files.writeString(source.resolve("jbang.jar"), "new jar");
105+
Files.writeString(target.resolve("jbang.cmd"), "old cmd");
106+
environmentVariables.set(Util.JBANG_RUNTIME_SHELL, "powershell");
107+
108+
App.AppInstall.copyJBangFiles(source, target);
109+
110+
assertThat(Files.readString(target.resolve("jbang.cmd")), is("new cmd"));
111+
assertThat(target.resolve("jbang.cmd.new").toFile(), not(anExistingFile()));
112+
}
113+
63114
@Test
64115
void testAppInstallFile() throws Exception {
65116
String src = examplesTestFolder.resolve("with space/helloworld.java").toString();
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package dev.jbang.cli;
2+
3+
import static org.junit.jupiter.api.Assertions.assertEquals;
4+
import static org.junit.jupiter.api.Assertions.assertFalse;
5+
6+
import java.io.IOException;
7+
import java.io.InputStream;
8+
import java.nio.charset.StandardCharsets;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.nio.file.Paths;
12+
import java.util.ArrayList;
13+
import java.util.HashMap;
14+
import java.util.List;
15+
import java.util.Map;
16+
import java.util.jar.Attributes;
17+
import java.util.jar.JarEntry;
18+
import java.util.jar.JarOutputStream;
19+
import java.util.jar.Manifest;
20+
21+
import org.junit.jupiter.api.BeforeEach;
22+
import org.junit.jupiter.api.Test;
23+
import org.junit.jupiter.api.condition.EnabledOnOs;
24+
import org.junit.jupiter.api.condition.OS;
25+
26+
@EnabledOnOs(OS.WINDOWS)
27+
class TestPowerShellSelfUpdate extends AbstractScriptTest {
28+
29+
@BeforeEach
30+
void checkPowerShell() {
31+
requirePowerShell();
32+
}
33+
34+
@Test
35+
void replacesRunningPowerShellLauncher() throws Exception {
36+
Path binDir = Files.createDirectories(tempDir.resolve("bin"));
37+
Path launcher = binDir.resolve("jbang.ps1");
38+
Files.copy(PS1_SCRIPT, launcher);
39+
createUpdaterJar(binDir.resolve("jbang.jar"));
40+
41+
Map<String, String> env = new HashMap<>(System.getenv());
42+
env.put("JAVA_HOME", System.getProperty("java.home"));
43+
env.put("JBANG_DIR", tempDir.resolve("jbang-home").toString());
44+
env.put("JBANG_CACHE_DIR", tempDir.resolve("cache").toString());
45+
env.put("JBANG_NO_VERSION_CHECK", "true");
46+
47+
List<String> command = new ArrayList<>();
48+
command.add(psCommand);
49+
command.add("-NoProfile");
50+
command.add("-ExecutionPolicy");
51+
command.add("Bypass");
52+
command.add("-File");
53+
command.add(launcher.toString());
54+
RunResult result = runProcess(command, env);
55+
56+
assertEquals(0, result.exitCode, result.stderr);
57+
assertEquals("# updated while running\n", Files.readString(launcher).replace("\r\n", "\n"));
58+
assertFalse(Files.exists(launcher.resolveSibling("jbang.ps1.new")));
59+
}
60+
61+
private static void createUpdaterJar(Path jar) throws IOException {
62+
Manifest manifest = new Manifest();
63+
manifest.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
64+
manifest.getMainAttributes().put(Attributes.Name.MAIN_CLASS, PowerShellUpdater.class.getName());
65+
String classResource = PowerShellUpdater.class.getName().replace('.', '/') + ".class";
66+
try (InputStream input = PowerShellUpdater.class.getClassLoader().getResourceAsStream(classResource);
67+
JarOutputStream output = new JarOutputStream(Files.newOutputStream(jar), manifest)) {
68+
if (input == null) {
69+
throw new IOException("Could not find test updater class: " + classResource);
70+
}
71+
output.putNextEntry(new JarEntry(classResource));
72+
input.transferTo(output);
73+
output.closeEntry();
74+
}
75+
}
76+
77+
public static class PowerShellUpdater {
78+
public static void main(String[] args) throws IOException {
79+
Path launcher = Paths.get(System.getenv("JBANG_LAUNCH_CMD"));
80+
Files.write(launcher, "# updated while running\n".getBytes(StandardCharsets.UTF_8));
81+
}
82+
}
83+
}

0 commit comments

Comments
 (0)