diff --git a/LaunchServer/runtime/plugin.js b/LaunchServer/runtime/plugin.js index 08036e9..4d18c7e 100644 --- a/LaunchServer/runtime/plugin.js +++ b/LaunchServer/runtime/plugin.js @@ -9,8 +9,7 @@ getUsageDescription: function() { return "plugin.js test command"; }, invoke: function(args) { - LogHelper.info("[plugin.js] Command invoked! Args: " + - java.util.Arrays.toString(args)); + LogHelper.info("[plugin.js] Command invoked! Args: " + java.util.Arrays.toString(args)); } }))(server)); diff --git a/Launcher/runtime/config.js b/Launcher/runtime/config.js old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/dialog.css b/Launcher/runtime/dialog/dialog.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/dialog.fxml b/Launcher/runtime/dialog/dialog.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/dialog.js b/Launcher/runtime/dialog/dialog.js old mode 100755 new mode 100644 index c41b662..40d31fc --- a/Launcher/runtime/dialog/dialog.js +++ b/Launcher/runtime/dialog/dialog.js @@ -4,7 +4,7 @@ var movePoint = null; // Point2D // State variables -var sign, profiles, pingers = {}; +var pingers = {}; function initDialog() { // Lookup news WebView @@ -64,6 +64,26 @@ pane.lookup("#goSettings").setOnAction(goSettings); } +function initOffline() { + // Update title + stage.setTitle(config.title + " [Offline]"); + + // Set login field as username field + loginField.setPromptText("Имя пользователя"); + if (!VerifyHelper.isValidUsername(settings.login)) { + loginField.setText(""); // Reset if not valid + } + + // Disable password field + passwordField.setDisable(true); + passwordField.setPromptText("Недоступно"); + passwordField.setText(""); + + // Switch news view to offline page + var offlineURL = Launcher.getResourceURL("dialog/offline/offline.html"); + news.getEngine().load(offlineURL.toString()); +} + /* ======== Handler functions ======== */ function goAuth(event) { // Verify there's no other overlays @@ -83,22 +103,24 @@ return; // Maybe throw exception?) } - // Get password - var rsaPassword; - var password = passwordField.getText(); - if (!password.isEmpty()) { - rsaPassword = settings.setPassword(password); - } else if (settings.rsaPassword !== null) { - rsaPassword = settings.rsaPassword; - } else { - return; // No password - no auth, sorry :C + // Get password if online-mode + var rsaPassword = null; + if (!passwordField.isDisable()) { + var password = passwordField.getText(); + if (!password.isEmpty()) { + rsaPassword = settings.setPassword(password); + } else if (settings.rsaPassword !== null) { + rsaPassword = settings.rsaPassword; + } else { + return; // No password - no auth, sorry :C + } + + // Remember or reset password + settings.rsaPassword = savePasswordBox.isSelected() ? rsaPassword : null; } - // Store login and password - settings.login = login; - settings.rsaPassword = savePasswordBox.isSelected() ? rsaPassword : null; - // Show auth overlay + settings.login = login; doAuth(profile, login, rsaPassword); } @@ -116,11 +138,16 @@ function verifyLauncher(e) { processing.resetOverlay(); overlay.show(processing.overlay, function(event) makeLauncherRequest(function(result) { - sign = result.sign; - profiles = result.profiles; - updateProfilesList(); + settings.lastSign = result.sign; + settings.lastProfiles = result.profiles; + + // Init offline if set + if (settings.offline) { + initOffline(); + } - // Hide overlay + // Update profiles list and hide overlay + updateProfilesList(result.profiles); overlay.hide(0, function() { if (cliParams.autoLogin) { goAuth(null); @@ -141,16 +168,25 @@ overlay.swap(0, update.overlay, function(event) { var jvmDir = settings.updatesDir.resolve(jvmDirName); makeUpdateRequest(jvmDirName, jvmDir, null, function(jvmHDir) { + settings.lastHDirs.put(jvmDirName, jvmHDir); + + // Update assets update.resetOverlay("Обновление файлов ресурсов"); var assetDirName = profile.object.block.getEntryValue("assetDir", StringConfigEntryClass); var assetDir = settings.updatesDir.resolve(assetDirName); makeUpdateRequest(assetDirName, assetDir, null, function(assetHDir) { + settings.lastHDirs.put(assetDirName, assetHDir); + + // Update clients update.resetOverlay("Обновление файлов клиента"); var clientDirName = profile.object.block.getEntryValue("dir", StringConfigEntryClass); var clientDir = settings.updatesDir.resolve(clientDirName); - makeUpdateRequest(clientDirName, clientDir, profile.object.getUpdateMatcher(), function(clientHDir) - doLaunchClient(jvmDir, jvmHDir, clientHDir, assetDir, clientDir, profile, pp, accessToken) - ); + makeUpdateRequest(clientDirName, clientDir, profile.object.getUpdateMatcher(), function(clientHDir) { + settings.lastHDirs.put(clientDirName, clientHDir); + + // Launch client + doLaunchClient(jvmDir, jvmHDir, clientHDir, assetDir, clientDir, profile, pp, accessToken); + }); }); }); }); @@ -159,7 +195,7 @@ function doLaunchClient(jvmDir, jvmHDir, clientHDir, assetDir, clientDir, profile, pp, accessToken) { processing.resetOverlay(); overlay.swap(0, processing.overlay, function(event) - launchClient(jvmDir, jvmHDir, clientHDir, profile, new ClientLauncherParams(sign, assetDir, clientDir, // Dirs + launchClient(jvmDir, jvmHDir, clientHDir, profile, new ClientLauncherParams(settings.lastSign, assetDir, clientDir, pp, accessToken, settings.autoEnter, settings.fullScreen, settings.ram, 0, 0), doDebugClient) ); } @@ -176,7 +212,7 @@ } /* ======== Server handler functions ======== */ -function updateProfilesList() { +function updateProfilesList(profiles) { // Set profiles items profilesBox.setItems(javafx.collections.FXCollections.observableList(profiles)); for each (var profile in profiles) { @@ -303,8 +339,10 @@ dimPane.requestFocus(); // Hide old overlay - var child = dimPane.getChildren(); - child.set(child.indexOf(overlay.current), newOverlay); + if (overlay.current !== newOverlay) { + var child = dimPane.getChildren(); + child.set(child.indexOf(overlay.current), newOverlay); + } // Fix overlay position newOverlay.setLayoutX((dimPane.getPrefWidth() - newOverlay.getPrefWidth()) / 2.0); diff --git a/Launcher/runtime/dialog/offline/offline.html b/Launcher/runtime/dialog/offline/offline.html new file mode 100644 index 0000000..c9a9b70 --- /dev/null +++ b/Launcher/runtime/dialog/offline/offline.html @@ -0,0 +1,15 @@ + + + + + Offline-режим + + + +

