Core features of FREEd PID1 working in a systemd container
[gpl/argeo-freed.git] / sjbin / src / freed-pid1.java
index 0239cb755f22690cf1271d5d5ca641e32f9dc863..31865bfa03596aac108781d94b378d86f6469001 100644 (file)
 //#! /usr/bin/java --source 17 @/usr/local/etc/freed/pid1/jvm.args
 
+import static java.lang.System.Logger.Level.DEBUG;
+import static java.lang.System.Logger.Level.ERROR;
+import static java.lang.System.Logger.Level.INFO;
+import static java.lang.System.Logger.Level.WARNING;
+
+import java.io.Console;
 import java.io.IOException;
+import java.lang.System.Logger;
 import java.lang.management.ManagementFactory;
+import java.net.InetAddress;
+import java.net.InterfaceAddress;
+import java.net.NetworkInterface;
+import java.net.SocketException;
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.nio.file.Paths;
+import java.util.Enumeration;
 import java.util.TreeMap;
+import java.util.concurrent.atomic.AtomicInteger;
 
 import org.argeo.init.Service;
 
-import jdk.jshell.tool.JavaShellToolBuilder;
 import sun.misc.Signal;
 
 class FreedPid1 {
+       final static AtomicInteger runLevel = new AtomicInteger(-1);
+
+       private final static Logger logger = System.getLogger(FreedPid1.class.getName());
+
        public static void main(String... args) {
-               final long pid = ProcessHandle.current().pid();
-               System.out.println("FREEd Init daemon starting with pid " + pid + "...");
-//             System.out.println(System.getProperty("user.dir"));
-//             System.out.println(System.getProperty("user.name"));
-//             System.out.println(System.getProperty("user.home"));
-
-               // System.setProperty("user.dir", "/tmp");
-               for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
-                       System.out.println(key + "=" + System.getProperty(key.toString()));
-               }
+               try {
+                       final long pid = ProcessHandle.current().pid();
+                       Signal.handle(new Signal("TERM"), (signal) -> {
+                               System.out.println("SIGTERM caught");
+                               System.exit(0);
+                       });
+                       Signal.handle(new Signal("INT"), (signal) -> {
+                               System.out.println("SIGINT caught");
+                               System.exit(0);
+                       });
+                       Signal.handle(new Signal("HUP"), (signal) -> {
+                               System.out.println("SIGHUP caught");
+                               System.exit(0);
+                       });
 
-               System.out.flush();
-
-               Signal.handle(new Signal("TERM"), (signal) -> {
-                       System.out.println("SIGTERM caught");
-                       System.exit(0);
-               });
-               Signal.handle(new Signal("INT"), (signal) -> {
-                       System.out.println("SIGINT caught");
-                       System.exit(0);
-               });
-               Signal.handle(new Signal("HUP"), (signal) -> {
-                       System.out.println("SIGHUP caught");
-                       System.exit(0);
-               });
-
-               if (args.length > 0 && ("1".equals(args[0]) //
-                               || "single".equals(args[0]) //
-                               || "emergency".equals(args[0]))) {
-                       // TODO check if we can remove dependency to management
-                       String classpath = ManagementFactory.getRuntimeMXBean().getClassPath();
-                       String feedbackMode = "concise";
-                       // TODO --startup script
-                       JavaShellToolBuilder builder = JavaShellToolBuilder.builder();
-                       try {
-                               builder.start("--execution", "direct", "--class-path", classpath, "--feedback", feedbackMode);
-                       } catch (Exception e) {
-                               e.printStackTrace();
-                               System.err.flush();
-                               System.exit(1);
-                               return;
-                       }
+                       boolean isSystemInit = pid == 1 || pid == 2;
+
+                       if (isSystemInit && args.length > 0 && ("1".equals(args[0]) //
+                                       || "single".equals(args[0]) //
+                                       || "emergency".equals(args[0]))) {
+                               runLevel.set(1);
+                               for (Object key : new TreeMap<>(System.getProperties()).keySet()) {
+                                       System.out.println(key + "=" + System.getProperty(key.toString()));
+                               }
+                               System.out.println("Single user mode");
+                               System.out.flush();
+                               ProcessBuilder pb = new ProcessBuilder("/bin/bash");
+                               pb.redirectError(ProcessBuilder.Redirect.INHERIT);
+                               pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+                               pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
+                               Process singleUserShell = pb.start();
+                               singleUserShell.waitFor();
+                       } else {
+                               if (args.length == 0)
+                                       runLevel.set(5);
+                               else
+                                       runLevel.set(Integer.parseInt(args[0]));
 
-               } else {
-                       // init Linux services
-                       initSysctl();
-                       startInitDService("networking");
-                       startInitDService("nslcd");
+                               if (runLevel.get() == 0) {// shutting down the whole system
+                                       if (!isSystemInit) {
+                                               logger.log(INFO, "Shutting down system...");
+                                               new ProcessBuilder("/usr/bin/kill", "1").start();
+                                               System.exit(0);
+                                       } else {
+                                               logger.log(ERROR, "Cannot start at run level " + runLevel.get());
+                                               System.exit(1);
+                                       }
+                               } else if (runLevel.get() == 6) {// reboot the whole system
+                                       if (!isSystemInit) {
+                                               logger.log(INFO, "Rebooting down system...");
+                                               Path sysrqP = Paths.get("/proc/sys/kernel/sysrq");
+                                               Files.writeString(sysrqP, "1");
+                                               Path sysrqTriggerP = Paths.get("/proc/sysrq-trigger");
+                                               Files.writeString(sysrqTriggerP, "b");
+                                               System.exit(0);
+                                       } else {
+                                               logger.log(ERROR, "Cannot start at run level " + runLevel.get());
+                                               System.exit(1);
+                                       }
+                               }
+
+                               logger.log(INFO, "FREEd Init daemon starting with pid " + pid + " after "
+                                               + ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
+                               // networking
+                               initSysctl();
+                               startInitDService("networking", true);
+                               if (!waitForNetwork(30 * 1000))
+                                       logger.log(ERROR, "No network available");
+
+                               // NSS services
+                               startInitDService("nslcd", false);// Note: nslcd fails to stop
 
-                       waitForNetwork();
+                               // login prompt
+                               Service.addPostStart(() -> new LoginThread().start());
 
-                       // init Argeo CMS
-                       Service.main(args);
+                               // init Argeo CMS
+                               logger.log(INFO, "FREEd Init daemon starting Argeo Init after "
+                                               + ManagementFactory.getRuntimeMXBean().getUptime() + " ms");
+                               Service.main(args);
+                       }
+               } catch (Throwable e) {
+                       logger.log(ERROR, "Unexpected exception in free-pid1 init, shutting down... ", e);
+                       System.exit(1);
                }
        }
 
@@ -85,32 +131,121 @@ class FreedPid1 {
                }
        }
 
-       static void startInitDService(String serviceName) {
+       static void startInitDService(String serviceName, boolean stopOnShutdown) {
                Path serviceInit = Paths.get("/etc/init.d/", serviceName);
                if (Files.exists(serviceInit))
                        try {
                                new ProcessBuilder(serviceInit.toString(), "start").start().waitFor();
-                               System.out.println("Service " + serviceName + " started");
-                               Runtime.getRuntime().addShutdownHook(new Thread(() -> {
-                                       try {
-                                               new ProcessBuilder(serviceInit.toString(), "stop").start().waitFor();
-                                       } catch (IOException | InterruptedException e) {
-                                               e.printStackTrace();
-                                       }
-                               }, "Stop service " + serviceName));
+                               logger.log(DEBUG, "Service " + serviceName + " started");
+                               if (stopOnShutdown)
+                                       Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                                               try {
+                                                       new ProcessBuilder(serviceInit.toString(), "stop").start().waitFor();
+                                               } catch (IOException | InterruptedException e) {
+                                                       e.printStackTrace();
+                                               }
+                                       }, "FREEd stop service " + serviceName));
                        } catch (IOException | InterruptedException e) {
                                e.printStackTrace();
                        }
                else
