<commit_before>/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.karaf.instance.core.internal;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;

import org.apache.felix.utils.properties.InterpolationHelper;
import org.apache.felix.utils.properties.Properties;
import org.apache.felix.utils.properties.TypedProperties;
import org.apache.karaf.instance.core.Instance;
import org.apache.karaf.instance.core.InstanceService;
import org.apache.karaf.instance.core.InstanceSettings;
import org.apache.karaf.instance.main.Execute;
import org.apache.karaf.jpm.Process;
import org.apache.karaf.jpm.impl.ProcessBuilderFactoryImpl;
import org.apache.karaf.jpm.impl.ScriptUtils;
import org.apache.karaf.profile.Profile;
import org.apache.karaf.profile.ProfileBuilder;
import org.apache.karaf.profile.ProfileService;
import org.apache.karaf.shell.support.ansi.SimpleAnsi;
import org.apache.karaf.util.StreamUtils;
import org.apache.karaf.util.config.PropertiesLoader;
import org.apache.karaf.util.locks.FileLockUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class InstanceServiceImpl implements InstanceService {

    public static final String STORAGE_FILE = "instance.properties";
    public static final String BACKUP_EXTENSION = ".bak";
    private static final String FEATURES_CFG = "etc/org.apache.karaf.features.cfg";
    private static final String RESOURCE_BASE = "org/apache/karaf/instance/resources/";

    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceServiceImpl.class);

    private static final String CONFIG_PROPERTIES_FILE_NAME = "config.properties";

    private static final String KARAF_SHUTDOWN_PORT = "karaf.shutdown.port";

    private static final String KARAF_SHUTDOWN_HOST = "karaf.shutdown.host";

    private static final String KARAF_SHUTDOWN_PORT_FILE = "karaf.shutdown.port.file";

    private static final String KARAF_SHUTDOWN_COMMAND = "karaf.shutdown.command";

    private static final String KARAF_SHUTDOWN_TIMEOUT = "karaf.shutdown.timeout";

    private static final String DEFAULT_SHUTDOWN_COMMAND = "SHUTDOWN";

    public static final String DEFAULT_JAVA_OPTS = "-server -Xmx512M -Dcom.sun.management.jmxremote -XX:+UnlockDiagnosticVMOptions";

    private LinkedHashMap<String, InstanceImpl> proxies = new LinkedHashMap<>();

    private File storageLocation;

    private long stopTimeout = 30000;

    static class InstanceState {
        String name;
        String loc;
        String opts;
        int pid;
        boolean root;
    }

    static class State {
        int defaultSshPortStart = 8101;
        int defaultRmiRegistryPortStart = 1099;
        int defaultRmiServerPortStart = 44444;
        Map<String, InstanceState> instances;
        public State() {
            //read port start value from the root instance configuration
            try {
                TypedProperties shellProperty = new TypedProperties();
                shellProperty.load(new File(System.getProperty("karaf.etc"), "org.apache.karaf.shell.cfg"));
                defaultSshPortStart = getInt(shellProperty,"sshPort", 8101);
                TypedProperties managementProperty = new TypedProperties();
                managementProperty.load(new File(System.getProperty("karaf.etc"), "org.apache.karaf.management.cfg"));
                defaultRmiRegistryPortStart = getInt(managementProperty, "rmiRegistryPort", 1099);
                defaultRmiServerPortStart = getInt(managementProperty, "rmiServerPort", 1099);
            } catch (Exception e) {
                LOGGER.debug("Could not read port start value from the root instance configuration.", e);
            }
        }
        
        
    }

    public InstanceServiceImpl() {
        String prop = System.getProperty("karaf.instances");
        if (prop != null) {
            storageLocation = new File(prop);
        }
    }

    public File getStorageLocation() {
        return storageLocation;
    }

    public void setStorageLocation(File storage) {
        this.storageLocation = storage;
    }

    public long getStopTimeout() {
        return stopTimeout;
    }

    public void setStopTimeout(long stopTimeout) {
        this.stopTimeout = stopTimeout;
    }

    private State loadData(TypedProperties storage) {
        State state = new State();
        int count = getInt(storage, "count", 0);
        state.defaultSshPortStart = getInt(storage, "ssh.port", state.defaultSshPortStart);
        state.defaultRmiRegistryPortStart = getInt(storage, "rmi.registry.port", state.defaultRmiRegistryPortStart);
        state.defaultRmiServerPortStart = getInt(storage, "rmi.server.port", state.defaultRmiServerPortStart);
        state.instances = new LinkedHashMap<>();

        for (int i = 0; i < count; i++) {
            InstanceState instance = new InstanceState();
            instance.name = getString(storage, "item." + i + ".name", null);
            instance.loc = getString(storage, "item." + i + ".loc", null);
            instance.opts = getString(storage, "item." + i + ".opts", null);
            instance.pid = getInt(storage, "item." + i + ".pid", 0);
            instance.root = getBool(storage, "item." + i + ".root", false);
            state.instances.put(instance.name, instance);
        }
        // Update proxies list
        for (InstanceState instance : state.instances.values()) {
            if (!this.proxies.containsKey(instance.name)) {
                proxies.put(instance.name, new InstanceImpl(this, instance.name));
            }
        }
        List<String> names = new ArrayList<>(this.proxies.keySet());
        for (String name : names) {
            if (!state.instances.containsKey(name)) {
                this.proxies.remove(name);
            }
        }
        return state;
    }

    private void saveData(State state, TypedProperties storage) {
        storage.put("ssh.port", Integer.toString(state.defaultSshPortStart));
        storage.put("rmi.registry.port", Integer.toString(state.defaultRmiRegistryPortStart));
        storage.put("rmi.server.port", Integer.toString(state.defaultRmiServerPortStart));
        storage.put("count", Integer.toString(state.instances.size()));
        int i = 0;
        for (InstanceState instance : state.instances.values()) {
            storage.put("item." + i + ".name", instance.name);
            storage.put("item." + i + ".root", Boolean.toString(instance.root));
            storage.put("item." + i + ".loc", instance.loc);
            storage.put("item." + i + ".pid", Integer.toString(instance.pid));
            storage.put("item." + i + ".opts", instance.opts != null ? instance.opts : "");
            i++;
        }
        while (storage.containsKey("item." + i + ".name")) {
            storage.remove("item." + i + ".name");
            storage.remove("item." + i + ".root");
            storage.remove("item." + i + ".loc");
            storage.remove("item." + i + ".pid");
            storage.remove("item." + i + ".opts");
            i++;
        }
    }

    private static boolean getBool(TypedProperties storage, String name, boolean def) {
        Object value = storage.get(name);
        if (value instanceof Boolean) {
            return (Boolean) value;
        } else if (value != null) {
            return Boolean.parseBoolean(value.toString());
        } else {
            return def;
        }
    }

    private static int getInt(TypedProperties storage, String name, int def) {
        Object value = storage.get(name);
        if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value != null) {
            return Integer.parseInt(value.toString());
        } else {
            return def;
        }
    }

    private static String getString(TypedProperties storage, String name, String def) {
        Object value = storage.get(name);
        return value != null ? value.toString() : def;
    }

    interface Task<U, T> {
        T call(U state) throws IOException;
    }

    synchronized <T> T execute(final Task<State, T> callback, final boolean writeToFile) {
        final File storageFile = new File(storageLocation, STORAGE_FILE);
        if (!storageFile.exists()) {
            storageFile.getParentFile().mkdirs();
            try {
                storageFile.createNewFile();
            } catch (IOException e) {
                // Ignore
            }
        }
        if (storageFile.exists()) {
            if (!storageFile.isFile()) {
                throw new IllegalStateException("Instance storage location should be a file: " + storageFile);
            }
            try {
                return FileLockUtils.execute(storageFile, properties -> {
                    State state = loadData(properties);
                    T t = callback.call(state);
                    if (writeToFile) {
                        saveData(state, properties);
                    }
                    return t;
                }, writeToFile);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new IllegalStateException("Instance storage location does not exist: " + storageFile);
        }
    }

    private static void logInfo(String message, boolean printOutput, Object... args) {
        if (LOGGER.isInfoEnabled() || printOutput) {
            String formatted = String.format(message, args);
            LOGGER.info(formatted);
            if (printOutput) {
                println(formatted);
            }
        }
    }
    
    private static void logDebug(String message, boolean printOutput, Object... args) {
        if (LOGGER.isDebugEnabled() || printOutput) {
            String formatted = String.format(message, args);
            LOGGER.debug(formatted);
            if (printOutput) {
                println(formatted);
            }
        }
    }

    public synchronized Instance createInstance(final String name, final InstanceSettings settings, final boolean printOutput) throws Exception {
        return execute(state -> {
            if (state.instances.get(name) != null) {
                throw new IllegalArgumentException("Instance '" + name + "' already exists");
            }
            if (!settings.getProfiles().isEmpty()) {
                try {
                    ProfileApplier.verify();
                } catch (NoClassDefFoundError error) {
                    throw new IllegalArgumentException("Profile service package is not available");
                }
            }

            String loc = settings.getLocation() != null ? settings.getLocation() : name;
            File karafBase = new File(loc);
            if (!karafBase.isAbsolute()) {
                karafBase = new File(storageLocation, loc);
            }
            int sshPort = settings.getSshPort();
            if (sshPort <= 0) {
                sshPort = ++state.defaultSshPortStart;
            }
            String sshHost = settings.getAddress();
            int rmiRegistryPort = settings.getRmiRegistryPort();
            if (rmiRegistryPort <= 0) {
                rmiRegistryPort = ++state.defaultRmiRegistryPortStart;
            }
            int rmiServerPort = settings.getRmiServerPort();
            if (rmiServerPort <= 0) {
                rmiServerPort = ++state.defaultRmiServerPortStart;
            }
            logInfo("Creating new instance on SSH port %d and registry port %d / RMI server port %d at: %s",
                    printOutput, sshPort, rmiRegistryPort, rmiServerPort, karafBase);

            mkdir(karafBase, "bin", printOutput);
            mkdir(karafBase, "etc", printOutput);
            mkdir(karafBase, "etc/scripts", printOutput);
            mkdir(karafBase, "system", printOutput);
            mkdir(karafBase, "deploy", printOutput);
            mkdir(karafBase, "data", printOutput);

            Map<String, URL> textResources = new HashMap<>(settings.getTextResources());
            Map<String, URL> binaryResources = new HashMap<>(settings.getBinaryResources());

            String[] resources =
            {
                    "etc/all.policy",
                    "etc/config.properties",
                    "etc/custom.properties",
                    "etc/distribution.info",
                    "etc/equinox-debug.properties",
                    "etc/java.util.logging.properties",
                    "etc/jmx.acl.cfg",
                    "etc/jmx.acl.java.lang.Memory.cfg",
                    "etc/jmx.acl.org.apache.karaf.bundle.cfg",
                    "etc/jmx.acl.org.apache.karaf.config.cfg",
                    "etc/jmx.acl.org.apache.karaf.security.jmx.cfg",
                    "etc/jmx.acl.osgi.compendium.cm.cfg",
                    "etc/jre.properties",
                    "etc/keys.properties",
                    "etc/org.apache.felix.eventadmin.impl.EventAdmin.cfg",
                    "etc/org.apache.felix.fileinstall-deploy.cfg",
                    "etc/org.apache.karaf.command.acl.bundle.cfg",
                    "etc/org.apache.karaf.command.acl.config.cfg",
                    "etc/org.apache.karaf.command.acl.feature.cfg",
                    "etc/org.apache.karaf.command.acl.jaas.cfg",
                    "etc/org.apache.karaf.command.acl.kar.cfg",
                    "etc/org.apache.karaf.command.acl.scope_bundle.cfg",
                    "etc/org.apache.karaf.command.acl.shell.cfg",
                    "etc/org.apache.karaf.command.acl.system.cfg",
                    "etc/org.apache.karaf.features.repos.cfg",
                    "etc/org.apache.karaf.jaas.cfg",
                    "etc/org.apache.karaf.kar.cfg",
                    "etc/org.apache.karaf.log.cfg",
                    "etc/org.ops4j.pax.logging.cfg",
                    "etc/org.ops4j.pax.url.mvn.cfg",
                    "etc/shell.init.script",
                    "etc/users.properties",
                    "etc/scripts/shell.completion.script",
                    FEATURES_CFG
            };
            copyResourcesToDir(resources, karafBase, textResources, printOutput);
            addFeaturesFromSettings(new File(karafBase, FEATURES_CFG), settings);

            // The startup.properties is now generated by the karaf maven plugin, so
            // we use the one from the root instance instead of embedding it
            File rootEtc = new File(System.getProperty("karaf.etc"));
            copy(new File(rootEtc, "startup.properties"), new File(karafBase, "etc/startup.properties"));

            // align child with any bundles we have overriden in the root instance
            File rootOverrides = new File(rootEtc, "overrides.properties");
            if (rootOverrides.exists()) {
                copy(rootOverrides, new File(karafBase, "etc/overrides.properties"));
            }

            HashMap<String, String> props = new HashMap<>();
            props.put("${SUBST-KARAF-NAME}", name);
            props.put("${SUBST-KARAF-HOME}", System.getProperty("karaf.home"));
            props.put("${SUBST-KARAF-BASE}", karafBase.getPath());
            props.put("${SUBST-SSH-PORT}", Integer.toString(sshPort));
            props.put("${SUBST-SSH-HOST}", sshHost);
            props.put("${SUBST-RMI-REGISTRY-PORT}", Integer.toString(rmiRegistryPort));
            props.put("${SUBST-RMI-SERVER-PORT}", Integer.toString(rmiServerPort));

            String[] filteredResources =
            {
                 "etc/system.properties",
                 "etc/org.apache.karaf.shell.cfg",
                 "etc/org.apache.karaf.management.cfg",
                 "bin/karaf",
                 "bin/start",
                 "bin/stop",
                 "bin/karaf.bat",
                 "bin/start.bat",
                 "bin/stop.bat"
            };
            copyFilteredResourcesToDir(filteredResources, karafBase, textResources, props, printOutput);

            try {
                makeFileExecutable(new File(karafBase, "bin/karaf"));
                makeFileExecutable(new File(karafBase, "bin/start"));
                makeFileExecutable(new File(karafBase, "bin/stop"));
            } catch (IOException e) {
                LOGGER.debug("Could not set file mode on scripts.", e);
            }

            for (String resource : textResources.keySet()) {
                copyFilteredResourceToDir(resource, karafBase, textResources, props, printOutput);
            }

            for (String resource : binaryResources.keySet()) {
                copyBinaryResourceToDir(resource, karafBase, binaryResources, printOutput);
            }

            if (!settings.getProfiles().isEmpty()) {
                ProfileApplier.applyProfiles(karafBase, settings.getProfiles(), printOutput);
            }

            String javaOpts = settings.getJavaOpts();
            if (javaOpts == null || javaOpts.length() == 0) {
                javaOpts = DEFAULT_JAVA_OPTS;
            }
            InstanceState is = new InstanceState();
            is.name = name;
            is.loc = karafBase.toString();
            is.opts = javaOpts;
            state.instances.put(name, is);
            InstanceImpl instance = new InstanceImpl(InstanceServiceImpl.this, name);
            InstanceServiceImpl.this.proxies.put(name, instance);
            return instance;
        }, true);
    }

    void addFeaturesFromSettings(File featuresCfg, final InstanceSettings settings) throws IOException {
        FileLockUtils.execute(featuresCfg, properties -> {
            appendToPropList(properties, "featuresBoot", settings.getFeatures());
            appendToPropList(properties, "featuresRepositories", settings.getFeatureURLs());
        }, true);
    }

    private static void appendToPropList(TypedProperties p, String key, List<String> elements) {
        if (elements == null) {
            return;
        }
        StringBuilder sb = new StringBuilder(p.get(key).toString().trim());
        for (String f : elements) {
            if (sb.length() > 0) {
                sb.append(',');
            }
            sb.append(f);
        }
        p.put(key, sb.toString());
    }

    public Instance[] getInstances() {
        return execute(state -> proxies.values().toArray(new Instance[proxies.size()]), false);
    }

    public Instance getInstance(final String name) {
        return execute(state -> proxies.get(name), false);
    }

    public void startInstance(final String name, final String javaOpts) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance already started");
            }
            doStart(instance, name, javaOpts);
            return null;
        }, true);
    }

    private void doStart(InstanceState instance, String name, String javaOpts) throws IOException {
        String opts = javaOpts;
        if (opts == null || opts.length() == 0) {
            opts = instance.opts;
        }
        if (opts == null || opts.length() == 0) {
            opts = DEFAULT_JAVA_OPTS;
        }

        // fallback and read karafOpts from KARAF_OPTS environment if no System property present
        String karafOptsEnv = System.getenv("KARAF_OPTS");
        String karafOpts = System.getProperty("karaf.opts", karafOptsEnv != null ? karafOptsEnv : "");

        String location = instance.loc;

        File libDir = new File(System.getProperty("karaf.home"), "lib");
        File bootLibDir = new File(libDir, "boot");
        File childLibDir = new File(location, "lib");

        StringBuilder classpath = classpathFromLibDir(bootLibDir);
        StringBuilder childClasspath = classpathFromLibDir(childLibDir);
        if (childClasspath.length() > 0 && !bootLibDir.equals(childLibDir)) {
            classpath.append(System.getProperty("path.separator"));
            classpath.append(childClasspath);
        }

        String jdkOpts;
        if (!System.getProperty("java.version").startsWith("1.")) {
            jdkOpts = " --add-opens java.base/java.security=ALL-UNNAMED" +
                      " --add-opens java.base/java.net=ALL-UNNAMED" +
                      " --add-opens java.base/java.lang=ALL-UNNAMED" +
                      " --add-opens java.base/java.util=ALL-UNNAMED" +
                      " --add-opens java.naming/javax.naming.spi=ALL-UNNAMED" +
                      " --add-opens java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.http=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.https=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.jar=ALL-UNNAMED" +
                      " --add-exports=java.xml.bind/com.sun.xml.internal.bind.v2.runtime=ALL-UNNAMED" +
                      " --add-exports=jdk.xml.dom/org.w3c.dom.html=ALL-UNNAMED" +
                      " --add-exports=jdk.naming.rmi/com.sun.jndi.url.rmi=ALL-UNNAMED" +
                      " --add-modules java.xml.ws.annotation,java.corba,java.transaction,java.xml.bind,java.xml.ws";
        } else {
            jdkOpts = " -Djava.endorsed.dirs=\"" + new File(new File(new File(System.getProperty("java.home"), "jre"), "lib"), "endorsed") + System.getProperty("path.separator") + new File(new File(System.getProperty("java.home"), "lib"), "endorsed") + System.getProperty("path.separator") + new File(libDir, "endorsed").getCanonicalPath() + "\""
                    + " -Djava.ext.dirs=\"" + new File(new File(new File(System.getProperty("java.home"), "jre"), "lib"), "ext") + System.getProperty("path.separator") + new File(new File(System.getProperty("java.home"), "lib"), "ext") + System.getProperty("path.separator") + new File(libDir, "ext").getCanonicalPath() + "\"";
        }
        String command = "\""
                + new File(System.getProperty("java.home"), ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath()
                + "\" " + opts
                + " " + karafOpts
                + " " + jdkOpts
                + " -Djava.util.logging.config.file=\"" + new File(location, "etc/java.util.logging.properties").getCanonicalPath() + "\""
                + " -Dkaraf.home=\"" + System.getProperty("karaf.home") + "\""
                + " -Dkaraf.base=\"" + new File(location).getCanonicalPath() + "\""
                + " -Dkaraf.data=\"" + new File(new File(location).getCanonicalPath(), "data") + "\""
                + " -Dkaraf.etc=\"" + new File(new File(location).getCanonicalPath(), "etc") + "\""
                + " -Djava.io.tmpdir=\"" + new File(new File(location).getCanonicalPath(), "data" + File.separator + "tmp") + "\""
                + " -Dkaraf.startLocalConsole=false"
                + " -Dkaraf.startRemoteShell=true"
                + " -classpath \"" + classpath.toString() + "\""
                + " org.apache.karaf.main.Main server";
        if (System.getenv("KARAF_REDIRECT") != null && !System.getenv("KARAF_REDIRECT").isEmpty()) {
            command = command + " >> " + System.getenv("KARAF_REDIRECT");
        }

        LOGGER.debug("Starting instance " + name + " with command: " + command);
        Process process = new ProcessBuilderFactoryImpl().newBuilder()
                .directory(new File(location))
                .command(command)
                .start();
        instance.pid = process.getPid();
    }

    private StringBuilder classpathFromLibDir(File libDir) throws IOException {
        File[] jars = libDir.listFiles((dir, name) -> name.endsWith(".jar"));
        StringBuilder classpath = new StringBuilder();
        if (jars != null) {
            for (File jar : jars) {
                if (classpath.length() > 0) {
                    classpath.append(System.getProperty("path.separator"));
                }
                classpath.append(jar.getCanonicalPath());
            }
        }
        return classpath;
    }

    private void addJar(StringBuilder sb, String groupId, String artifactId) {
        File artifactDir = new File(System.getProperty("karaf.home") + File.separator +
                                    "system" + File.separator +
                                    groupId.replaceAll("\\.", File.separator) + File.separator +
                                    artifactId + File.separator);
        TreeMap<String, File> jars = new TreeMap<>();
        String[] versions = artifactDir.list();
        if (versions != null) {
            for (String version : versions) {
                File jar = new File(artifactDir, version + File.separator + artifactId + "-" + version + ".jar");
                if (jar.exists()) {
                    jars.put(version, jar);
                }
            }
        }
        if (jars.isEmpty()) {
            throw new IllegalStateException("Cound not find jar for " + groupId + "/" + artifactId);
        }
        if (sb.length() > 0) {
            sb.append(File.pathSeparator);
        }
        sb.append(jars.lastEntry().getValue().getAbsolutePath());
    }

    public void restartInstance(final String name, final String javaOpts) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            String current = System.getProperty("karaf.name");
            if (name.equals(current)) {
                String location = System.getProperty("karaf.home");
                StringBuilder classpath = new StringBuilder();
                addJar(classpath, "org.apache.karaf.instance", "org.apache.karaf.instance.core");
                addJar(classpath, "org.apache.karaf.shell", "org.apache.karaf.shell.core");
                addJar(classpath, "org.ops4j.pax.logging", "pax-logging-api");
                addJar(classpath, "jline", "jline");
                String command = "\""
                        + new File(System.getProperty("java.home"), ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath()
                        + "\" "
                        + " -Djava.util.logging.config.file=\"" + new File(location, "etc/java.util.logging.properties").getCanonicalPath() + "\""
                        + " -Dkaraf.home=\"" + System.getProperty("karaf.home") + "\""
                        + " -Dkaraf.base=\"" + new File(location).getCanonicalPath() + "\""
                        + " -Dkaraf.data=\"" + new File(new File(location).getCanonicalPath(), "data") + "\""
                        + " -Dkaraf.etc=\"" + new File(new File(location).getCanonicalPath(), "etc") + "\""
                        + " -Dkaraf.instances=\"" + System.getProperty("karaf.instances") + "\""
                        + " -classpath \"" + classpath.toString() + "\""
                        + " " + Execute.class.getName()
                        + " restart --java-opts \"" + javaOpts + "\" " + name;
                new ProcessBuilderFactoryImpl().newBuilder()
                        .directory(new File(System.getProperty("karaf.home")))
                        .command(command)
                        .start();
            } else {
                checkPid(instance);
                if (instance.pid != 0) {
                    cleanShutdown(instance);
                }
                doStart(instance, name, javaOpts);
            }
            return null;
        }, true);
    }

    public void stopInstance(final String name) {
        Integer pid = execute(state -> {
            int rootInstancePID = 0;
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid == 0) {
                throw new IllegalStateException("Instance already stopped");
            }
            cleanShutdown(instance);
            if (instance.pid > 0) {
                if (!instance.root) {
                    Process process = new ProcessBuilderFactoryImpl().newBuilder().attach(instance.pid);
                    process.destroy();
                } else {
                    //can't simply destroy root instance here
                    //as it will lose the update in instances.properties
                    //because of no chance to run the saveData
                    rootInstancePID = instance.pid;
                }
                instance.pid = 0;

            }
            return rootInstancePID;
        }, true);
        if (pid != 0 && isInstanceRoot(name)) {
            Process process;
            try {
                process = new ProcessBuilderFactoryImpl().newBuilder().attach(pid);
                process.destroy(); 
            } catch (IOException e) {
                LOGGER.debug("Unable to cleanly shutdown root instance ", e);
            }
        }
    }

    public void destroyInstance(final String name) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance not stopped");
            }
            deleteFile(new File(instance.loc));
            state.instances.remove(name);
            InstanceServiceImpl.this.proxies.remove(name);
            return null;
        }, true);
    }

    public void renameInstance(final String oldName, final String newName, final boolean printOutput) throws Exception {
        execute(state -> {
            if (state.instances.get(newName) != null) {
                throw new IllegalArgumentException("Instance " + newName + " already exists");
            }
            InstanceState instance = state.instances.get(oldName);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + oldName + " not found");
            }
            if (instance.root) {
                throw new IllegalArgumentException("Root instance cannot be renamed");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance not stopped");
            }

            println("Renaming instance "
                    + SimpleAnsi.INTENSITY_BOLD + oldName + SimpleAnsi.INTENSITY_NORMAL
                    + " to "
                    + SimpleAnsi.INTENSITY_BOLD + newName + SimpleAnsi.INTENSITY_NORMAL);
            // rename directory
            String oldLocationPath = instance.loc;
            File oldLocation = new File(oldLocationPath);
            String basedir = oldLocation.getParent();
            File newLocation = new File(basedir, newName);
            oldLocation.renameTo(newLocation);
            // create the properties map including the instance name and instance location
            // TODO: replacing is bad, we should re-extract the needed files
            HashMap<String, String> props = new HashMap<>();
            props.put(oldName, newName);
            props.put(oldLocationPath, newLocation.getPath());
            // replace all references to the "old" name by the new one in etc/system.properties
            // NB: it's replacement to avoid to override the user's changes
            filterResource(newLocation, "etc/system.properties", props);
            // replace all references to the "old" name by the new one in bin/karaf
            filterResource(newLocation, "bin/karaf", props);
            filterResource(newLocation, "bin/start", props);
            filterResource(newLocation, "bin/stop", props);
            filterResource(newLocation, "bin/karaf.bat", props);
            filterResource(newLocation, "bin/start.bat", props);
            filterResource(newLocation, "bin/stop.bat", props);
            // update instance
            instance.name = newName;
            instance.loc = newLocation.getPath();
            state.instances.put(newName, instance);
            state.instances.remove(oldName);
            InstanceImpl proxy = InstanceServiceImpl.this.proxies.remove(oldName);
            if (proxy == null) {
                proxy = new InstanceImpl(InstanceServiceImpl.this, newName);
            } else {
                proxy.doSetName(newName);
            }
            InstanceServiceImpl.this.proxies.put(newName, proxy);
            return null;
        }, true);
    }

    public synchronized Instance cloneInstance(final String name, final String cloneName, final InstanceSettings settings, final boolean printOutput) throws Exception {
        final int instanceSshPort = getInstanceSshPort(name);
        final int instanceRmiRegistryPort = getInstanceRmiRegistryPort(name);
        final int instanceRmiServerPort = getInstanceRmiServerPort(name);
        return execute(state -> {
            if (state.instances.get(cloneName) != null) {
                throw new IllegalArgumentException("Instance " + cloneName + " already exists");
            }
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }

            // define the clone instance location
            String cloneLocationPath = settings.getLocation() != null ? settings.getLocation() : cloneName;
            File cloneLocation = new File(cloneLocationPath);
            if (!cloneLocation.isAbsolute()) {
                cloneLocation = new File(storageLocation, cloneLocationPath);
            }
            // copy instance directory
            String locationPath = instance.loc;
            File location = new File(locationPath);
            copy(location, cloneLocation);
            // create the properties map including the instance name, location, ssh and rmi port numbers
            // TODO: replacing stuff anywhere is not really good, we might end up replacing unwanted stuff
            // TODO: if no ports are overriden, shouldn't we choose new ports ?
            HashMap<String, String> props = new HashMap<>();
            props.put(name, cloneName);
            props.put(locationPath, cloneLocationPath);
            if (settings.getSshPort() > 0)
                props.put(Integer.toString(instanceSshPort), Integer.toString(settings.getSshPort()));
            if (settings.getRmiRegistryPort() > 0)
                props.put(Integer.toString(instanceRmiRegistryPort), Integer.toString(settings.getRmiRegistryPort()));
            if (settings.getRmiServerPort() > 0)
                props.put(Integer.toString(instanceRmiServerPort), Integer.toString(settings.getRmiServerPort()));

            // filtering clone files
            filterResource(cloneLocation, "etc/custom.properties", props);
            filterResource(cloneLocation, "etc/org.apache.karaf.management.cfg", props);
            filterResource(cloneLocation, "etc/org.apache.karaf.shell.cfg", props);
            filterResource(cloneLocation, "etc/system.properties", props);
            filterResource(cloneLocation, "bin/karaf", props);
            filterResource(cloneLocation, "bin/start", props);
            filterResource(cloneLocation, "bin/stop", props);
            filterResource(cloneLocation, "bin/karaf.bat", props);
            filterResource(cloneLocation, "bin/start.bat", props);
            filterResource(cloneLocation, "bin/stop.bat", props);
            // create and add the clone instance in the registry
            String javaOpts = settings.getJavaOpts();
            if (javaOpts == null || javaOpts.length() == 0) {
                javaOpts = DEFAULT_JAVA_OPTS;
            }
            InstanceState is = new InstanceState();
            is.name = cloneName;
            is.loc = cloneLocation.toString();
            is.opts = javaOpts;
            state.instances.put(cloneName, is);
            InstanceImpl cloneInstance = new InstanceImpl(InstanceServiceImpl.this, cloneName);
            InstanceServiceImpl.this.proxies.put(cloneName, cloneInstance);
            return cloneInstance;
        }, true);
    }

    private void checkPid(InstanceState instance) throws IOException {
        if (instance.pid != 0) {
            Process process = new ProcessBuilderFactoryImpl().newBuilder().attach(instance.pid);
            if (!process.isRunning()) {
                instance.pid = 0;
            }
        }
    }

    protected void cleanShutdown(InstanceState instance) {
        try {
            File file = new File(new File(instance.loc, "etc"), CONFIG_PROPERTIES_FILE_NAME);
            Properties props = PropertiesLoader.loadPropertiesFile(file.toURI().toURL(), false);
            props.put("karaf.base", new File(instance.loc).getCanonicalPath());
            props.put("karaf.home", System.getProperty("karaf.home"));
            props.put("karaf.data", new File(new File(instance.loc), "data").getCanonicalPath());
            props.put("karaf.etc", new File(new File(instance.loc), "etc").getCanonicalPath());
            InterpolationHelper.performSubstitution(props, null, true, false, true);
            int port = Integer.parseInt(props.getProperty(KARAF_SHUTDOWN_PORT, "0"));
            String host = props.getProperty(KARAF_SHUTDOWN_HOST, "localhost");
            String portFile = props.getProperty(KARAF_SHUTDOWN_PORT_FILE);
            String shutdown = props.getProperty(KARAF_SHUTDOWN_COMMAND, DEFAULT_SHUTDOWN_COMMAND);
            if (port == 0 && portFile != null) {
                BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(portFile)));
                String portStr = r.readLine();
                port = Integer.parseInt(portStr);
                r.close();
            }
            // We found the port, try to send the command
            if (port > 0) {
                Socket s = new Socket(host, port);
                s.getOutputStream().write(shutdown.getBytes());
                s.close();
                long stopTimeout = Long.parseLong(props.getProperty(KARAF_SHUTDOWN_TIMEOUT,
                                                                    Long.toString(getStopTimeout())));
                long t = System.currentTimeMillis() + stopTimeout;
                do {
                    Thread.sleep(100);
                    checkPid(instance);
                } while (System.currentTimeMillis() < t && instance.pid > 0);
            }
        } catch (Exception e) {
            LOGGER.debug("Unable to cleanly shutdown instance " + instance.name, e);
        }
    }<commit_msg>Add karaf.log support in instances<commit_after>/*
 * Licensed to the Apache Software Foundation (ASF) under one or more
 * contributor license agreements.  See the NOTICE file distributed with
 * this work for additional information regarding copyright ownership.
 * The ASF licenses this file to You under the Apache License, Version 2.0
 * (the "License"); you may not use this file except in compliance with
 * the License.  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.apache.karaf.instance.core.internal;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.InterruptedIOException;
import java.io.OutputStream;
import java.io.PrintStream;
import java.net.Socket;
import java.net.URL;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.attribute.PosixFilePermission;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.Set;
import java.util.TreeMap;

import org.apache.felix.utils.properties.InterpolationHelper;
import org.apache.felix.utils.properties.Properties;
import org.apache.felix.utils.properties.TypedProperties;
import org.apache.karaf.instance.core.Instance;
import org.apache.karaf.instance.core.InstanceService;
import org.apache.karaf.instance.core.InstanceSettings;
import org.apache.karaf.instance.main.Execute;
import org.apache.karaf.jpm.Process;
import org.apache.karaf.jpm.impl.ProcessBuilderFactoryImpl;
import org.apache.karaf.jpm.impl.ScriptUtils;
import org.apache.karaf.profile.Profile;
import org.apache.karaf.profile.ProfileBuilder;
import org.apache.karaf.profile.ProfileService;
import org.apache.karaf.shell.support.ansi.SimpleAnsi;
import org.apache.karaf.util.StreamUtils;
import org.apache.karaf.util.config.PropertiesLoader;
import org.apache.karaf.util.locks.FileLockUtils;
import org.osgi.framework.BundleContext;
import org.osgi.framework.FrameworkUtil;
import org.osgi.framework.ServiceReference;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

public class InstanceServiceImpl implements InstanceService {

    public static final String STORAGE_FILE = "instance.properties";
    public static final String BACKUP_EXTENSION = ".bak";
    private static final String FEATURES_CFG = "etc/org.apache.karaf.features.cfg";
    private static final String RESOURCE_BASE = "org/apache/karaf/instance/resources/";

    private static final Logger LOGGER = LoggerFactory.getLogger(InstanceServiceImpl.class);

    private static final String CONFIG_PROPERTIES_FILE_NAME = "config.properties";

    private static final String KARAF_SHUTDOWN_PORT = "karaf.shutdown.port";

    private static final String KARAF_SHUTDOWN_HOST = "karaf.shutdown.host";

    private static final String KARAF_SHUTDOWN_PORT_FILE = "karaf.shutdown.port.file";

    private static final String KARAF_SHUTDOWN_COMMAND = "karaf.shutdown.command";

    private static final String KARAF_SHUTDOWN_TIMEOUT = "karaf.shutdown.timeout";

    private static final String DEFAULT_SHUTDOWN_COMMAND = "SHUTDOWN";

    public static final String DEFAULT_JAVA_OPTS = "-server -Xmx512M -Dcom.sun.management.jmxremote -XX:+UnlockDiagnosticVMOptions";

    private LinkedHashMap<String, InstanceImpl> proxies = new LinkedHashMap<>();

    private File storageLocation;

    private long stopTimeout = 30000;

    static class InstanceState {
        String name;
        String loc;
        String opts;
        int pid;
        boolean root;
    }

    static class State {
        int defaultSshPortStart = 8101;
        int defaultRmiRegistryPortStart = 1099;
        int defaultRmiServerPortStart = 44444;
        Map<String, InstanceState> instances;
        public State() {
            //read port start value from the root instance configuration
            try {
                TypedProperties shellProperty = new TypedProperties();
                shellProperty.load(new File(System.getProperty("karaf.etc"), "org.apache.karaf.shell.cfg"));
                defaultSshPortStart = getInt(shellProperty,"sshPort", 8101);
                TypedProperties managementProperty = new TypedProperties();
                managementProperty.load(new File(System.getProperty("karaf.etc"), "org.apache.karaf.management.cfg"));
                defaultRmiRegistryPortStart = getInt(managementProperty, "rmiRegistryPort", 1099);
                defaultRmiServerPortStart = getInt(managementProperty, "rmiServerPort", 1099);
            } catch (Exception e) {
                LOGGER.debug("Could not read port start value from the root instance configuration.", e);
            }
        }
        
        
    }

    public InstanceServiceImpl() {
        String prop = System.getProperty("karaf.instances");
        if (prop != null) {
            storageLocation = new File(prop);
        }
    }

    public File getStorageLocation() {
        return storageLocation;
    }

    public void setStorageLocation(File storage) {
        this.storageLocation = storage;
    }

    public long getStopTimeout() {
        return stopTimeout;
    }

    public void setStopTimeout(long stopTimeout) {
        this.stopTimeout = stopTimeout;
    }

    private State loadData(TypedProperties storage) {
        State state = new State();
        int count = getInt(storage, "count", 0);
        state.defaultSshPortStart = getInt(storage, "ssh.port", state.defaultSshPortStart);
        state.defaultRmiRegistryPortStart = getInt(storage, "rmi.registry.port", state.defaultRmiRegistryPortStart);
        state.defaultRmiServerPortStart = getInt(storage, "rmi.server.port", state.defaultRmiServerPortStart);
        state.instances = new LinkedHashMap<>();

        for (int i = 0; i < count; i++) {
            InstanceState instance = new InstanceState();
            instance.name = getString(storage, "item." + i + ".name", null);
            instance.loc = getString(storage, "item." + i + ".loc", null);
            instance.opts = getString(storage, "item." + i + ".opts", null);
            instance.pid = getInt(storage, "item." + i + ".pid", 0);
            instance.root = getBool(storage, "item." + i + ".root", false);
            state.instances.put(instance.name, instance);
        }
        // Update proxies list
        for (InstanceState instance : state.instances.values()) {
            if (!this.proxies.containsKey(instance.name)) {
                proxies.put(instance.name, new InstanceImpl(this, instance.name));
            }
        }
        List<String> names = new ArrayList<>(this.proxies.keySet());
        for (String name : names) {
            if (!state.instances.containsKey(name)) {
                this.proxies.remove(name);
            }
        }
        return state;
    }

    private void saveData(State state, TypedProperties storage) {
        storage.put("ssh.port", Integer.toString(state.defaultSshPortStart));
        storage.put("rmi.registry.port", Integer.toString(state.defaultRmiRegistryPortStart));
        storage.put("rmi.server.port", Integer.toString(state.defaultRmiServerPortStart));
        storage.put("count", Integer.toString(state.instances.size()));
        int i = 0;
        for (InstanceState instance : state.instances.values()) {
            storage.put("item." + i + ".name", instance.name);
            storage.put("item." + i + ".root", Boolean.toString(instance.root));
            storage.put("item." + i + ".loc", instance.loc);
            storage.put("item." + i + ".pid", Integer.toString(instance.pid));
            storage.put("item." + i + ".opts", instance.opts != null ? instance.opts : "");
            i++;
        }
        while (storage.containsKey("item." + i + ".name")) {
            storage.remove("item." + i + ".name");
            storage.remove("item." + i + ".root");
            storage.remove("item." + i + ".loc");
            storage.remove("item." + i + ".pid");
            storage.remove("item." + i + ".opts");
            i++;
        }
    }

    private static boolean getBool(TypedProperties storage, String name, boolean def) {
        Object value = storage.get(name);
        if (value instanceof Boolean) {
            return (Boolean) value;
        } else if (value != null) {
            return Boolean.parseBoolean(value.toString());
        } else {
            return def;
        }
    }

    private static int getInt(TypedProperties storage, String name, int def) {
        Object value = storage.get(name);
        if (value instanceof Number) {
            return ((Number) value).intValue();
        } else if (value != null) {
            return Integer.parseInt(value.toString());
        } else {
            return def;
        }
    }

    private static String getString(TypedProperties storage, String name, String def) {
        Object value = storage.get(name);
        return value != null ? value.toString() : def;
    }

    interface Task<U, T> {
        T call(U state) throws IOException;
    }

    synchronized <T> T execute(final Task<State, T> callback, final boolean writeToFile) {
        final File storageFile = new File(storageLocation, STORAGE_FILE);
        if (!storageFile.exists()) {
            storageFile.getParentFile().mkdirs();
            try {
                storageFile.createNewFile();
            } catch (IOException e) {
                // Ignore
            }
        }
        if (storageFile.exists()) {
            if (!storageFile.isFile()) {
                throw new IllegalStateException("Instance storage location should be a file: " + storageFile);
            }
            try {
                return FileLockUtils.execute(storageFile, properties -> {
                    State state = loadData(properties);
                    T t = callback.call(state);
                    if (writeToFile) {
                        saveData(state, properties);
                    }
                    return t;
                }, writeToFile);
            } catch (IOException e) {
                throw new RuntimeException(e);
            }
        } else {
            throw new IllegalStateException("Instance storage location does not exist: " + storageFile);
        }
    }

    private static void logInfo(String message, boolean printOutput, Object... args) {
        if (LOGGER.isInfoEnabled() || printOutput) {
            String formatted = String.format(message, args);
            LOGGER.info(formatted);
            if (printOutput) {
                println(formatted);
            }
        }
    }
    
    private static void logDebug(String message, boolean printOutput, Object... args) {
        if (LOGGER.isDebugEnabled() || printOutput) {
            String formatted = String.format(message, args);
            LOGGER.debug(formatted);
            if (printOutput) {
                println(formatted);
            }
        }
    }

    public synchronized Instance createInstance(final String name, final InstanceSettings settings, final boolean printOutput) throws Exception {
        return execute(state -> {
            if (state.instances.get(name) != null) {
                throw new IllegalArgumentException("Instance '" + name + "' already exists");
            }
            if (!settings.getProfiles().isEmpty()) {
                try {
                    ProfileApplier.verify();
                } catch (NoClassDefFoundError error) {
                    throw new IllegalArgumentException("Profile service package is not available");
                }
            }

            String loc = settings.getLocation() != null ? settings.getLocation() : name;
            File karafBase = new File(loc);
            if (!karafBase.isAbsolute()) {
                karafBase = new File(storageLocation, loc);
            }
            int sshPort = settings.getSshPort();
            if (sshPort <= 0) {
                sshPort = ++state.defaultSshPortStart;
            }
            String sshHost = settings.getAddress();
            int rmiRegistryPort = settings.getRmiRegistryPort();
            if (rmiRegistryPort <= 0) {
                rmiRegistryPort = ++state.defaultRmiRegistryPortStart;
            }
            int rmiServerPort = settings.getRmiServerPort();
            if (rmiServerPort <= 0) {
                rmiServerPort = ++state.defaultRmiServerPortStart;
            }
            logInfo("Creating new instance on SSH port %d and registry port %d / RMI server port %d at: %s",
                    printOutput, sshPort, rmiRegistryPort, rmiServerPort, karafBase);

            mkdir(karafBase, "bin", printOutput);
            mkdir(karafBase, "etc", printOutput);
            mkdir(karafBase, "etc/scripts", printOutput);
            mkdir(karafBase, "system", printOutput);
            mkdir(karafBase, "deploy", printOutput);
            mkdir(karafBase, "data", printOutput);

            Map<String, URL> textResources = new HashMap<>(settings.getTextResources());
            Map<String, URL> binaryResources = new HashMap<>(settings.getBinaryResources());

            String[] resources =
            {
                    "etc/all.policy",
                    "etc/config.properties",
                    "etc/custom.properties",
                    "etc/distribution.info",
                    "etc/equinox-debug.properties",
                    "etc/java.util.logging.properties",
                    "etc/jmx.acl.cfg",
                    "etc/jmx.acl.java.lang.Memory.cfg",
                    "etc/jmx.acl.org.apache.karaf.bundle.cfg",
                    "etc/jmx.acl.org.apache.karaf.config.cfg",
                    "etc/jmx.acl.org.apache.karaf.security.jmx.cfg",
                    "etc/jmx.acl.osgi.compendium.cm.cfg",
                    "etc/jre.properties",
                    "etc/keys.properties",
                    "etc/org.apache.felix.eventadmin.impl.EventAdmin.cfg",
                    "etc/org.apache.felix.fileinstall-deploy.cfg",
                    "etc/org.apache.karaf.command.acl.bundle.cfg",
                    "etc/org.apache.karaf.command.acl.config.cfg",
                    "etc/org.apache.karaf.command.acl.feature.cfg",
                    "etc/org.apache.karaf.command.acl.jaas.cfg",
                    "etc/org.apache.karaf.command.acl.kar.cfg",
                    "etc/org.apache.karaf.command.acl.scope_bundle.cfg",
                    "etc/org.apache.karaf.command.acl.shell.cfg",
                    "etc/org.apache.karaf.command.acl.system.cfg",
                    "etc/org.apache.karaf.features.repos.cfg",
                    "etc/org.apache.karaf.jaas.cfg",
                    "etc/org.apache.karaf.kar.cfg",
                    "etc/org.apache.karaf.log.cfg",
                    "etc/org.ops4j.pax.logging.cfg",
                    "etc/org.ops4j.pax.url.mvn.cfg",
                    "etc/shell.init.script",
                    "etc/users.properties",
                    "etc/scripts/shell.completion.script",
                    FEATURES_CFG
            };
            copyResourcesToDir(resources, karafBase, textResources, printOutput);
            addFeaturesFromSettings(new File(karafBase, FEATURES_CFG), settings);

            // The startup.properties is now generated by the karaf maven plugin, so
            // we use the one from the root instance instead of embedding it
            File rootEtc = new File(System.getProperty("karaf.etc"));
            copy(new File(rootEtc, "startup.properties"), new File(karafBase, "etc/startup.properties"));

            // align child with any bundles we have overriden in the root instance
            File rootOverrides = new File(rootEtc, "overrides.properties");
            if (rootOverrides.exists()) {
                copy(rootOverrides, new File(karafBase, "etc/overrides.properties"));
            }

            HashMap<String, String> props = new HashMap<>();
            props.put("${SUBST-KARAF-NAME}", name);
            props.put("${SUBST-KARAF-HOME}", System.getProperty("karaf.home"));
            props.put("${SUBST-KARAF-BASE}", karafBase.getPath());
            props.put("${SUBST-SSH-PORT}", Integer.toString(sshPort));
            props.put("${SUBST-SSH-HOST}", sshHost);
            props.put("${SUBST-RMI-REGISTRY-PORT}", Integer.toString(rmiRegistryPort));
            props.put("${SUBST-RMI-SERVER-PORT}", Integer.toString(rmiServerPort));

            String[] filteredResources =
            {
                 "etc/system.properties",
                 "etc/org.apache.karaf.shell.cfg",
                 "etc/org.apache.karaf.management.cfg",
                 "bin/karaf",
                 "bin/start",
                 "bin/stop",
                 "bin/karaf.bat",
                 "bin/start.bat",
                 "bin/stop.bat"
            };
            copyFilteredResourcesToDir(filteredResources, karafBase, textResources, props, printOutput);

            try {
                makeFileExecutable(new File(karafBase, "bin/karaf"));
                makeFileExecutable(new File(karafBase, "bin/start"));
                makeFileExecutable(new File(karafBase, "bin/stop"));
            } catch (IOException e) {
                LOGGER.debug("Could not set file mode on scripts.", e);
            }

            for (String resource : textResources.keySet()) {
                copyFilteredResourceToDir(resource, karafBase, textResources, props, printOutput);
            }

            for (String resource : binaryResources.keySet()) {
                copyBinaryResourceToDir(resource, karafBase, binaryResources, printOutput);
            }

            if (!settings.getProfiles().isEmpty()) {
                ProfileApplier.applyProfiles(karafBase, settings.getProfiles(), printOutput);
            }

            String javaOpts = settings.getJavaOpts();
            if (javaOpts == null || javaOpts.length() == 0) {
                javaOpts = DEFAULT_JAVA_OPTS;
            }
            InstanceState is = new InstanceState();
            is.name = name;
            is.loc = karafBase.toString();
            is.opts = javaOpts;
            state.instances.put(name, is);
            InstanceImpl instance = new InstanceImpl(InstanceServiceImpl.this, name);
            InstanceServiceImpl.this.proxies.put(name, instance);
            return instance;
        }, true);
    }

    void addFeaturesFromSettings(File featuresCfg, final InstanceSettings settings) throws IOException {
        FileLockUtils.execute(featuresCfg, properties -> {
            appendToPropList(properties, "featuresBoot", settings.getFeatures());
            appendToPropList(properties, "featuresRepositories", settings.getFeatureURLs());
        }, true);
    }

    private static void appendToPropList(TypedProperties p, String key, List<String> elements) {
        if (elements == null) {
            return;
        }
        StringBuilder sb = new StringBuilder(p.get(key).toString().trim());
        for (String f : elements) {
            if (sb.length() > 0) {
                sb.append(',');
            }
            sb.append(f);
        }
        p.put(key, sb.toString());
    }

    public Instance[] getInstances() {
        return execute(state -> proxies.values().toArray(new Instance[proxies.size()]), false);
    }

    public Instance getInstance(final String name) {
        return execute(state -> proxies.get(name), false);
    }

    public void startInstance(final String name, final String javaOpts) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance already started");
            }
            doStart(instance, name, javaOpts);
            return null;
        }, true);
    }

    private void doStart(InstanceState instance, String name, String javaOpts) throws IOException {
        String opts = javaOpts;
        if (opts == null || opts.length() == 0) {
            opts = instance.opts;
        }
        if (opts == null || opts.length() == 0) {
            opts = DEFAULT_JAVA_OPTS;
        }

        // fallback and read karafOpts from KARAF_OPTS environment if no System property present
        String karafOptsEnv = System.getenv("KARAF_OPTS");
        String karafOpts = System.getProperty("karaf.opts", karafOptsEnv != null ? karafOptsEnv : "");

        String location = instance.loc;

        File libDir = new File(System.getProperty("karaf.home"), "lib");
        File bootLibDir = new File(libDir, "boot");
        File childLibDir = new File(location, "lib");

        StringBuilder classpath = classpathFromLibDir(bootLibDir);
        StringBuilder childClasspath = classpathFromLibDir(childLibDir);
        if (childClasspath.length() > 0 && !bootLibDir.equals(childLibDir)) {
            classpath.append(System.getProperty("path.separator"));
            classpath.append(childClasspath);
        }

        String jdkOpts;
        if (!System.getProperty("java.version").startsWith("1.")) {
            jdkOpts = " --add-opens java.base/java.security=ALL-UNNAMED" +
                      " --add-opens java.base/java.net=ALL-UNNAMED" +
                      " --add-opens java.base/java.lang=ALL-UNNAMED" +
                      " --add-opens java.base/java.util=ALL-UNNAMED" +
                      " --add-opens java.naming/javax.naming.spi=ALL-UNNAMED" +
                      " --add-opens java.rmi/sun.rmi.transport.tcp=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.http=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.https=ALL-UNNAMED" +
                      " --add-exports=java.base/sun.net.www.protocol.jar=ALL-UNNAMED" +
                      " --add-exports=java.xml.bind/com.sun.xml.internal.bind.v2.runtime=ALL-UNNAMED" +
                      " --add-exports=jdk.xml.dom/org.w3c.dom.html=ALL-UNNAMED" +
                      " --add-exports=jdk.naming.rmi/com.sun.jndi.url.rmi=ALL-UNNAMED" +
                      " --add-modules java.xml.ws.annotation,java.corba,java.transaction,java.xml.bind,java.xml.ws";
        } else {
            jdkOpts = " -Djava.endorsed.dirs=\"" + new File(new File(new File(System.getProperty("java.home"), "jre"), "lib"), "endorsed") + System.getProperty("path.separator") + new File(new File(System.getProperty("java.home"), "lib"), "endorsed") + System.getProperty("path.separator") + new File(libDir, "endorsed").getCanonicalPath() + "\""
                    + " -Djava.ext.dirs=\"" + new File(new File(new File(System.getProperty("java.home"), "jre"), "lib"), "ext") + System.getProperty("path.separator") + new File(new File(System.getProperty("java.home"), "lib"), "ext") + System.getProperty("path.separator") + new File(libDir, "ext").getCanonicalPath() + "\"";
        }
        String command = "\""
                + new File(System.getProperty("java.home"), ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath()
                + "\" " + opts
                + " " + karafOpts
                + " " + jdkOpts
                + " -Djava.util.logging.config.file=\"" + new File(location, "etc/java.util.logging.properties").getCanonicalPath() + "\""
                + " -Dkaraf.home=\"" + System.getProperty("karaf.home") + "\""
                + " -Dkaraf.base=\"" + new File(location).getCanonicalPath() + "\""
                + " -Dkaraf.data=\"" + new File(new File(location).getCanonicalPath(), "data") + "\""
                + " -Dkaraf.etc=\"" + new File(new File(location).getCanonicalPath(), "etc") + "\""
                + " -Dkaraf.log=\"" + new File(new File(new File(location).getCanonicalFile(), "data"), "log") + "\""
                + " -Djava.io.tmpdir=\"" + new File(new File(location).getCanonicalPath(), "data" + File.separator + "tmp") + "\""
                + " -Dkaraf.startLocalConsole=false"
                + " -Dkaraf.startRemoteShell=true"
                + " -classpath \"" + classpath.toString() + "\""
                + " org.apache.karaf.main.Main server";
        if (System.getenv("KARAF_REDIRECT") != null && !System.getenv("KARAF_REDIRECT").isEmpty()) {
            command = command + " >> " + System.getenv("KARAF_REDIRECT");
        }

        LOGGER.debug("Starting instance " + name + " with command: " + command);
        Process process = new ProcessBuilderFactoryImpl().newBuilder()
                .directory(new File(location))
                .command(command)
                .start();
        instance.pid = process.getPid();
    }

    private StringBuilder classpathFromLibDir(File libDir) throws IOException {
        File[] jars = libDir.listFiles((dir, name) -> name.endsWith(".jar"));
        StringBuilder classpath = new StringBuilder();
        if (jars != null) {
            for (File jar : jars) {
                if (classpath.length() > 0) {
                    classpath.append(System.getProperty("path.separator"));
                }
                classpath.append(jar.getCanonicalPath());
            }
        }
        return classpath;
    }

    private void addJar(StringBuilder sb, String groupId, String artifactId) {
        File artifactDir = new File(System.getProperty("karaf.home") + File.separator +
                                    "system" + File.separator +
                                    groupId.replaceAll("\\.", File.separator) + File.separator +
                                    artifactId + File.separator);
        TreeMap<String, File> jars = new TreeMap<>();
        String[] versions = artifactDir.list();
        if (versions != null) {
            for (String version : versions) {
                File jar = new File(artifactDir, version + File.separator + artifactId + "-" + version + ".jar");
                if (jar.exists()) {
                    jars.put(version, jar);
                }
            }
        }
        if (jars.isEmpty()) {
            throw new IllegalStateException("Cound not find jar for " + groupId + "/" + artifactId);
        }
        if (sb.length() > 0) {
            sb.append(File.pathSeparator);
        }
        sb.append(jars.lastEntry().getValue().getAbsolutePath());
    }

    public void restartInstance(final String name, final String javaOpts) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            String current = System.getProperty("karaf.name");
            if (name.equals(current)) {
                String location = System.getProperty("karaf.home");
                StringBuilder classpath = new StringBuilder();
                addJar(classpath, "org.apache.karaf.instance", "org.apache.karaf.instance.core");
                addJar(classpath, "org.apache.karaf.shell", "org.apache.karaf.shell.core");
                addJar(classpath, "org.ops4j.pax.logging", "pax-logging-api");
                addJar(classpath, "jline", "jline");
                String command = "\""
                        + new File(System.getProperty("java.home"), ScriptUtils.isWindows() ? "bin\\java.exe" : "bin/java").getCanonicalPath()
                        + "\" "
                        + " -Djava.util.logging.config.file=\"" + new File(location, "etc/java.util.logging.properties").getCanonicalPath() + "\""
                        + " -Dkaraf.home=\"" + System.getProperty("karaf.home") + "\""
                        + " -Dkaraf.base=\"" + new File(location).getCanonicalPath() + "\""
                        + " -Dkaraf.data=\"" + new File(new File(location).getCanonicalPath(), "data") + "\""
                        + " -Dkaraf.etc=\"" + new File(new File(location).getCanonicalPath(), "etc") + "\""
                        + " -Dkaraf.log=\"" + new File(new File(new File(location).getCanonicalFile(), "data"), "log") + "\""
                        + " -Dkaraf.instances=\"" + System.getProperty("karaf.instances") + "\""
                        + " -classpath \"" + classpath.toString() + "\""
                        + " " + Execute.class.getName()
                        + " restart --java-opts \"" + javaOpts + "\" " + name;
                new ProcessBuilderFactoryImpl().newBuilder()
                        .directory(new File(System.getProperty("karaf.home")))
                        .command(command)
                        .start();
            } else {
                checkPid(instance);
                if (instance.pid != 0) {
                    cleanShutdown(instance);
                }
                doStart(instance, name, javaOpts);
            }
            return null;
        }, true);
    }

    public void stopInstance(final String name) {
        Integer pid = execute(state -> {
            int rootInstancePID = 0;
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid == 0) {
                throw new IllegalStateException("Instance already stopped");
            }
            cleanShutdown(instance);
            if (instance.pid > 0) {
                if (!instance.root) {
                    Process process = new ProcessBuilderFactoryImpl().newBuilder().attach(instance.pid);
                    process.destroy();
                } else {
                    //can't simply destroy root instance here
                    //as it will lose the update in instances.properties
                    //because of no chance to run the saveData
                    rootInstancePID = instance.pid;
                }
                instance.pid = 0;

            }
            return rootInstancePID;
        }, true);
        if (pid != 0 && isInstanceRoot(name)) {
            Process process;
            try {
                process = new ProcessBuilderFactoryImpl().newBuilder().attach(pid);
                process.destroy(); 
            } catch (IOException e) {
                LOGGER.debug("Unable to cleanly shutdown root instance ", e);
            }
        }
    }

    public void destroyInstance(final String name) {
        execute(state -> {
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance not stopped");
            }
            deleteFile(new File(instance.loc));
            state.instances.remove(name);
            InstanceServiceImpl.this.proxies.remove(name);
            return null;
        }, true);
    }

    public void renameInstance(final String oldName, final String newName, final boolean printOutput) throws Exception {
        execute(state -> {
            if (state.instances.get(newName) != null) {
                throw new IllegalArgumentException("Instance " + newName + " already exists");
            }
            InstanceState instance = state.instances.get(oldName);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + oldName + " not found");
            }
            if (instance.root) {
                throw new IllegalArgumentException("Root instance cannot be renamed");
            }
            checkPid(instance);
            if (instance.pid != 0) {
                throw new IllegalStateException("Instance not stopped");
            }

            println("Renaming instance "
                    + SimpleAnsi.INTENSITY_BOLD + oldName + SimpleAnsi.INTENSITY_NORMAL
                    + " to "
                    + SimpleAnsi.INTENSITY_BOLD + newName + SimpleAnsi.INTENSITY_NORMAL);
            // rename directory
            String oldLocationPath = instance.loc;
            File oldLocation = new File(oldLocationPath);
            String basedir = oldLocation.getParent();
            File newLocation = new File(basedir, newName);
            oldLocation.renameTo(newLocation);
            // create the properties map including the instance name and instance location
            // TODO: replacing is bad, we should re-extract the needed files
            HashMap<String, String> props = new HashMap<>();
            props.put(oldName, newName);
            props.put(oldLocationPath, newLocation.getPath());
            // replace all references to the "old" name by the new one in etc/system.properties
            // NB: it's replacement to avoid to override the user's changes
            filterResource(newLocation, "etc/system.properties", props);
            // replace all references to the "old" name by the new one in bin/karaf
            filterResource(newLocation, "bin/karaf", props);
            filterResource(newLocation, "bin/start", props);
            filterResource(newLocation, "bin/stop", props);
            filterResource(newLocation, "bin/karaf.bat", props);
            filterResource(newLocation, "bin/start.bat", props);
            filterResource(newLocation, "bin/stop.bat", props);
            // update instance
            instance.name = newName;
            instance.loc = newLocation.getPath();
            state.instances.put(newName, instance);
            state.instances.remove(oldName);
            InstanceImpl proxy = InstanceServiceImpl.this.proxies.remove(oldName);
            if (proxy == null) {
                proxy = new InstanceImpl(InstanceServiceImpl.this, newName);
            } else {
                proxy.doSetName(newName);
            }
            InstanceServiceImpl.this.proxies.put(newName, proxy);
            return null;
        }, true);
    }

    public synchronized Instance cloneInstance(final String name, final String cloneName, final InstanceSettings settings, final boolean printOutput) throws Exception {
        final int instanceSshPort = getInstanceSshPort(name);
        final int instanceRmiRegistryPort = getInstanceRmiRegistryPort(name);
        final int instanceRmiServerPort = getInstanceRmiServerPort(name);
        return execute(state -> {
            if (state.instances.get(cloneName) != null) {
                throw new IllegalArgumentException("Instance " + cloneName + " already exists");
            }
            InstanceState instance = state.instances.get(name);
            if (instance == null) {
                throw new IllegalArgumentException("Instance " + name + " not found");
            }

            // define the clone instance location
            String cloneLocationPath = settings.getLocation() != null ? settings.getLocation() : cloneName;
            File cloneLocation = new File(cloneLocationPath);
            if (!cloneLocation.isAbsolute()) {
                cloneLocation = new File(storageLocation, cloneLocationPath);
            }
            // copy instance directory
            String locationPath = instance.loc;
            File location = new File(locationPath);
            copy(location, cloneLocation);
            // create the properties map including the instance name, location, ssh and rmi port numbers
            // TODO: replacing stuff anywhere is not really good, we might end up replacing unwanted stuff
            // TODO: if no ports are overriden, shouldn't we choose new ports ?
            HashMap<String, String> props = new HashMap<>();
            props.put(name, cloneName);
            props.put(locationPath, cloneLocationPath);
            if (settings.getSshPort() > 0)
                props.put(Integer.toString(instanceSshPort), Integer.toString(settings.getSshPort()));
            if (settings.getRmiRegistryPort() > 0)
                props.put(Integer.toString(instanceRmiRegistryPort), Integer.toString(settings.getRmiRegistryPort()));
            if (settings.getRmiServerPort() > 0)
                props.put(Integer.toString(instanceRmiServerPort), Integer.toString(settings.getRmiServerPort()));

            // filtering clone files
            filterResource(cloneLocation, "etc/custom.properties", props);
            filterResource(cloneLocation, "etc/org.apache.karaf.management.cfg", props);
            filterResource(cloneLocation, "etc/org.apache.karaf.shell.cfg", props);
            filterResource(cloneLocation, "etc/system.properties", props);
            filterResource(cloneLocation, "bin/karaf", props);
            filterResource(cloneLocation, "bin/start", props);
            filterResource(cloneLocation, "bin/stop", props);
            filterResource(cloneLocation, "bin/karaf.bat", props);
            filterResource(cloneLocation, "bin/start.bat", props);
            filterResource(cloneLocation, "bin/stop.bat", props);
            // create and add the clone instance in the registry
            String javaOpts = settings.getJavaOpts();
            if (javaOpts == null || javaOpts.length() == 0) {
                javaOpts = DEFAULT_JAVA_OPTS;
            }
            InstanceState is = new InstanceState();
            is.name = cloneName;
            is.loc = cloneLocation.toString();
            is.opts = javaOpts;
            state.instances.put(cloneName, is);
            InstanceImpl cloneInstance = new InstanceImpl(InstanceServiceImpl.this, cloneName);
            InstanceServiceImpl.this.proxies.put(cloneName, cloneInstance);
            return cloneInstance;
        }, true);
    }

    private void checkPid(InstanceState instance) throws IOException {
        if (instance.pid != 0) {
            Process process = new ProcessBuilderFactoryImpl().newBuilder().attach(instance.pid);
            if (!process.isRunning()) {
                instance.pid = 0;
            }
        }
    }

    protected void cleanShutdown(InstanceState instance) {
        try {
            File file = new File(new File(instance.loc, "etc"), CONFIG_PROPERTIES_FILE_NAME);
            Properties props = PropertiesLoader.loadPropertiesFile(file.toURI().toURL(), false);
            props.put("karaf.base", new File(instance.loc).getCanonicalPath());
            props.put("karaf.home", System.getProperty("karaf.home"));
            props.put("karaf.data", new File(new File(instance.loc), "data").getCanonicalPath());
            props.put("karaf.etc", new File(new File(instance.loc), "etc").getCanonicalPath());
            props.put("karaf.log", new File(new File(new File(instance.loc), "data"), "log").getCanonicalPath());
            InterpolationHelper.performSubstitution(props, null, true, false, true);
            int port = Integer.parseInt(props.getProperty(KARAF_SHUTDOWN_PORT, "0"));
            String host = props.getProperty(KARAF_SHUTDOWN_HOST, "localhost");
            String portFile = props.getProperty(KARAF_SHUTDOWN_PORT_FILE);
            String shutdown = props.getProperty(KARAF_SHUTDOWN_COMMAND, DEFAULT_SHUTDOWN_COMMAND);
            if (port == 0 && portFile != null) {
                BufferedReader r = new BufferedReader(new InputStreamReader(new FileInputStream(portFile)));
                String portStr = r.readLine();
                port = Integer.parseInt(portStr);
                r.close();
            }
            // We found the port, try to send the command
            if (port > 0) {
                Socket s = new Socket(host, port);
                s.getOutputStream().write(shutdown.getBytes());
                s.close();
                long stopTimeout = Long.parseLong(props.getProperty(KARAF_SHUTDOWN_TIMEOUT,
                                                                    Long.toString(getStopTimeout())));
                long t = System.currentTimeMillis() + stopTimeout;
                do {
                    Thread.sleep(100);
                    checkPid(instance);
                } while (System.currentTimeMillis() < t && instance.pid > 0);
            }
        } catch (Exception e) {
            LOGGER.debug("Unable to cleanly shutdown instance " + instance.name, e);
        }
    }
{'fraction_non_alphanumeric': 0.11849295722759146, 'fraction_numerical': 0.001989414350800064, 'mean_word_length': 2.049088254015801, 'pattern_counts': {'":': 0, '<': 57, '<?xml version=': 0, '>': 95, 'https://': 0, 'lorem ipsum': 0, 'www.': 8, 'xml': 12}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '1120619', 'n_tokens_mistral': 21743, 'n_tokens_neox': 21177, 'n_words': 4601}