/* * Minecraft Forge * Copyright (c) 2018. * * This library is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation version 2.1 * of the License. * * This library is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public * License along with this library; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA */ package net.minecraftforge.fml; import java.util.Iterator; import java.util.concurrent.CountDownLatch; import java.util.concurrent.atomic.AtomicBoolean; import java.util.function.Consumer; import java.util.function.Supplier; import net.minecraft.client.Minecraft; import net.minecraft.server.MinecraftServer; import net.minecraft.server.dedicated.DedicatedServer; import net.minecraft.server.dedicated.PendingCommand; import net.minecraftforge.fml.client.gui.GuiConfirmation; import net.minecraftforge.fml.client.gui.GuiNotification; import net.minecraftforge.fml.common.thread.EffectiveSide; import net.minecraftforge.fml.loading.FMLEnvironment; import net.minecraftforge.fml.server.ServerLifecycleHooks; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import org.apache.logging.log4j.Marker; import org.apache.logging.log4j.MarkerManager; import javax.annotation.Nullable; public class StartupQuery { // internal class/functionality, do not use private static final Logger LOGGER = LogManager.getLogger(); private static final Marker SQ = MarkerManager.getMarker("STARTUPQUERY"); public static boolean confirm(String text) { StartupQuery query = new StartupQuery(text, new AtomicBoolean()); query.execute(); return query.getResult(); } private InterruptedException exception; public static void notify(String text) { StartupQuery query = new StartupQuery(text, null); query.execute(); } public static void abort() { MinecraftServer server = ServerLifecycleHooks.getCurrentServer(); if (server != null) server.initiateShutdown(); aborted = true; // to abort loading and go back to the main menu throw new AbortedException(); // to halt the server } public static void reset() { pending = null; aborted = false; } public static boolean check() { if (pending != null) { try { try { SidedProvider.STARTUPQUERY.>get().accept(pending); } catch (RuntimeException e) { } pending.throwException(); } catch (InterruptedException e) { LOGGER.warn(SQ, "query interrupted"); abort(); } pending = null; } return !aborted; } private void throwException() throws InterruptedException { if (exception != null) throw exception; } private static volatile StartupQuery pending; private static volatile boolean aborted = false; private StartupQuery(String text, @Nullable AtomicBoolean result) { this.text = text; this.result = result; } @Nullable public Boolean getResult() { return result == null ? null : result.get(); } public void setResult(boolean result) { this.result.set(result); } public String getText() { return text; } public boolean isSynchronous() { return synchronous; } public void finish() { signal.countDown(); } private void execute() { String prop = System.getProperty("fml.queryResult"); if (result != null && prop != null) { LOGGER.info(SQ, "Using fml.queryResult {} to answer the following query:\n{}", prop, text); if (prop.equalsIgnoreCase("confirm")) { setResult(true); return; } else if (prop.equalsIgnoreCase("cancel")) { setResult(false); return; } LOGGER.warn(SQ, "Invalid value for fml.queryResult: {}, expected confirm or cancel", prop); } synchronous = false; pending = this; // let the other thread start the query // from the integrated server thread: the client will eventually check pending and execute the query // from the client thread: synchronous execution // dedicated server: command handling in mc is synchronous, execute the server-side query directly if (FMLEnvironment.dist.isDedicatedServer() || EffectiveSide.get() == LogicalSide.CLIENT) { synchronous = true; check(); } try { signal.await(); reset(); } catch (InterruptedException e) { LOGGER.warn(SQ, "query interrupted"); abort(); } } private String text; @Nullable private AtomicBoolean result; private CountDownLatch signal = new CountDownLatch(1); private volatile boolean synchronous; /** * Exception not being caught by the crash report generation logic. */ public static class AbortedException extends RuntimeException { private static final long serialVersionUID = -5933665223696833921L; private AbortedException() { super(); } } public static class QueryWrapper { public static Consumer clientQuery(Supplier clientSupplier) { return (query) -> { Minecraft client = clientSupplier.get(); if (query.getResult() == null) { client.displayGuiScreen(new GuiNotification(query)); } else { client.displayGuiScreen(new GuiConfirmation(query)); } if (query.isSynchronous()) { while (client.currentScreen instanceof GuiNotification) { if (Thread.interrupted()) { query.exception = new InterruptedException(); throw new RuntimeException(); } try { Thread.sleep(50); } catch (InterruptedException ie) { query.exception = ie; } } } }; } public static Consumer dedicatedServerQuery(Supplier serverSupplier) { return (query) -> { DedicatedServer server = serverSupplier.get(); if (query.getResult() == null) { LOGGER.warn(SQ, query.getText()); query.finish(); } else { String text = query.getText() + "\n\nRun the command /fml confirm or or /fml cancel to proceed." + "\nAlternatively start the server with -Dfml.queryResult=confirm or -Dfml.queryResult=cancel to preselect the answer."; LOGGER.warn(SQ, text); if (!query.isSynchronous()) return; // no-op until mc does commands in another thread (if ever) boolean done = false; while (!done && server.isServerRunning()) { if (Thread.interrupted()) { query.exception = new InterruptedException(); throw new RuntimeException(); } DedicatedServer dedServer = (DedicatedServer)server; // rudimentary command processing, check for fml confirm/cancel and stop commands synchronized (dedServer.pendingCommandList) { for (Iterator it = dedServer.pendingCommandList.iterator(); it.hasNext(); ) { String cmd = it.next().command.trim().toLowerCase(); if (cmd.equals("/fml confirm")) { LOGGER.info(SQ, "confirmed"); query.setResult(true); done = true; it.remove(); } else if (cmd.equals("/fml cancel")) { LOGGER.info(SQ, "cancelled"); query.setResult(false); done = true; it.remove(); } else if (cmd.equals("/stop")) { StartupQuery.abort(); } } } try { Thread.sleep(10L); } catch (InterruptedException ie) { query.exception = ie; } } query.finish(); } }; } } }