diff --git a/devenv-module.nix b/devenv-module.nix index be4bdc1fe..9cb374f7c 100644 --- a/devenv-module.nix +++ b/devenv-module.nix @@ -148,6 +148,31 @@ that is defined in flake-module.nix touch $out ''; }; + + run-devserver-sigterm = pkgs.stdenv.mkDerivation { + name = "run-devserver-sigterm"; + src = self; + sourceRoot = "source"; + nativeBuildInputs = [ + (pkgs.ghc.ghc.withPackages (p: with p; [ + ihp ihp-ide ihp-schema-compiler + ])) + pkgs.gnumake + pkgs.postgresql + pkgs.procps + ]; + buildPhase = '' + export IHP_LIB=${self.packages.${system}.ihp-env-var-backwards-compat} + export IHP_STATIC=${self.packages.${system}.ihp-static} + export PS_BIN=${pkgs.procps}/bin/ps + export RUN_DEVSERVER=${pkgs.ghc.ihp-ide}/bin/RunDevServer + + bash integration-test/run-devserver-sigterm-check.sh + ''; + installPhase = '' + touch $out + ''; + }; } # GHC 9.12 compatibility checks (build and test all IHP packages) diff --git a/integration-test/.ghci b/integration-test/.ghci new file mode 100644 index 000000000..443698c89 --- /dev/null +++ b/integration-test/.ghci @@ -0,0 +1,4 @@ +:set -XNoImplicitPrelude +:def loadFromIHP \file -> (System.Environment.getEnv "IHP_LIB") >>= (\ihpLib -> readFile (ihpLib <> "/" <> file)) +:loadFromIHP applicationGhciConfig +import IHP.Prelude diff --git a/integration-test/Main.hs b/integration-test/Main.hs new file mode 100644 index 000000000..1c28efbdf --- /dev/null +++ b/integration-test/Main.hs @@ -0,0 +1,16 @@ +module Main where + +import IHP.Prelude +import IHP.FrameworkConfig +import qualified IHP.Server +import IHP.Job.Types + +import Config +import Web.FrontController () +import Web.SlowLoad () + +instance Worker RootApplication where + workers _ = [] + +main :: IO () +main = IHP.Server.run config diff --git a/integration-test/Web/SlowLoad.hs b/integration-test/Web/SlowLoad.hs new file mode 100644 index 000000000..72988648a --- /dev/null +++ b/integration-test/Web/SlowLoad.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE TemplateHaskell #-} +module Web.SlowLoad where + +import Control.Concurrent (threadDelay) +import Language.Haskell.TH.Syntax (Dec, runIO) +import Prelude (Int, pure, (*)) + +$(do + -- Keep GHCi in the initial :l Main.hs load long enough for the SIGTERM + -- regression check to hit the orphaning window deterministically. + runIO (threadDelay (10 * (1000000 :: Int))) + pure ([] :: [Dec])) diff --git a/integration-test/run-devserver-sigterm-check.sh b/integration-test/run-devserver-sigterm-check.sh new file mode 100644 index 000000000..617ac9e8c --- /dev/null +++ b/integration-test/run-devserver-sigterm-check.sh @@ -0,0 +1,99 @@ +#!/usr/bin/env bash +set -euo pipefail + +app_dir="$(pwd)/integration-test" +log_file="${TMPDIR:-/tmp}/run-devserver.log" +devserver_pid="" +ghci_pid="" +ps_bin="${PS_BIN:-ps}" + +cleanup() { + if [ -n "${ghci_pid:-}" ] && kill -0 "$ghci_pid" 2>/dev/null; then + kill -KILL "$ghci_pid" 2>/dev/null || true + fi + + if [ -n "${devserver_pid:-}" ] && kill -0 "$devserver_pid" 2>/dev/null; then + kill -KILL "$devserver_pid" 2>/dev/null || true + fi + + if [ -n "${PGDATA:-}" ]; then + pg_ctl -D "$PGDATA" stop >/dev/null 2>&1 || true + fi +} +trap cleanup EXIT + +find_ghci_child() { + "$ps_bin" -axo pid=,ppid=,command= | awk -v ppid="$devserver_pid" ' + $2 == ppid && $0 ~ /--interactive/ { print $1; exit } + ' +} + +export HOME="${TMPDIR:-/tmp}/home" +mkdir -p "$HOME" + +export PGDATA="${TMPDIR:-/tmp}/pgdata" +export PGHOST="${TMPDIR:-/tmp}/pghost" +mkdir -p "$PGHOST" +initdb -D "$PGDATA" --no-locale --encoding=UTF8 +echo "unix_socket_directories = '$PGHOST'" >> "$PGDATA/postgresql.conf" +echo "listen_addresses = ''" >> "$PGDATA/postgresql.conf" +pg_ctl -D "$PGDATA" -l "${TMPDIR:-/tmp}/pg.log" start +createdb -h "$PGHOST" app + +export DATABASE_URL="postgresql:///app?host=$PGHOST" +export IHP_BROWSER=true + +cd "$app_dir" +"$RUN_DEVSERVER" >"$log_file" 2>&1 & +devserver_pid="$!" + +for _ in $(seq 1 200); do + ghci_pid="$(find_ghci_child || true)" + if [ -n "$ghci_pid" ]; then + break + fi + sleep 0.1 +done + +if [ -z "$ghci_pid" ]; then + echo "RunDevServer never spawned the GHCi child" >&2 + cat "$log_file" >&2 + exit 1 +fi + +sleep 1 + +if grep -Eq 'modules (loaded|reloaded)\.|Server started' "$log_file"; then + echo "RunDevServer reached steady state before SIGTERM; fixture is too fast" >&2 + cat "$log_file" >&2 + exit 1 +fi + +kill -TERM "$devserver_pid" + +for _ in $(seq 1 100); do + if ! kill -0 "$devserver_pid" 2>/dev/null; then + break + fi + sleep 0.1 +done + +if kill -0 "$devserver_pid" 2>/dev/null; then + echo "RunDevServer did not exit after SIGTERM" >&2 + cat "$log_file" >&2 + exit 1 +fi + +for _ in $(seq 1 20); do + if ! kill -0 "$ghci_pid" 2>/dev/null; then + break + fi + sleep 0.1 +done + +if kill -0 "$ghci_pid" 2>/dev/null; then + echo "Orphaned GHCi process survived RunDevServer SIGTERM" >&2 + "$ps_bin" -o pid=,ppid=,command= -p "$ghci_pid" >&2 || true + cat "$log_file" >&2 + exit 1 +fi