Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,3 +2,6 @@
build
.idea
RuneLiteSetup*.exe
*_jre.tar.gz
*_jre.zip
jdk-*-jre
3 changes: 3 additions & 0 deletions native/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
build-aarch64
build-x64
build-x86
115 changes: 115 additions & 0 deletions src/main/java/net/runelite/launcher/DockMenu.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
/*
* Copyright (c) 2026, Nick <nickspaghettidev@gmail.com>
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR
* ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
* (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
* LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND
* ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS
* SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package net.runelite.launcher;

import java.awt.MenuItem;
import java.awt.PopupMenu;
import java.awt.Taskbar;
import java.io.IOException;
import java.util.List;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;

@Slf4j
class DockMenu
{
private static final String NEW_INSTANCE = "New Instance";

// Adds a "New Instance" item to the dock icon's context menu.
//
// This has to be called from the process which owns the dock icon for the session. In FORK launch
// mode that is the forked child, not the parent launcher, which exits as soon as it has forked.
static void install(@Nullable List<String> newInstanceCommand)
{
if (OS.getOs() != OS.OSType.MacOS)
{
return;
}

if (newInstanceCommand == null || newInstanceCommand.isEmpty())
{
log.debug("Not installing dock menu, unable to determine how to launch a new instance");
return;
}

try
{
if (!Taskbar.isTaskbarSupported())
{
log.debug("Not installing dock menu, taskbar is unsupported");
return;
}

var taskbar = Taskbar.getTaskbar();
if (!taskbar.isSupported(Taskbar.Feature.MENU))
{
log.debug("Not installing dock menu, menu feature is unsupported");
return;
}

var item = new MenuItem(NEW_INSTANCE);
item.addActionListener(ev -> newInstance(newInstanceCommand));

var menu = new PopupMenu();
menu.add(item);
taskbar.setMenu(menu);

log.debug("Installed dock menu, new instance command: {}", newInstanceCommand);
}
catch (Exception ex)
{
// the dock menu is cosmetic, so it must never prevent the client from starting
log.warn("unable to install dock menu", ex);
}
}

/**
* {@link ProcessBuilder#start()} forks, so it is kept off the Event Dispatch Thread. Unlike
* {@link ForkLauncher#launch}, this process outlives the one it starts, so the child's output is
* discarded.
*/
private static void newInstance(List<String> command)
{
log.info("Launching new instance: {}", command);

var thread = new Thread(() ->
{
try
{
new ProcessBuilder(command)
.redirectOutput(ProcessBuilder.Redirect.DISCARD)
.redirectError(ProcessBuilder.Redirect.DISCARD)
.start();
}
catch (IOException ex)
{
log.error("unable to launch new instance", ex);
}
});
thread.setName(NEW_INSTANCE);
thread.setDaemon(true);
thread.start();
}
}
136 changes: 101 additions & 35 deletions src/main/java/net/runelite/launcher/ForkLauncher.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@
*/
package net.runelite.launcher;

import com.google.common.base.Strings;
import java.io.File;
import java.io.IOException;
import java.lang.management.ManagementFactory;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import java.util.Map;
import javax.annotation.Nullable;
import lombok.extern.slf4j.Slf4j;
import net.runelite.launcher.beans.Bootstrap;