Offline-режим

+ Лаунчер запущен в Offline-режиме. В этом режиме Вы можете запустить любой ранее загруженный клиент + с любым именем пользователя, при этом вход на серверы с авторизацией, а так же система скинов и плащей может не работать. + Скорее всего, проблема вызвана сбоем на сервере или неполадками в интернет-подключении. + Проверьте состояние интернет-подключения или обратитесь к администратору сервера. + + \ No newline at end of file diff --git a/Launcher/runtime/dialog/overlay/debug/debug.css b/Launcher/runtime/dialog/overlay/debug/debug.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/debug/debug.fxml b/Launcher/runtime/dialog/overlay/debug/debug.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/debug/debug.js b/Launcher/runtime/dialog/overlay/debug/debug.js old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/processing/error.png b/Launcher/runtime/dialog/overlay/processing/error.png old mode 100755 new mode 100644 Binary files differ diff --git a/Launcher/runtime/dialog/overlay/processing/processing.css b/Launcher/runtime/dialog/overlay/processing/processing.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/processing/processing.fxml b/Launcher/runtime/dialog/overlay/processing/processing.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/processing/processing.js b/Launcher/runtime/dialog/overlay/processing/processing.js old mode 100755 new mode 100644 index 48bdd1b..ba8eb77 --- a/Launcher/runtime/dialog/overlay/processing/processing.js +++ b/Launcher/runtime/dialog/overlay/processing/processing.js @@ -30,13 +30,15 @@ processing.description.setText(e.toString()); }, - setTaskProperties: function(task, callback, hide) { + setTaskProperties: function(task, callback, errorCallback, hide) { processing.description.textProperty().bind(task.messageProperty()); task.setOnFailed(function(event) { processing.description.textProperty().unbind(); processing.setError(task.getException()); if (hide) { - overlay.hide(2500, null); + overlay.hide(2500, errorCallback); + } else if (errorCallback !== null) { + errorCallback(); } }); task.setOnSucceeded(function(event) { @@ -48,17 +50,61 @@ } }; +function offlineLauncherRequest() { + if (settings.lastSign === null || settings.lastProfiles.isEmpty()) { + Request.requestError("Запуск в оффлайн-режиме невозможен"); + return; + } + + // Verify launcher signature + SecurityHelper.verifySign(LauncherRequest.BINARY_PATH, + settings.lastSign, Launcher.getConfig().publicKey); + + // Return last sign and profiles + return { + sign: settings.lastSign, + profiles: settings.lastProfiles + }; +} + +function offlineAuthRequest(login) { + return function() { + if (!VerifyHelper.isValidUsername(login)) { + Request.requestError("Имя пользователя некорректно"); + return; + } + + // Return offline profile and random access token + return { + pp: PlayerProfile.newOfflineProfile(login), + accessToken: SecurityHelper.randomStringToken() + } + }; +} + /* Export functions */ function makeLauncherRequest(callback) { - var task = newRequestTask(new LauncherRequest()); - processing.setTaskProperties(task, callback, false); + var task = settings.offline ? newTask(offlineLauncherRequest) : + newRequestTask(new LauncherRequest()); + + // Set task properties and start + processing.setTaskProperties(task, callback, function() { + if (settings.offline) { + return; + } + + // Repeat request, but in offline mode + settings.offline = true; + overlay.swap(2500, processing.overlay, function() makeLauncherRequest(callback)); + }, false); task.updateMessage("Обновление списка серверов"); startTask(task); } -function makeAuthRequest(username, rsaPassword, callback) { - var task = newRequestTask(new AuthRequest(username, rsaPassword)); - processing.setTaskProperties(task, callback, true); +function makeAuthRequest(login, rsaPassword, callback) { + var task = rsaPassword === null ? newTask(offlineAuthRequest(login)) : + newRequestTask(new AuthRequest(login, rsaPassword)); + processing.setTaskProperties(task, callback, null, true); task.updateMessage("Авторизация на сервере"); startTask(task); } @@ -66,7 +112,7 @@ function launchClient(jvmDir, jvmHDir, clientHDir, profile, params, callback) { var task = newTask(function() ClientLauncher.launch(jvmDir, jvmHDir, clientHDir, profile, params, LogHelper.isDebugEnabled())); - processing.setTaskProperties(task, callback, true); + processing.setTaskProperties(task, callback, null, true); task.updateMessage("Запуск выбранного клиента"); startTask(task); } diff --git a/Launcher/runtime/dialog/overlay/processing/spinner.gif b/Launcher/runtime/dialog/overlay/processing/spinner.gif old mode 100755 new mode 100644 Binary files differ diff --git a/Launcher/runtime/dialog/overlay/settings/settings.css b/Launcher/runtime/dialog/overlay/settings/settings.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/settings/settings.fxml b/Launcher/runtime/dialog/overlay/settings/settings.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/settings/settings.js b/Launcher/runtime/dialog/overlay/settings/settings.js old mode 100755 new mode 100644 index 11847a5..13e5ed6 --- a/Launcher/runtime/dialog/overlay/settings/settings.js +++ b/Launcher/runtime/dialog/overlay/settings/settings.js @@ -1,8 +1,13 @@ var settings = { file: dir.resolve("settings.bin"), // Settings file login: null, rsaPassword: null, profile: 0, // Auth - updatesDir: null, // Client download - autoEnter: false, fullScreen: false, ram: 0, // Client + updatesDir: null, autoEnter: false, fullScreen: false, ram: 0, // Client + + // Offline cache + offline: false, + lastSign: null, + lastProfiles: new java.util.LinkedList(), + lastHDirs: new java.util.HashMap(16), /* Settings and overlay functions */ load: function() { @@ -42,13 +47,27 @@ settings.rsaPassword = input.readBoolean() ? input.readByteArray(IOHelper.BUFFER_SIZE) : null; settings.profile = input.readLength(0); - // Client download settings - settings.updatesDir = IOHelper.toPath(input.readString(0)); - // Client settings + settings.updatesDir = IOHelper.toPath(input.readString(0)); settings.autoEnter = input.readBoolean(); settings.fullScreen = input.readBoolean(); settings.setRAM(input.readLength(JVMHelper.RAM)); + + // Offline cache + var publicKey = Launcher.getConfig().publicKey; + settings.lastSign = input.readBoolean() ? input.readByteArray(-SecurityHelper.RSA_KEY_LENGTH) : null; + settings.lastProfiles.clear(); + var lastProfilesCount = input.readLength(0); + for (var i = 0; i < lastProfilesCount; i++) { + settings.lastProfiles.add(new SignedObjectHolder(input, publicKey, ClientProfile.RO_ADAPTER)); + } + settings.lastHDirs.clear(); + var lastHDirsCount = input.readLength(0); + for (var i = 0; i < lastHDirsCount; i++) { + var name = IOHelper.verifyFileName(input.readString(255)); + VerifyHelper.putIfAbsent(settings.lastHDirs, name, new SignedObjectHolder(input, publicKey, function(i) new HashedDir(i)), + java.lang.String.format("Duplicate offline hashed dir: '%s'", name)); + } // Apply CLI params cliParams.applySettings(); @@ -71,13 +90,26 @@ } output.writeLength(settings.profile, 0); - // Client download settings - output.writeString(IOHelper.toString(settings.updatesDir), 0); - // Client settings + output.writeString(IOHelper.toString(settings.updatesDir), 0); output.writeBoolean(settings.autoEnter); output.writeBoolean(settings.fullScreen); output.writeLength(settings.ram, JVMHelper.RAM); + + // Offline cache + output.writeBoolean(settings.lastSign !== null); + if (settings.lastSign !== null) { + output.writeByteArray(settings.lastSign, -SecurityHelper.RSA_KEY_LENGTH); + } + output.writeLength(settings.lastProfiles.size(), 0); + for each (var profile in settings.lastProfiles) { + profile.write(output); + } + output.writeLength(settings.lastHDirs.size(), 0); + for each (var entry in settings.lastHDirs.entrySet()) { + output.writeString(entry.getKey(), 0); + entry.getValue().write(output); + } }, setDefault: function() { @@ -86,13 +118,16 @@ settings.rsaPassword = null; settings.profile = 0; - // Client download settings - settings.updatesDir = defaultUpdatesDir; - // Client settings + settings.updatesDir = defaultUpdatesDir; settings.autoEnter = config.autoEnterDefault; settings.fullScreen = config.fullScreenDefault; settings.setRAM(config.ramDefault); + + // Offline cache + settings.lastSign = null; + settings.lastProfiles.clear(); + settings.lastHDirs.clear(); // Apply CLI params cliParams.applySettings(); @@ -217,8 +252,8 @@ /* ====================== CLI PARAMS ===================== */ var cliParams = { login: null, password: null, profile: -1, autoLogin: false, // Auth - updatesDir: null, // Client download - autoEnter: null, fullScreen: null, ram: -1, // Client + updatesDir: null, autoEnter: null, fullScreen: null, ram: -1, // Client + offline: false, // Offline init: function(params) { var named = params.getNamed(); @@ -233,13 +268,11 @@ } cliParams.autoLogin = unnamed.contains("--autoLogin"); - // Read client download cli params + // Read client cli params var updatesDir = named.get("updatesDir"); if (updatesDir !== null) { cliParams.updatesDir = IOHelper.toPath(named.get("updatesDir")); } - - // Read client cli params var autoEnter = named.get("autoEnter"); if (autoEnter !== null) { cliParams.autoEnter = java.lang.Boolean.parseBoolean(autoEnter); @@ -252,10 +285,16 @@ if (ram !== null) { cliParams.ram = java.lang.Integer.parseInt(ram); } + + // Read offline cli param + var offline = named.get("offline"); + if (offline !== null) { + cliParams.offline = java.lang.Boolean.parseBoolean(offline); + } }, applySettings: function() { - // Apply auth settings + // Apply auth params if (cliParams.login !== null) { settings.login = cliParams.login; } @@ -266,12 +305,10 @@ settings.profile = cliParams.profile; } - // Apply client download settings + // Apply client params if (cliParams.updatesDir !== null) { settings.updatesDir = cliParams.updatesDir; } - - // Apply client settings if (cliParams.autoEnter !== null) { settings.autoLogin = cliParams.autoEnter; } @@ -281,5 +318,10 @@ if (cliParams.ram >= 0) { settings.setRAM(cliParams.ram); } + + // Apply offline param + if (cliParams.offline !== null) { + settings.offline = cliParams.offline; + } } }; diff --git a/Launcher/runtime/dialog/overlay/update/update.css b/Launcher/runtime/dialog/overlay/update/update.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/update/update.fxml b/Launcher/runtime/dialog/overlay/update/update.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/overlay/update/update.js b/Launcher/runtime/dialog/overlay/update/update.js old mode 100755 new mode 100644 index 15638ab..4a035ef --- a/Launcher/runtime/dialog/overlay/update/update.js +++ b/Launcher/runtime/dialog/overlay/update/update.js @@ -71,10 +71,35 @@ } }; +function offlineUpdateRequest(dirName, dir, matcher) { + return function() { + var hdir = settings.lastHDirs.get(dirName); + if (hdir === null) { + Request.requestError(String.format("Директории '%s' нет в кэше", dirName)); + return; + } + + // Verify dir with matcher + var verifyMatcher = matcher === null ? null : matcher.verifyOnly(); + var currentHDir = new HashedDir(dir, verifyMatcher); + if (!hdir.object.diff(currentHDir, verifyMatcher).isSame()) { + Request.requestError(String.format("Директория '%s' была изменена", dirName)); + return; + } + + // Return last hdir + return hdir; + }; +} + /* Export functions */ function makeUpdateRequest(dirName, dir, matcher, callback) { - var request = new UpdateRequest(dirName, dir, matcher); - var task = newRequestTask(request); + var request = settings.offline ? { setStateCallback: function(stateCallback) { /* Ignored */ } } : + new UpdateRequest(dirName, dir, matcher); + var task = settings.offline ? newTask(offlineUpdateRequest(dirName, dir, matcher)) : + newRequestTask(request); + + // Set task properties and start update.setTaskProperties(task, request, callback); task.updateMessage("Состояние: Хеширование"); task.updateProgress(-1, -1); diff --git a/Launcher/runtime/dialog/profileCell.css b/Launcher/runtime/dialog/profileCell.css old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/profileCell.fxml b/Launcher/runtime/dialog/profileCell.fxml old mode 100755 new mode 100644 diff --git a/Launcher/runtime/dialog/settings.png b/Launcher/runtime/dialog/settings.png old mode 100755 new mode 100644 Binary files differ diff --git a/Launcher/runtime/engine/api.js b/Launcher/runtime/engine/api.js old mode 100755 new mode 100644 diff --git a/Launcher/runtime/favicon.png b/Launcher/runtime/favicon.png old mode 100755 new mode 100644 Binary files differ diff --git a/Launcher/runtime/init.js b/Launcher/runtime/init.js old mode 100755 new mode 100644 diff --git a/Launcher/source/client/ClientLauncher.java b/Launcher/source/client/ClientLauncher.java index 8930790..2e34574 100644 --- a/Launcher/source/client/ClientLauncher.java +++ b/Launcher/source/client/ClientLauncher.java @@ -71,6 +71,10 @@ return LAUNCHED.get(); } + public static String jvmProperty(String name, String value) { + return String.format("-D%s=%s", name, value); + } + @LauncherAPI public static Process launch(Path jvmDir, SignedObjectHolder jvmHDir, SignedObjectHolder clientHDir, SignedObjectHolder profile, Params params, boolean pipeOutput) throws Throwable { // Write params file (instead of CLI; Mustdie32 API can't handle command line > 32767 chars) @@ -240,10 +244,6 @@ } } - private static String jvmProperty(String name, String value) { - return String.format("-D%s=%s", name, value); - } - private static void launch(ClientProfile profile, Params params) throws Throwable { // Add natives path JVMHelper.addNativePath(params.clientDir.resolve(NATIVES_DIR)); diff --git a/Launcher/source/hasher/FileNameMatcher.java b/Launcher/source/hasher/FileNameMatcher.java index 02b9a07..3b0bdb0 100644 --- a/Launcher/source/hasher/FileNameMatcher.java +++ b/Launcher/source/hasher/FileNameMatcher.java @@ -9,6 +9,9 @@ import launcher.helper.IOHelper; public final class FileNameMatcher { + private static final Entry[] NO_ENTRIES = new Entry[0]; + + // Instance private final Entry[] update; private final Entry[] verify; private final Entry[] exclusions; @@ -20,6 +23,12 @@ this.exclusions = toEntries(exclusions); } + private FileNameMatcher(Entry[] update, Entry[] verify, Entry[] exclusions) { + this.update = update; + this.verify = verify; + this.exclusions = exclusions; + } + @LauncherAPI public boolean shouldUpdate(Collection path) { return (anyMatch(update, path) || anyMatch(verify, path)) && !anyMatch(exclusions, path); @@ -30,6 +39,11 @@ return anyMatch(verify, path) && !anyMatch(exclusions, path); } + @LauncherAPI + public FileNameMatcher verifyOnly() { + return new FileNameMatcher(NO_ENTRIES, verify, exclusions); + } + private static boolean anyMatch(Entry[] entries, Collection path) { return Arrays.stream(entries).anyMatch(e -> e.matches(path)); } diff --git a/Launcher/source/request/Request.java b/Launcher/source/request/Request.java index b434071..0884f88 100644 --- a/Launcher/source/request/Request.java +++ b/Launcher/source/request/Request.java @@ -51,7 +51,7 @@ protected final void readError(HInput input) throws IOException { String error = input.readString(0); if (!error.isEmpty()) { - throw new RequestException(error); + requestError(error); } } @@ -67,11 +67,16 @@ // Verify is accepted if (!input.readBoolean()) { - throw new RequestException("Serverside not accepted this connection"); + requestError("Serverside not accepted this connection"); } } @LauncherAPI + public static void requestError(String message) throws RequestException { + throw new RequestException(message); + } + + @LauncherAPI public enum Type implements EnumSerializer.Itf { PING(0), // Ping request LAUNCHER(1), UPDATE(2), UPDATE_LIST(3), // Update requests diff --git a/Launcher/source/request/update/LauncherRequest.java b/Launcher/source/request/update/LauncherRequest.java index 8ce17b3..8cb8dc2 100644 --- a/Launcher/source/request/update/LauncherRequest.java +++ b/Launcher/source/request/update/LauncherRequest.java @@ -9,9 +9,11 @@ import launcher.Launcher; import launcher.LauncherAPI; +import launcher.client.ClientLauncher; import launcher.client.ClientProfile; import launcher.helper.IOHelper; import launcher.helper.JVMHelper; +import launcher.helper.LogHelper; import launcher.helper.SecurityHelper; import launcher.request.Request; import launcher.serialize.HInput; @@ -57,8 +59,9 @@ SecurityHelper.verifySign(binary, sign, publicKey); IOHelper.write(BINARY_PATH, binary); - // Start new launcher instance + // Start new launcher instance (java -jar works for Launch4J's EXE too) ProcessBuilder builder = new ProcessBuilder(IOHelper.resolveJavaBin(null).toString(), + ClientLauncher.jvmProperty(LogHelper.DEBUG_PROPERTY, Boolean.toString(LogHelper.isDebugEnabled())), "-jar", BINARY_PATH.toString()); builder.inheritIO(); builder.start();