SquirrelJME/SquirrelJME

View on GitHub
emulators/emulator-base/src/main/java/cc/squirreljme/emulator/EmulatedTaskShelf.java

Summary

Maintainability
A
1 hr
Test Coverage
// -*- Mode: Java; indent-tabs-mode: t; tab-width: 4 -*-
// ---------------------------------------------------------------------------
// SquirrelJME
//     Copyright (C) Stephanie Gawroriski <xer@multiphasicapps.net>
// ---------------------------------------------------------------------------
// SquirrelJME is under the Mozilla Public License Version 2.0.
// See license.mkd for licensing and copyright information.
// ---------------------------------------------------------------------------

package cc.squirreljme.emulator;

import cc.squirreljme.jvm.mle.RuntimeShelf;
import cc.squirreljme.jvm.mle.TaskShelf;
import cc.squirreljme.jvm.mle.brackets.JarPackageBracket;
import cc.squirreljme.jvm.mle.brackets.TaskBracket;
import cc.squirreljme.jvm.mle.constants.TaskPipeRedirectType;
import cc.squirreljme.jvm.mle.constants.TaskStatusType;
import cc.squirreljme.jvm.mle.constants.VMDescriptionType;
import cc.squirreljme.jvm.mle.exceptions.MLECallError;
import cc.squirreljme.runtime.cldc.debug.Debugging;
import java.io.File;
import java.io.IOException;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Arrays;
import java.util.Collection;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import java.util.regex.Pattern;

/**
 * Emulation for {@link TaskShelf}.
 *
 * @since 2020/12/31
 */
@SuppressWarnings("unused")
public final class EmulatedTaskShelf
{
    /** Property for the libraries that are available. */
    public static final String AVAILABLE_LIBRARIES =
        "squirreljme.hosted.libraries";
    
    /** Property for the classpath of the currently running task. */
    public static final String RUN_CLASSPATH =
        "squirreljme.hosted.classpath";
    
    /** Property for the classpath of the currently running VM. */
    public static final String HOSTED_VM_CLASSPATH =
        "squirreljme.hosted.vm.classpath";
    
    /** The virtual machine support path, the minimal needed to run a VM. */
    public static final String HOSTED_VM_SUPPORTPATH =
        "squirreljme.hosted.vm.supportpath";
    
    /** Original property prefix. */
    public static final String ORIGINAL_PROP_PREFIX =
        "squirreljme.orig.";
    
    /** Are we on Windows? */
    private static final boolean _ON_WINDOWS;
    
    static
    {
        String osName = System.getProperty("os.name");
        _ON_WINDOWS = osName.toLowerCase().contains("windows");
    }
    
    /**
     * Returns the class path as a string.
     *
     * @param __paths Class paths.
     * @return The class path as a string.
     * @throws NullPointerException On null arguments.
     * @since 2020/08/21
     */
    public static String classpathAsString(Path... __paths)
        throws NullPointerException
    {
        if (__paths == null)
            throw new NullPointerException("NARG");
        
        return EmulatedTaskShelf.classpathAsString(Arrays.asList(__paths));
    }
    
    /**
     * Returns the class path as a string.
     *
     * @param __paths Class paths.
     * @return The class path as a string.
     * @throws NullPointerException On null arguments.
     * @since 2020/02/29
     */
    public static String classpathAsString(Iterable<Path> __paths)
        throws NullPointerException
    {
        if (__paths == null)
            throw new NullPointerException("NARG");
        
        StringBuilder sb = new StringBuilder();
        
        for (Path path : __paths)
        {
            if (sb.length() > 0)
                sb.append(File.pathSeparatorChar);
            sb.append(path);
        }
        
        return sb.toString();
    }
    