Expand Down Expand Up @@ -76,37 +79,12 @@ static void launch(
Map<String, String> jvmProps,
List<String> jvmArgs) throws IOException
{
ProcessHandle current = ProcessHandle.current();
Path path;

switch (OS.getOs())
{
case Windows:
path = Paths.get(current.info().command().get());
break;
case MacOS:
path = Paths.get(current.info().command().get());
// on macOS packr changes the cwd to the resource directory prior to launching the JVM,
// causing current.info().command() to return /Applications/RuneLite.app/Contents/Resources/./RuneLite
// despite the executable really being at /Applications/RuneLite.app/Contents/MacOS/RuneLite
path = path.normalize()
.resolveSibling(Path.of("..", "MacOS", path.getFileName().toString()))
.normalize();
break;
case Linux:
// the executable is in the fuse-mounted filesystem of the appimage, which goes away after the launcher
// exits. So relaunch the appimage instead.
var appimage = System.getenv("APPIMAGE");
path = Path.of(appimage);
break;
default:
throw new IllegalStateException("invalid os");
}
Path path = launcherExecutable();

var commands = new ArrayList<>();
commands.add(path.toAbsolutePath().toString());
commands.add("-c");
// bootstrap vm args

var clientJvmArgs = JvmLauncher.getJvmArguments(bootstrap);
if (clientJvmArgs != null)
{
Expand All @@ -116,29 +94,70 @@ static void launch(
commands.add(arg);
}
}
// launcher vm props

for (var prop : jvmProps.entrySet())
{
commands.add("-J");
commands.add("-D" + prop.getKey() + "=" + prop.getValue());
}
// launcher vm args

for (var arg : jvmArgs)
{
commands.add("-J");
commands.add(arg);
}

// program arguments
commands.add("--");

if (classpath.isEmpty())
{
// avoid looping
throw new RuntimeException("cannot fork launch with an empty classpath");
}

commands.add("--classpath");
commands.add(classpathOption(classpath));

commands.addAll(clientArgs);

log.debug("Running process: {}", commands);

var builder = new ProcessBuilder(commands.toArray(new String[0]));
builder.start();
}
static Path launcherExecutable()
{
ProcessHandle current = ProcessHandle.current();
Path path;

switch (OS.getOs())
{
case Windows:
path = Paths.get(current.info().command().get());
break;
case MacOS:
path = Paths.get(current.info().command().get());
// on macOS packr changes the cwd to the resource directory prior to launching the JVM,
// causing current.info().command() to return /Applications/RuneLite.app/Contents/Resources/./RuneLite
// despite the executable really being at /Applications/RuneLite.app/Contents/MacOS/RuneLite
path = path.normalize()
.resolveSibling(Path.of("..", "MacOS", path.getFileName().toString()))
.normalize();
break;
case Linux:
// the executable is in the fuse-mounted filesystem of the appimage, which goes away after the launcher
// exits. So relaunch the appimage instead.
var appimage = System.getenv("APPIMAGE");
path = Path.of(appimage);
break;
default:
throw new IllegalStateException("invalid os");
}

return path;
}

static String classpathOption(List<File> classpath)
{
var sb = new StringBuilder();
for (var f : classpath)
{
Expand All @@ -149,13 +168,60 @@ static void launch(

sb.append(f.getName());
}
commands.add(sb.toString());

return sb.toString();
}

static List<String> relaunchCommand(Path executable, List<String> vmArgs, String classpathOpt,
Collection<String> clientArgs)
{
var commands = new ArrayList<String>();
commands.add(executable.toAbsolutePath().toString());
commands.add("-c");

// the -J args launch() passed us are this JVM's input args
for (var arg : vmArgs)
{
commands.add("-J");
commands.add(arg);
}

// program arguments
commands.add("--");
commands.add("--classpath");
commands.add(classpathOpt);
commands.addAll(clientArgs);

log.debug("Running process: {}", commands);
return commands;
}

var builder = new ProcessBuilder(commands.toArray(new String[0]));
builder.start();
// The command to launch an additional instance of the client, or null if one can't be determined.
@Nullable
static List<String> relaunchCommand(String classpathOpt, Collection<String> clientArgs)
{
if (canForkLaunch())
{
return relaunchCommand(launcherExecutable(),
ManagementFactory.getRuntimeMXBean().getInputArguments(), classpathOpt, clientArgs);
}

var command = ProcessHandle.current().info().command();
var classpath = System.getProperty("java.class.path");
if (command.isEmpty() || Strings.isNullOrEmpty(classpath))
{
log.debug("Unable to build a relaunch command, command={} classpath={}", command, classpath);
return null;
}

var commands = new ArrayList<String>();
commands.add(command.get());
commands.add("-cp");
commands.add(classpath);
commands.add(Launcher.class.getName());
commands.add("--launch-mode");
commands.add(LaunchMode.REFLECT.name());
commands.addAll(clientArgs);

return commands;
}
}
6 changes: 5 additions & 1 deletion src/main/java/net/runelite/launcher/Launcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -215,9 +215,12 @@ public static void main(String[] args)
.split(classpathOpt))
.map(name -> new File(REPO_DIR, name))
.collect(Collectors.toList());
var clientArgs = getClientArgs(settings);
DockMenu.install(ForkLauncher.relaunchCommand(classpathOpt, clientArgs));

try
{
ReflectionLauncher.launch(classpath, getClientArgs(settings));
ReflectionLauncher.launch(classpath, clientArgs);
}
catch (Exception e)
{
Expand Down Expand Up @@ -434,6 +437,7 @@ public static void main(String[] args)
if (settings.launchMode == LaunchMode.REFLECT)
{
log.debug("Using launch mode: REFLECT");
DockMenu.install(ForkLauncher.relaunchCommand(ForkLauncher.classpathOption(classpath), clientArgs));
ReflectionLauncher.launch(classpath, clientArgs);
}
else if (settings.launchMode == LaunchMode.FORK || (settings.launchMode == LaunchMode.AUTO && ForkLauncher.canForkLaunch()))
Expand Down
Loading