/*
 * Licensed to Elasticsearch under one or more contributor
 * license agreements. See the NOTICE file distributed with
 * this work for additional information regarding copyright
 * ownership. Elasticsearch 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.elasticsearch.bootstrap;

import org.apache.lucene.util.IOUtils;
import org.elasticsearch.env.Environment;
import org.elasticsearch.plugins.Platforms;
import org.elasticsearch.plugins.PluginInfo;

import java.io.Closeable;
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.atomic.AtomicBoolean;

/**
 * Spawns native plugin controller processes if present. Will only work prior to a system call
 * filter being installed.
 */
final class Spawner implements Closeable {

    /*
     * References to the processes that have been spawned, so that we can destroy them.
     */
    private final List<Process> processes = new ArrayList<>();
    private AtomicBoolean spawned = new AtomicBoolean();

    @Override
    public void close() throws IOException {
        IOUtils.close(() -> processes.stream().map(s -> (Closeable) s::destroy).iterator());
    }

    /**
     * Spawns the native controllers for each plugin
     *
     * @param environment the node environment
     * @throws IOException if an I/O error occurs reading the plugins or spawning a native process
     */
    void spawnNativePluginControllers(final Environment environment) throws IOException {
        if (!spawned.compareAndSet(false, true)) {
            throw new IllegalStateException("native controllers already spawned");
        }
        final Path pluginsFile = environment.pluginsFile();
        if (!Files.exists(pluginsFile)) {
            throw new IllegalStateException("plugins directory [" + pluginsFile + "] not found");
        }
        /*
         * For each plugin, attempt to spawn the controller daemon. Silently ignore any plugin that
         * don't include a controller for the correct platform.
         */
        try (DirectoryStream<Path> stream = Files.newDirectoryStream(pluginsFile)) {
            for (final Path plugin : stream) {
                final PluginInfo info = PluginInfo.readFromProperties(plugin);
                final Path spawnPath = Platforms.nativeControllerPath(plugin);
                if (!Files.isRegularFile(spawnPath)) {
                    continue;
                }
                if (!info.hasNativeController()) {
                    final String message = String.format(
                            Locale.ROOT,
                            "plugin [%s] does not have permission to fork native controller",
                            plugin.getFileName());
                    throw new IllegalArgumentException(message);
                }
                final Process process =
                        spawnNativePluginController(spawnPath, environment.tmpFile());
                processes.add(process);
            }
        }
    }

    /**
     * Attempt to spawn the controller daemon for a given plugin. The spawned process will remain
     * connected to this JVM via its stdin, stdout, and stderr streams, but the references to these
     * streams are not available to code outside this package.
     */
    private Process spawnNativePluginController(
            final Path spawnPath,
            final Path tmpPath) throws IOException {
        final ProcessBuilder pb = new ProcessBuilder(spawnPath.toString());

        // the only environment variable passes on the path to the temporary directory
        pb.environment().clear();
        pb.environment().put("TMPDIR", tmpPath.toString());

        // the output stream of the process object corresponds to the daemon's stdin
        return pb.start();
    }

    /**
     * The collection of processes representing spawned native controllers.
     *
     * @return the processes
     */
    List<Process> getProcesses() {
        return Collections.unmodifiableList(processes);
    }

}
