ServerProcessIO.java

/*
 * 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.tika.pipes.core;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.stream.Stream;

import org.slf4j.Logger;

/**
 * Helpers for child pipes-server JVM stdio.
 * <p>
 * The managers default to {@link ProcessBuilder#inheritIO()} for the child's
 * stdout/stderr -- the production case is container deployments
 * (Docker/Kubernetes) where stdio is the canonical log stream and gets
 * picked up by the container runtime / log aggregator. The pipes server's
 * default log4j2 config writes to {@code SYSTEM_ERR}; inheriting routes
 * those records to the operator's existing observability stack with no
 * extra wiring.
 * <p>
 * A previous version of this code defaulted to DISCARD to work around a
 * Windows-only CI hang where surefire's pipe reader waited on a child that
 * had inherited (and was still holding) the parent JVM's stderr handle.
 * That hang is now mitigated structurally by the parent-PID watch in
 * {@code PipesServer.watchParentProcess()} -- when the parent dies, the
 * child notices via {@code ProcessHandle.onExit()} and exits within
 * milliseconds, releasing the inherited handle and letting upstream
 * readers see EOF.
 * <p>
 * To suppress child stdio (e.g. embedded use cases that don't want
 * pipes-server log records interleaved with their own stream, or rare
 * test environments where the parent-watch isn't enough), set the system
 * property {@value #STDIO_MODE_PROPERTY} to {@value #STDIO_MODE_DISCARD}
 * on the parent JVM.
 * <p>
 * Native JVM crashes ({@code hs_err_pid<N>.log}) are always redirected into
 * the manager's per-server tmpDir via {@code -XX:ErrorFile=}, and
 * {@link #surfaceCrashDiagnostics(Logger, String, Path)} reads any found
 * crash logs into the parent SLF4J logger before cleanup.
 */
final class ServerProcessIO {

    /** System property selecting child stdio handling.
     *  See class javadoc. */
    static final String STDIO_MODE_PROPERTY = "tika.pipes.server.stdio";

    /** Property value to suppress child stdio. Any other value (or unset)
     *  inherits. */
    static final String STDIO_MODE_DISCARD = "discard";

    private ServerProcessIO() {
    }

    /**
     * Returns {@code true} (the default) unless the operator has set
     * {@value #STDIO_MODE_PROPERTY}={@value #STDIO_MODE_DISCARD} to silence
     * the child's stdio.
     */
    static boolean inheritStdio() {
        return !STDIO_MODE_DISCARD.equalsIgnoreCase(
                System.getProperty(STDIO_MODE_PROPERTY));
    }

    /**
     * Emits any {@code hs_err_pid<N>.log} JVM crash logs found in
     * {@code tmpDir} via {@code log.error} so they show up in the parent's
     * log output. Call this on every abnormal-exit path before tmpDir gets
     * deleted, otherwise the diagnostics disappear with the temp dir.
     */
    static void surfaceCrashDiagnostics(Logger log, String contextLabel, Path tmpDir) {
        if (tmpDir == null || !Files.isDirectory(tmpDir)) {
            return;
        }
        try (Stream<Path> entries = Files.list(tmpDir)) {
            entries.filter(ServerProcessIO::isJvmCrashLog).forEach(p -> {
                try {
                    log.error("{}: JVM crash log {}:\n{}", contextLabel,
                            p.getFileName(),
                            Files.readString(p, StandardCharsets.UTF_8));
                } catch (IOException e) {
                    log.warn("{}: failed to read JVM crash log {}: {}",
                            contextLabel, p.getFileName(), e.toString());
                }
            });
        } catch (IOException e) {
            log.warn("{}: failed to list tmpDir for hs_err logs: {}",
                    contextLabel, e.toString());
        }
    }

    private static boolean isJvmCrashLog(Path p) {
        String name = p.getFileName().toString();
        return name.startsWith("hs_err_pid") && name.endsWith(".log");
    }
}