    /**
     * As {@link TaskShelf#start(JarPackageBracket[], String, String[],
     * String[], int, int)}. 
     * 
     * @param __classPath The class path that the launched application should
     * use.
     * @param __mainClass The main class to execute, this must be a class which
     * contains {@code static void main(String[] __args)}.
     * @param __args The arguments to the main class.
     * @param __sysPropPairs System property pairs, even values are keys and
     * odd values are values. This array must always be a multiple of two.
     * @param __stdOut The {@link TaskPipeRedirectType} for standard output.
     * @param __stdErr The {@link TaskPipeRedirectType} for standard error.
     * @return The bracket that represents the task.
     * @throws MLECallError If any argument is {@code null}, or an array
     * contains a {@code null} value, the {@code __sysPropPairs} is not a
     * multiple of two, {@code __classPath[0]} is not the same Jar package
     * as our own classpath's first JAR, if the task could not be created,
     * or if either the {@code __stdOut} or {@code __stdErr} redirect types
     * are not valid.
     * @since 2020/07/02
     */
    public static TaskBracket start(
        JarPackageBracket[] __classPath, String __mainClass, String[] __args,
        String[] __sysPropPairs, int __stdOut, int __stdErr)
        throws MLECallError
    {
        if (__classPath == null || __mainClass == null || __args == null ||
            __sysPropPairs == null)
            throw new MLECallError("Null start() arguments.");
        
        // Start building the process
        @SuppressWarnings("UseOfProcessBuilder")
        ProcessBuilder builder = new ProcessBuilder();
        List<String> args = new LinkedList<>();
        
        // We will be calling the Java executable, if we cannot find one then
        // assume it is just "java"
        args.add(Objects.toString(RuntimeShelf.vmDescription(
            VMDescriptionType.EXECUTABLE_PATH), "java"));
        
        // Determine which system properties we inherit from
        Map<String, String> sysProps = new LinkedHashMap<>();
        for (Map.Entry<Object, Object> e : System.getProperties().entrySet())
        {
            // System properties 
            String key = e.getKey().toString();
            if (key.startsWith(EmulatedTaskShelf.ORIGINAL_PROP_PREFIX))
            {
                // Extract the original property so we use it directly
                sysProps.put(key.substring(
                    EmulatedTaskShelf.ORIGINAL_PROP_PREFIX.length()),
                    Objects.toString(e.getValue(), ""));
                
                // And add the property we based this off, so that we if the
                // task we are starting wants to launch another task it gets
                // all of them as well
                sysProps.put(key,
                    Objects.toString(e.getValue(), ""));
            }
            
            // Otherwise only initially inherit these specific properties
            else
            {
                switch (key)
                {
                    case EmulatedTaskShelf.AVAILABLE_LIBRARIES:
                    case EmulatedTaskShelf.RUN_CLASSPATH:
                    case EmulatedTaskShelf.HOSTED_VM_CLASSPATH:
                    case EmulatedTaskShelf.HOSTED_VM_SUPPORTPATH:
                    case NativeBinding.LIB_PRELOAD:
                        sysProps.put(key,
                            Objects.toString(e.getValue(), ""));
                        break;
                }
            }
        }
        
        // Library preload, if applicable?
        String preloadLib = sysProps.get(NativeBinding.LIB_PRELOAD);
        if (preloadLib == null || preloadLib.isEmpty())
        {
            Path emulatorLib = NativeBinding.loadedLibraryPath();
            if (emulatorLib != null)
                sysProps.put(NativeBinding.LIB_PRELOAD,
                    emulatorLib.toAbsolutePath().toString());
        }
        
        // Load system property pairs
        for (int i = 0, n = __sysPropPairs.length & (~1); i < n; i += 2)
            sysProps.put(__sysPropPairs[i], __sysPropPairs[i + 1]);
        
        // We need the support path to determine how we are launching this
        Path[] vmSupportPath = EmulatedTaskShelf.__classpathDecode(
            System.getProperty(EmulatedTaskShelf.HOSTED_VM_SUPPORTPATH));
        
        // Determine the classpath as if one were just running the application
        // itself, this is the virtual classpath to run the process
        Path[] runClassPath = new Path[__classPath.length];
        for (int i = 0, n = __classPath.length; i < n; i++)
            runClassPath[i] = ((EmulatedJarPackageBracket)__classPath[i])
                .vmLib.path();
        
        // We need to tell it what our own classpath is, logically
        sysProps.put(EmulatedTaskShelf.RUN_CLASSPATH,
            EmulatedTaskShelf.classpathAsString(runClassPath));
        
        // Combine these two to make the running classpath, this includes the
        // emulator as well
        Set<Path> allLibs = new LinkedHashSet<>();
        allLibs.addAll(Arrays.<Path>asList(vmSupportPath));
        allLibs.addAll(Arrays.<Path>asList(runClassPath));
        
        // Tell the target what classpath we are running under
        String allLibsStr = EmulatedTaskShelf.classpathAsString(allLibs);
        sysProps.put(EmulatedTaskShelf.HOSTED_VM_CLASSPATH,
            allLibsStr);
        
        // Place in the actual target classpath which is the combination of
        // the base VM one and the target one
        args.add("-classpath");
        args.add(allLibsStr);
        
        // Use all declared system properties to ensure that they are properly
        // inherited from the host virtual machine
        for (Map.Entry<String, String> e : sysProps.entrySet())
            args.add(EmulatedTaskShelf.__escape(String.format("-D%s=%s",
                e.getKey(), e.getValue())));
        
        
        // Use special main handler which handles loading the required
        // methods for the hosted environment to work correctly with
        // SquirrelJME... our sub-tasks need to have this in order to properly
        // work
        args.add("cc.squirreljme.emulator.NativeBinding");
        
        // The main class is our direct main class, we do not need special
        // handling for it at all
        args.add(__mainClass);
        
        // Add all of the calling arguments we desire to be forwarded to the
        // sub-process
        args.addAll(Arrays.<String>asList(__args));
        
        // Use these arguments
        builder.command(args);
        
        // Debug
        Debugging.debugNote("Forked CmdLine: %s", args);
        
        // Alternative piping for standard output
        switch (__stdOut)
        {
            case TaskPipeRedirectType.TERMINAL:
                builder.redirectOutput(ProcessBuilder.Redirect.INHERIT);
                break;
            
            case TaskPipeRedirectType.BUFFER:
            case TaskPipeRedirectType.DISCARD:
                throw Debugging.todo();
            
            default:
                throw new MLECallError("Unknown stdOut: " + __stdOut);
        }
        
        // Alternative piping for standard output
        switch (__stdErr)
        {
            case TaskPipeRedirectType.TERMINAL:
                builder.redirectError(ProcessBuilder.Redirect.INHERIT);
                break;
            
            case TaskPipeRedirectType.BUFFER:
            case TaskPipeRedirectType.DISCARD:
                throw Debugging.todo();
            
            default:
                throw new MLECallError("Unknown stdOut: " + __stdOut);
        }
        
        // Start and track the process
        try
        {
            // Start the process and ensure that when this VM terminates that
            // the sub-process is terminated as well.
            Process process = builder.start();
            Runtime.getRuntime()
                .addShutdownHook(new Thread(new ProcessKiller(process),
                    String.format("processKiller@%08x",
                        System.identityHashCode(process))));
            
            return new EmulatedTaskBracket(process);
        }
        catch (IOException e)
        {
            throw new MLECallError("Could not spawn task.", e);
        }
    }
    