-                       System.out.println("Service " + serviceName + " not found and therefore not started");
+                       logger.log(WARNING, "Service " + serviceName + " not found and therefore not started");
        }
 
-       static void waitForNetwork() {
-               // TODO Do it properly
+       static boolean waitForNetwork(long timeout) {
+               long begin = System.currentTimeMillis();
+               long duration = 0;
+               boolean networkAvailable = false;
                try {
-                       Thread.sleep(1000);
-               } catch (InterruptedException e) {
-                       e.printStackTrace();
+                       networkAvailable: while (!networkAvailable) {
+                               duration = System.currentTimeMillis() - begin;
+                               if (duration > timeout)
+                                       break networkAvailable;
+                               Enumeration<NetworkInterface> netInterfaces = null;
+                               try {
+                                       netInterfaces = NetworkInterface.getNetworkInterfaces();
+                               } catch (SocketException e) {
+                                       throw new IllegalStateException("Cannot list network interfaces", e);
+                               }
+                               if (netInterfaces != null) {
+                                       while (netInterfaces.hasMoreElements()) {
+                                               NetworkInterface netInterface = netInterfaces.nextElement();
+                                               for (InterfaceAddress addr : netInterface.getInterfaceAddresses()) {
+                                                       InetAddress inetAddr = addr.getAddress();
+                                                       if (!inetAddr.isLoopbackAddress() && !inetAddr.isLinkLocalAddress()) {
+                                                               try {
+                                                                       if (inetAddr.isReachable((int) timeout)) {
+                                                                               networkAvailable = true;
+                                                                               duration = System.currentTimeMillis() - begin;
+                                                                               logger.log(DEBUG,
+                                                                                               "Network available after " + duration + " ms. IP: " + inetAddr);
+                                                                               break networkAvailable;
+                                                                       }
+                                                               } catch (IOException e) {
+                                                                       logger.log(ERROR, "Cannot check whether " + inetAddr + " is reachable", e);
+                                                               }
+                                                       }
+                                               }
+                                       }
+                               } else {
+                                       throw new IllegalStateException("No network interface has been found");
+                               }
+                               try {
+                                       Thread.sleep(100);
+                               } catch (InterruptedException e) {
+                                       // silent
+                               }
+                       }
+               } catch (Exception e) {
+                       logger.log(ERROR, "Cannot check whether network is avialable", e);
+               }
+               return networkAvailable;
+       }
+
+       /** A thread watching the login prompt. */
+       static class LoginThread extends Thread {
+               private boolean systemShuttingDown = false;
+               private Process process = null;
+
+               public LoginThread() {
+                       super("FREEd login prompt");
+                       setDaemon(true);
+                       Runtime.getRuntime().addShutdownHook(new Thread(() -> {
+                               systemShuttingDown = true;
+                               if (process != null)
+                                       process.destroy();
+                       }));
                }
+
+               @Override
+               public void run() {
+                       Console console = System.console();
+                       console.readLine(); // type return once to activate login prompt
+                       prompt: while (!systemShuttingDown) {
+                               try {
+                                       console.printf("login: ");
+                                       String username = console.readLine();
+                                       username = username.trim();
+                                       if ("".equals(username))
+                                               continue prompt;
+                                       ProcessBuilder pb = new ProcessBuilder("su", "--login", username);
+                                       pb.redirectError(ProcessBuilder.Redirect.INHERIT);
+                                       pb.redirectOutput(ProcessBuilder.Redirect.INHERIT);
+                                       pb.redirectInput(ProcessBuilder.Redirect.INHERIT);
+                                       process = pb.start();
+                                       Runtime.getRuntime().addShutdownHook(new Thread(() -> process.destroy()));
+                                       try {
+                                               process.waitFor();
+                                       } catch (InterruptedException e) {
+                                               process.destroy();
+                                       }
+                               } catch (Exception e) {
+                                       e.printStackTrace();
+                               } finally {
+                                       process = null;
+                               }
+                       }
+               }
+
        }
 }