    /**
     * As {@link TaskShelf#status(TaskBracket)}. 
     * 
     * @param __task The task to request the status of.
     * @return One of {@link TaskStatusType}.
     * @throws MLECallError If the task is not valid.
     * @since 2020/12/31
     */
    public static int status(TaskBracket __task)
        throws MLECallError
    {
        if (__task == null)
            throw new MLECallError("Null task.");
        
        Process process = ((EmulatedTaskBracket)__task).process;
        if (process.isAlive())
            return TaskStatusType.ALIVE;
        return TaskStatusType.EXITED;
    }
    
    /**
     * Decodes the classpath string.
     * 
     * @param __string The string to decode.
     * @return The decoded path.
     * @throws NullPointerException On null arguments.
     * @since 2020/10/17
     */
    static Path[] __classpathDecode(String __string)
        throws NullPointerException
    {
        if (__string == null)
            throw new NullPointerException("NARG");
        
        Collection<Path> result = new LinkedList<>();
        for (String split : __string.split(Pattern.quote(File.pathSeparator)))
            result.add(Paths.get(split));
        
        return result.<Path>toArray(new Path[result.size()]);
    }
    
    /**
     * Escapes the given string.
     * 
     * @param __s The string to escape.
     * @return The escaped string.
     * @since 2023/04/13
     */
    static String __escape(String __s)
    {
        // Do not escape outside of Windows
        if (!EmulatedTaskShelf._ON_WINDOWS)
            return __s;
        
        // No quotes to escape?
        if (__s.indexOf('"') < 0)
            return __s;
        
        // Process each character and look for quotes
        StringBuilder result = new StringBuilder();
        for (int i = 0, n = __s.length(); i < n; i++)
        {
            char c = __s.charAt(i);
            
            // Escape quote if found
            if (c == '"')
                result.append('\\');
            result.append(c);
        }
        
        return result.toString();
    }
}