Developer-Dashboard

 view release on metacpan or  search on metacpan

Changes  view on Meta::CPAN

      the same extension-resolution path already used for Perl, Go, Java, and
      shell helpers
    - fixed the platform runnable-file resolver so logical command names can
      discover matching `.js` files on Unix and Windows without breaking the
      existing `package.json` dependency-install chain used by
      `dashboard skills install`
    - fixed the prompt tmux-status unit coverage to clear `WORKSPACE_REF`
      explicitly so ordinary tmux sessions still test the non-dashboard path

3.72  2026-05-14
    - fixed collector watchdog supervision so a managed loop that stays alive
      but stops making progress is treated as stalled, restarted
      automatically, and reported explicitly instead of sitting silent until
      an operator notices
    - renamed the primary tmux workflow from `dashboard ticket` to
      `dashboard workspace`, while keeping the older `dashboard ticket`
      spelling plus `TICKET_REF` session state as compatibility fallbacks
    - added layered plain-directory `.env` refresh for workspace sessions, so
      the highest ancestor `.env` acts as the base, parent `.env` files merge
      next, the current directory `.env` overrides last, and resumed tmux
      workspace sessions are refreshed in place with dropped keys unset

Changes  view on Meta::CPAN

    - removed the final stale API Dashboard POD example from the public
      dashboard entrypoint so the extracted API and SQL dashboards are now
      fully pruned from core code, docs, POD, tests, and shipped assets

3.20  2026-04-29
    - fixed bare dashboard skills install so a first successful install from
      the home root ddfile is reported as installed even when the skill ships
      no .env VERSION metadata, instead of showing unknown with blank version
      markers
    - fixed named collector stop summaries so dashboard stop collector NAME
      still reports the stopped collector row when the managed loop is alive
      but its process title has not become observable yet

3.19  2026-04-29
    - extracted optional browser workspaces out of the core distribution,
      leaving the core runtime seeded-page set and manuals focused on the
      features that still ship in Developer Dashboard itself
    - removed core seeded-page handling, runtime bootstrap wiring, release
      metadata assertions, and shipped browser tests for the extracted
      optional browser workspaces
    - moved the extracted page assets, dedicated tests, and supporting

README.md  view on Meta::CPAN

`allow_fast_dashboard_poll` on that collector, or set
`DEVELOPER_DASHBOARD_MIN_DASHBOARD_COMMAND_INTERVAL_SECONDS`, when the faster
cadence is intentional and understood.
- When a collector sets `disable => 1` or `"disable": true`, dashboard
will not start that collector, explicit named starts reject it, and any
already-running managed loop for that collector is stopped during the next
collector lifecycle action. Managed indicator state for that collector is
also removed instead of lingering as if it were still active.
- Stopping a singleton collector loop also terminates the long-running command
currently owned by that loop, so `dashboard stop collector foo` does not leave
the old worker command alive behind the stopped dispatcher.

Collector indicators follow the collector exit code automatically: `0`
stores an `ok` indicator state and any non-zero exit code stores `error`.
When `indicator.name` is omitted, the collector name is reused
automatically. When `indicator.label` is omitted, it defaults to that same
name. Configured collector indicators are seeded immediately, so prompt and
page status strips show them before the first collector run. Before a
collector has produced real output it appears as missing. Prompt output
renders an explicit status glyph in front of the collector icon, so
successful checks show fragments such as `✅🔑` while failing or not-yet-run

README.md  view on Meta::CPAN

dispatch contract during cross-platform tests or tarball installs.

The repository-only Windows verification assets follow the same layered
approach: fast forced-Windows unit coverage in `t/`, a real Strawberry Perl
host smoke in the source checkout, and a host-side rerun helper that delegates
to the QEMU launcher for release-grade Windows compatibility claims. The
supported baseline on Windows is PowerShell plus Strawberry Perl. Git Bash is
optional. Scoop is optional. They are setup helpers, not runtime requirements
for the installed `dashboard` command. In the Dockur-backed path, the launcher
stages the Strawberry Perl MSI from the Linux host into the OEM bundle and can
keep multiple retained Windows guests alive on configurable host web/RDP ports
while it reruns the same smoke.

## Browser Access Model

The browser security model follows the original local-first trust concept:

- requests from loopback with a loopback host, such as `127.0.0.1`, `::1`, or `localhost`, are treated as local admin
- requests from loopback with a hostname listed under `web.ssl_subject_alt_names` are also treated as local admin
- requests from non-loopback IPs are treated as helper access
- outsider requests return `401` without a login page until at least one helper user exists

doc/collector-stop.md  view on Meta::CPAN

# Stopping Collectors

`dashboard collector start <name>` starts two things: the collector **loop**
(the process titled `dashboard collector: <name>` that schedules and runs the
collector command) and a background **supervisor** (watchdog) that keeps the
collector alive by restarting the loop if it dies.

Because of that supervisor, stopping a collector is not just "kill the loop" —
the stop must also tell the supervisor to stop watching that collector, or the
supervisor immediately restarts it.

## The two stop commands (both supervisor-aware)

- `dashboard collector stop <name>` — the collector-subcommand form.
- `dashboard stop collector <name>` — the top-level runtime-control form.

doc/testing.md  view on Meta::CPAN

`Local::DockerGuard::reclaim_guard_containers` (`t/lib/Local/DockerGuard.pm`)
and removes every guard container its predecessors leaked. Reclaiming first also
releases the loopback ports those leaks were holding, which is what keeps the
guard's free-port search from turning intermittent.

A container is judged a leak when the pid embedded in its name no longer
resolves to a running process, or when it is older than one hour. The pid check
collects a fresh leak on the very next run; the age window exists only to
backstop pid recycling, where a dead run's pid has been reissued to an unrelated
live process that would otherwise vouch for the leak forever. A pid that exists
but cannot be signalled from this uid counts as alive, because the leaked
processes run as root and treating "permission denied" as "gone" would be
exactly backwards. Names that do not match the
`dd-smart-router-two-stage-<pid>-<epoch>` shape are never touched, so a sweep
can only ever remove a container this guard created, and the removal is verified
by re-reading the container inventory rather than by trusting `docker rm`'s exit
status — which is non-zero both when removal genuinely failed and when the
container had already gone.

`t/141-smart-router-guard-container-reclaim.t` pins those rules. It drives the
whole decision through an injected command runner and an injected pid probe, so

doc/windows-testing.md  view on Meta::CPAN

  returns near the configured timeout, the result reports `timed_out` with
  exit code 124 and preserves pre-deadline stdout, the blocker and its
  descendant are really gone from `Win32_Process`, the cached collector status
  reflects the timeout, and a healthy collector run afterwards proves the
  collector agent survived
- the harness also pins `DD_STATE_ROOT_USER`, because a non-interactive Windows
  session sets neither `USER` nor `LOGNAME` and the remaining username fallback
  is POSIX-only
- drive the whole gate from the Linux host with
  `integration/windows/run-dockur-collector-timeout-e2e.sh`, which checks the
  Dockur guest and its job agent are alive, installs the freshly built tarball
  in the guest, refuses to continue unless the installed
  `Developer::Dashboard::CollectorRunner` really carries the Windows
  command-timeout implementation, and then runs the harness and returns its
  guest exit code
- use this gate for any change touching collector timeout, signal, or
  process-subtree behavior on Windows

4. Full-system QEMU smoke

- run the one-command host helper `integration/windows/run-host-windows-smoke.sh`

integration/windows/run-collector-timeout-e2e.ps1  view on Meta::CPAN


$config = [ordered]@{
    collectors = @(
        [ordered]@{
            name    = "timeout.e2e"
            command = "& '$Perl' '$blockerPath' '$Marker' '$flagRoot'"
            timeout = $TimeoutSeconds
            cwd     = "home"
        },
        [ordered]@{
            name    = "alive.e2e"
            command = "Write-Output 'agent-alive-ok'"
            timeout = 60
            cwd     = "home"
        }
    )
}
$config | ConvertTo-Json -Depth 5 | Set-Content -Path (Join-Path $configRoot "config.json") -Encoding ASCII

Push-Location $projectRoot
try {
    Write-Host "==> trigger the blocking collector (timeout ${TimeoutSeconds}s, block 3600s)"

integration/windows/run-collector-timeout-e2e.ps1  view on Meta::CPAN


    # Prove the blocker really started and really spawned its descendant, so a
    # command that never ran cannot masquerade as a passing timeout.
    Assert-True -Condition (Test-Path (Join-Path $flagRoot "blocker.pid")) -Label "blocker recorded its pid before blocking"
    Assert-True -Condition (Test-Path (Join-Path $flagRoot "descendant.pid")) -Label "blocker spawned and recorded its asynchronous descendant"

    Write-Host "==> verify the whole marker-tagged command subtree is gone"
    Assert-MarkerProcessesGone -MarkerText $Marker -GraceSeconds $CleanupGraceSeconds
    foreach ($pidFile in @("blocker.pid", "descendant.pid")) {
        $recordedPid = [int](Get-Content -Path (Join-Path $flagRoot $pidFile))
        $alive = Get-Process -Id $recordedPid -ErrorAction SilentlyContinue
        Assert-True -Condition ($null -eq $alive) -Label "$pidFile process $recordedPid is no longer alive"
    }

    Write-Host "==> verify the cached collector status reflects the timeout"
    $statusOutput = Invoke-Dashboard -Dashboard $Dashboard -Arguments @("collector", "status", "timeout.e2e") -Label "dashboard collector status timeout.e2e"
    Write-Host $statusOutput
    $status = $statusOutput | ConvertFrom-Json
    Assert-True -Condition ($status.timed_out -eq 1) -Label "cached collector status reports timed_out=1"
    Assert-True -Condition ($status.last_exit_code -eq 124) -Label "cached collector status reports last_exit_code=124"
    Assert-True -Condition ($status.running -eq 0) -Label "cached collector status reports running=0 after the timeout"

    Write-Host "==> verify the collector agent survived: run a healthy collector after the timeout"
    $aliveOutput = Invoke-Dashboard -Dashboard $Dashboard -Arguments @("collector", "run", "alive.e2e") -Label "dashboard collector run alive.e2e"
    Write-Host $aliveOutput
    $aliveResult = $aliveOutput | ConvertFrom-Json
    Assert-True -Condition ($aliveResult.exit_code -eq 0) -Label "post-timeout healthy collector run exits 0"
    Assert-True -Condition ($aliveResult.timed_out -eq 0) -Label "post-timeout healthy collector run is not marked timed out"
    Assert-True -Condition ($aliveResult.stdout -match 'agent-alive-ok') -Label "post-timeout healthy collector run captured its output"
}
finally {
    Pop-Location
    foreach ($leftover in @( Get-ProcessesMatching -Fragment $Marker )) {
        Write-Host ("cleanup: stopping leftover pid {0}: {1}" -f $leftover.ProcessId, $leftover.CommandLine)
        Stop-Process -Id $leftover.ProcessId -Force -ErrorAction SilentlyContinue
    }
    if (-not $KeepTemp) {
        try {
            Remove-Item -Recurse -Force $tempRoot

integration/windows/run-dockur-collector-timeout-e2e.sh  view on Meta::CPAN

  # Purpose: confirm the Dockur QEMU Windows guest container is really running.
  # Input: DOCKUR_CONTAINER name and an available docker CLI.
  # Output: returns nothing, or exits non-zero when the guest is not up.
  command -v docker >/dev/null 2>&1 || fail "docker is required to reach the Dockur Windows guest"
  local running
  running="$(docker inspect -f '{{.State.Running}}' "$DOCKUR_CONTAINER" 2>/dev/null || true)"
  [[ "$running" == "true" ]] || fail "Dockur Windows guest container '$DOCKUR_CONTAINER' is not running"
  log "guest container $DOCKUR_CONTAINER is running"
}

require_agent_alive() {
  # Purpose: prove the in-guest job agent loop is still ticking before queueing work.
  # Input: the shared folder holding ddagent-alive.txt and AGENT_ALIVE_WAIT seconds.
  # Output: returns nothing, or exits non-zero when the tick does not advance.
  local alive="$DOCKUR_SHARE/ddagent-alive.txt"
  [[ -f "$alive" ]] || fail "in-guest job agent has never reported: $alive is missing (re-establish ddagent.ps1 in the guest)"
  local first second waited=0
  first="$(cat "$alive")"
  while (( waited < AGENT_ALIVE_WAIT )); do
    sleep 3
    waited=$(( waited + 3 ))
    second="$(cat "$alive")"
    if [[ "$second" != "$first" ]]; then
      log "in-guest job agent is alive: $second"
      return
    fi
  done
  fail "in-guest job agent tick did not advance within ${AGENT_ALIVE_WAIT}s (last: $first) — re-establish ddagent.ps1 in the guest"
}

resolve_tarball() {
  # Purpose: resolve the release tarball whose product the E2E must exercise.
  # Input: optional TARBALL env var, otherwise the newest tarball in the repo root.
  # Output: exports TARBALL as an absolute path.

integration/windows/run-dockur-collector-timeout-e2e.sh  view on Meta::CPAN

}

[[ -f "$HARNESS" ]] || fail "in-guest harness is missing: $HARNESS"
# The job namespace and timeout are interpolated into the generated guest script,
# so keep them to shapes that cannot terminate a quoted PowerShell literal.
[[ "$JOB_ID" =~ ^[A-Za-z0-9._-]+$ ]] || fail "JOB_ID must be limited to letters, digits, dot, underscore, and dash: $JOB_ID"
[[ "$TIMEOUT_SECONDS" =~ ^[0-9]+$ ]] || fail "TIMEOUT_SECONDS must be a whole number of seconds: $TIMEOUT_SECONDS"

resolve_share
require_guest
require_agent_alive
resolve_tarball
log "tarball: $TARBALL"
write_job
await_job

: <<'__END__'

=pod

=head1 NAME

integration/windows/run-strawberry-smoke.ps1  view on Meta::CPAN

    }
    if ($timeoutProof -notmatch 'elapsed_ms=(\d+)\s+exit_code=124\s+timed_out=1') {
        throw "Windows collector timeout proof was incomplete: $timeoutProof"
    }
    $timeoutElapsed = [int]$Matches[1]
    if ($timeoutElapsed -gt 10000) {
        throw "Windows collector timeout exceeded bounded cleanup grace: $timeoutElapsed ms"
    }
    $timeoutDescendants = @(Get-CimInstance Win32_Process | Where-Object { $_.CommandLine -like "*$timeoutMarker*" })
    if ($timeoutDescendants.Count -ne 0) {
        throw "Timed-out Windows collector left $($timeoutDescendants.Count) marked descendant process(es) alive"
    }
    Write-Host "Windows collector timeout $timeoutProof marker_processes=$($timeoutDescendants.Count)"

    Invoke-LoggedCommand -Label "dashboard auth add-user helper smoke-pass-123" -Command @($Dashboard, "auth", "add-user", "helper", "smoke-pass-123")

    Write-PhaseStatus -Phase "start-dashboard-server"
    $serve = Start-Process -FilePath $Dashboard -ArgumentList @("serve", "--host", "127.0.0.1", "--port", $Port) -PassThru -NoNewWindow
    try {
        Write-PhaseStatus -Phase "wait-for-http"
        Wait-HttpOk -Url "http://127.0.0.1:$Port/"

lib/Developer/Dashboard.pm  view on Meta::CPAN

When a collector sets C<disable =E<gt> 1> or C<"disable": true>, dashboard
will not start that collector, explicit named starts reject it, and any
already-running managed loop for that collector is stopped during the next
collector lifecycle action. Managed indicator state for that collector is
also removed instead of lingering as if it were still active.

=item *

Stopping a singleton collector loop also terminates the long-running command
currently owned by that loop, so C<dashboard stop collector foo> does not leave
the old worker command alive behind the stopped dispatcher.

=back

Collector indicators follow the collector exit code automatically: C<0>
stores an C<ok> indicator state and any non-zero exit code stores C<error>.
When C<indicator.name> is omitted, the collector name is reused
automatically. When C<indicator.label> is omitted, it defaults to that same
name. Configured collector indicators are seeded immediately, so prompt and
page status strips show them before the first collector run. Before a
collector has produced real output it appears as missing. Prompt output

lib/Developer/Dashboard.pm  view on Meta::CPAN

dispatch contract during cross-platform tests or tarball installs.

The repository-only Windows verification assets follow the same layered
approach: fast forced-Windows unit coverage in C<t/>, a real Strawberry Perl
host smoke in the source checkout, and a host-side rerun helper that delegates
to the QEMU launcher for release-grade Windows compatibility claims. The
supported baseline on Windows is PowerShell plus Strawberry Perl. Git Bash is
optional. Scoop is optional. They are setup helpers, not runtime requirements
for the installed C<dashboard> command. In the Dockur-backed path, the launcher
stages the Strawberry Perl MSI from the Linux host into the OEM bundle and can
keep multiple retained Windows guests alive on configurable host web/RDP ports
while it reruns the same smoke.

=head2 Browser Access Model

The browser security model follows the original local-first trust concept:

=over 4

=item *

lib/Developer/Dashboard/ActionRunner.pm  view on Meta::CPAN

        system 'ps', '-o', 'stat=', '-p', $pid;
        return $? >> 8;
    };
    return if $exit_code != 0;
    $stdout =~ s/^\s+|\s+$//g if defined $stdout;
    return if !defined $stdout || $stdout eq '';
    return substr( $stdout, 0, 1 );
}

# _pid_is_running($pid)
# Determines whether one background action pid is still alive after
# opportunistic reaping and zombie-state checks.
# Input: process id integer.
# Output: boolean true when the process still appears to be running.
sub _pid_is_running {
    my ( $self, $pid ) = @_;
    return 0 if !defined $pid || $pid !~ /^\d+$/ || $pid < 1;
    return 0 if $self->_reap_child_process($pid);
    return 0 if ( $self->_read_process_state($pid) || '' ) eq 'Z';
    return kill( 0, $pid ) ? 1 : 0;
}

lib/Developer/Dashboard/CLI/Progress.pm  view on Meta::CPAN

Construct and drive one progress board.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module turns runtime lifecycle events into a visible task list on the terminal so restart and stop do not leave the user staring at a blank prompt while the runtime waits through managed stability windows.

=head1 WHY IT EXISTS

It exists because the restart and stop flows intentionally wait for collectors and the web service to prove they stayed alive or shut down cleanly. Without a dedicated renderer that delay looks like a hang even when the runtime is behaving exactly as...

=head1 WHEN TO USE

Use this file when changing the terminal progress UX for lifecycle commands, when adding new restart or stop tasks that need to appear in the task list, or when adjusting how task boards redraw in interactive shells versus captured non-interactive ru...

=head1 HOW TO USE

Construct the object with a title and the full ordered task list before work begins, then pass the callback into the runtime lifecycle method. The runtime reports task-id and status updates, and this renderer prints the current state of the whole boa...

=head1 WHAT USES IT

lib/Developer/Dashboard/CollectorRunner.pm  view on Meta::CPAN

        next if !$self->_pid_is_running($pid);
        kill 15, -$pid if !is_windows();
        kill 15, $pid;
    }
    for my $pid ( keys %{$active_workers} ) {
        for ( 1 .. 20 ) {
            last if !$self->_pid_is_running($pid);
            sleep 0.1;
        }
        # Send the group SIGKILL unconditionally: a command child that ignores
        # SIGTERM can still be alive in the worker's process group even after
        # the worker (group leader) has exited, and a running leader is only
        # signalled directly when it is still alive. kill on an empty group is a
        # harmless no-op.
        kill 9, -$pid if !is_windows();
        kill 9, $pid if $self->_pid_is_running($pid);
        $self->_reap_child_process($pid);
        delete $active_workers->{$pid};
    }
    return 1;
}

# _active_worker_pids($active_workers)

lib/Developer/Dashboard/CollectorRunner.pm  view on Meta::CPAN

# _process_exists($pid)
# Checks whether the current process can still signal one process id.
# Input: process id integer.
# Output: boolean true when signal 0 succeeds.
sub _process_exists {
    my ( $self, $pid ) = @_;
    return kill( 0, $pid ) ? 1 : 0;
}

# _pid_is_running($pid)
# Determines whether one collector loop pid is still alive after opportunistic
# child reaping.
# Input: process id integer.
# Output: boolean true when the pid is still running.
sub _pid_is_running {
    my ( $self, $pid ) = @_;
    return 0 if !defined $pid || $pid !~ /^\d+$/ || $pid < 1;
    return 0 if $self->_reap_child_process($pid);
    return 0 if ( $self->_read_process_state($pid) || '' ) eq 'Z';
    return $self->_process_exists($pid) ? 1 : 0;
}

lib/Developer/Dashboard/PageDocument.pm  view on Meta::CPAN

  if (!window.__dashboardAjaxSingletons) window.__dashboardAjaxSingletons = {};
  if (window.__dashboardAjaxSingletons[name]) return;
  window.__dashboardAjaxSingletons[name] = true;
  window.addEventListener('pagehide', function() {
    let url = '/ajax/singleton/stop?singleton=' + encodeURIComponent(name);
    if (navigator.sendBeacon) {
      navigator.sendBeacon(url, '');
      return;
    }
    if (window.fetch) {
      fetch(url, { method: 'POST', keepalive: true, credentials: 'same-origin' }).catch(function () {});
    }
  });
}
function dashboard_target_nodes(target) {
  if (!target) return [];
  if (typeof target === 'string') return Array.prototype.slice.call(document.querySelectorAll(target));
  if (target instanceof Element) return [target];
  if (target.length && typeof target !== 'string') return Array.prototype.slice.call(target);
  return [];
}

lib/Developer/Dashboard/PageRuntime.pm  view on Meta::CPAN


# _await_saved_ajax_exit($pid, $process_group, $status_ref)
# Waits out the SIGTERM grace window after a saved-Ajax worker was signalled,
# reaping the worker the moment it exits so its own TERM handler is never cut
# short and the caller does not block on a second wait. Elapsed wall-clock time
# bounds the wait, so the escalation is deterministic instead of depending on a
# fixed number of poll iterations.
# Input: worker pid, owned POSIX process-group id or undef for direct-pid
# cleanup, and an optional scalar reference that receives the reaped wait status.
# Output: true when the worker and any owned group went away inside the window,
# false when the window expired with something still alive.
sub _await_saved_ajax_exit {
    my ( $self, $pid, $process_group, $status_ref ) = @_;
    my $deadline = Time::HiRes::time() + $SAVED_AJAX_TERM_GRACE_SECONDS;
    my $reaped   = 0;
    while (1) {
        if ( !$reaped ) {
            my ( $exited, $status ) = $self->_saved_ajax_child_exited($pid);
            if ($exited) {
                $reaped = 1;
                ${$status_ref} = $status if ref($status_ref) eq 'SCALAR';
            }
        }

        # A reaped worker can still have live descendants in the group it led,
        # and those descendants are exactly what the grace window exists for.
        my $worker_gone = $reaped || !kill 0, $pid;
        my $group_alive = defined $process_group && kill 0, -$process_group;
        return 1 if $worker_gone && !$group_alive;
        return 0 if Time::HiRes::time() >= $deadline;
        Time::HiRes::sleep($SAVED_AJAX_TERM_POLL_SECONDS);
    }
}

# _looks_like_stream_disconnect_error($error)
# Detects writer failures that mean the browser stream was closed and the worker should just be stopped.
# Input: raw exception text from one writer callback.
# Output: boolean true when the error matches a disconnect or closed-stream condition.
sub _looks_like_stream_disconnect_error {

lib/Developer/Dashboard/RuntimeManager.pm  view on Meta::CPAN

            task_id => 'stop_web',
            status  => 'done',
            label   => 'Stop dashboard web service',
        }
    );
    return _numeric_pid($pid);
}

# _wait_for_unix_web_shutdown(%args)
# Checks whether Unix web shutdown still has managed web, ajax worker, legacy
# serve, or listener processes alive before escalation to KILL.
# Input: optional pid integer plus array references for listener_pids,
# ajax_pids, and legacy_pids.
# Output: boolean true when shutdown work is still pending.
sub _wait_for_unix_web_shutdown {
    my ( $self, %args ) = @_;
    my $pid = $args{pid};
    my @listener_pids = @{ $args{listener_pids} || [] };
    my @ajax_pids     = @{ $args{ajax_pids}     || [] };
    my @legacy_pids   = @{ $args{legacy_pids}   || [] };

lib/Developer/Dashboard/RuntimeManager.pm  view on Meta::CPAN

# Input: process id integer.
# Output: boolean true when waitpid reaped the child.
sub _reap_child_process {
    my ( $self, $pid ) = @_;
    return 0 if !defined $pid || $pid !~ /^\d+$/ || $pid < 1;
    my $waited = waitpid( $pid, 1 );
    return $waited == $pid ? 1 : 0;
}

# _pid_is_running($pid)
# Determines whether one runtime-managed pid is still alive after opportunistic
# child reaping.
# Input: process id integer.
# Output: boolean true when the process is still running.
sub _pid_is_running {
    my ( $self, $pid ) = @_;
    return 0 if !defined $pid || $pid !~ /^\d+$/ || $pid < 1;
    return 0 if $self->_reap_child_process($pid);
    return 0 if ( $self->_read_process_state($pid) || '' ) eq 'Z';
    return $self->_process_exists($pid) ? 1 : 0;
}

lib/Developer/Dashboard/RuntimeManager.pm  view on Meta::CPAN

        $count++;
    }
    return $count;
}

# _wait_for_windows_web_shutdown($pid, $port, $listener_pids)
# Checks whether the Windows-managed web process and its listener port have
# both gone away after shutdown signals were sent.
# Input: optional saved pid, optional listen port, and array reference of
# listener pids discovered from persisted state.
# Output: boolean true while the web runtime still appears alive.
sub _wait_for_windows_web_shutdown {
    my ( $self, $pid, $port, $listener_pids ) = @_;
    my @listener_pids = ref($listener_pids) eq 'ARRAY' ? @{$listener_pids} : ();
    return 1 if $pid && kill 0, $pid;
    return 1 if grep { kill 0, $_ } @listener_pids;
    return 1 if $port && scalar $self->_listener_pids_for_port($port);
    return 0;
}

# start_collectors()

lib/Developer/Dashboard/RuntimeManager.pm  view on Meta::CPAN

            }
        );
        $self->_log_collector_watchdog_event( $name, "Watchdog restarted collector '$name' (attempt $restart_count)" );
        push @{ $result{restarted} }, { name => $name, pid => $pid };
    }

    return \%result;
}

# _collector_stalled_for_watchdog($job, $status)
# Detects when a managed scheduled collector loop is alive but has stopped
# making progress long enough that the watchdog should recycle it.
# Input: collector job hash reference and collector status hash reference.
# Output: boolean true when the collector is stalled.
sub _collector_stalled_for_watchdog {
    my ( $self, $job, $status ) = @_;
    return 0 if ref($job) ne 'HASH';
    return 0 if ref($status) ne 'HASH';
    my $latest_epoch = $self->_collector_watchdog_last_progress_epoch($status);
    return 0 if !$latest_epoch;
    my $stale_after = $self->_collector_watchdog_stale_seconds($job);

lib/Developer/Dashboard/RuntimeManager.pm  view on Meta::CPAN

    return 0 if !is_windows();
    my $listener_port = 0;
    $listener_port = $port if $port;
    $listener_port = $running->{port} if !$listener_port && $running->{port};
    return 0 if !$listener_port;
    return 0 if ( $running->{port} || 0 ) != $listener_port;
    return 1;
}

# _collector_runtime_ready($name, $pid)
# Confirms that a newly started collector loop became visible and stayed alive
# long enough to catch an immediate post-ready crash.
# Input: collector name string and process id integer.
# Output: boolean true when the collector loop became visible and survived the
# short confirmation window afterwards.
sub _collector_runtime_ready {
    my ( $self, $name, $pid ) = @_;
    return 0 if !defined $name || $name eq '';
    return 0 if !defined $pid || $pid !~ /^\d+$/ || $pid < 1;
    my $ready_polls = 0;
    for ( 1 .. $self->_runtime_stability_polls ) {

lib/Developer/Dashboard/Zipper.pm  view on Meta::CPAN

=head2 zip, unzip, acmdx, Ajax, __cmdx, _cmdx, _cmdp

Encode and decode token payloads and generate older-style ajax links. Saved
bookmark Ajax file handlers are stored under the dashboards ajax tree as
executable files so the web runtime can run them as real processes.

=for comment FULL-POD-DOC START

=head1 PURPOSE

This module keeps the older bookmark and Ajax helper compatibility surface alive. It builds tokenised URLs, saved Ajax endpoints, and helper snippets such as C<Ajax()> while routing the actual encoding work through the modern codec module.

=head1 WHY IT EXISTS

It exists because older bookmarks still expect the historical helper names and URL-building patterns. Keeping those wrappers in one module preserves compatibility without forcing newer runtime code to keep re-implementing the old API directly.

=head1 WHEN TO USE

Use this file when changing older Ajax helper behavior, saved Ajax file validation, token URL generation, or the compatibility wrappers that older bookmark instructions still reference.

=head1 HOW TO USE

t/07-core-units.t  view on Meta::CPAN

    my $error = '';
    {
        no warnings 'redefine';
        local *Developer::Dashboard::CollectorRunner::_reap_child_process = sub { return 0 };
        local *Developer::Dashboard::CollectorRunner::_same_pid_namespace = sub { return 1 };
        local *Developer::Dashboard::CollectorRunner::_is_managed_loop = sub { return 1 };
        local *Developer::Dashboard::CollectorRunner::_pid_is_running = sub { return 1 };
        local *Developer::Dashboard::CollectorRunner::sleep = sub { return 0 };
        eval { $runner->stop_loop('stubborn-state'); 1 } or $error = $@;
    }
    like( $error, qr/Collector 'stubborn-state' did not stop after TERM and KILL/, 'stop_loop fails explicitly when a managed collector still appears alive after TERM and KILL' );
    ok( -f $pidfile, 'stop_loop keeps the pidfile when a managed collector refuses to stop' );
    ok( defined $runner->loop_state('stubborn-state'), 'stop_loop keeps loop state metadata when a managed collector refuses to stop' );
    $runner->_cleanup_loop_files('stubborn-state');
}

{
    my $pidfile = File::Spec->catfile( $paths->collectors_root, 'worker-shutdown.pid' );
    open my $worker_pid, '>', $pidfile or die $!;
    print {$worker_pid} "535353\n";
    close $worker_pid;

t/09-runtime-manager.t  view on Meta::CPAN

        return {
            pid    => $$,
            name   => 'alpha.collector',
            status => 'starting',
        };
    };
    ok(
        $manager->_collector_runtime_ready( 'alpha.collector', $$ ),
        '_collector_runtime_ready falls back to the persisted loop state while the managed process title is not observable yet',
    );
    is( $polls, 0, '_collector_runtime_ready trusts the persisted loop-state fallback without consulting running_loops when the pid is already proven alive' );
}

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::sleep = sub { return 0 };
    local *Local::RuntimeRunner::running_loops = sub {
        die "running_loops should not be consulted while the persisted loop-state fallback already proves the pid is alive\n";
    };
    local *Local::RuntimeRunner::loop_state = sub {
        return {
            pid    => $$,
            name   => 'alpha.collector',
            status => 'starting',
        };
    };
    ok(
        $manager->_collector_runtime_ready( 'alpha.collector', $$ ),

t/09-runtime-manager.t  view on Meta::CPAN

    my $calls = 0;
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::running_web = sub {
        return $calls++ == 0 ? { pid => $listener, port => 7908 } : undef;
    };
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return ($listener) };
    local *Developer::Dashboard::RuntimeManager::_find_legacy_web_processes = sub { return () };
    local *Developer::Dashboard::RuntimeManager::_pkill_perl = sub { return 1 };
    is( $manager->stop_web, $listener, 'stop_web returns the recorded pid while it also tracks listener pids on the bound port' );
    waitpid( $listener, 0 );
    ok( !kill( 0, $listener ), 'stop_web escalates listener-port pids to KILL when they remain alive after TERM' );
}

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::web_state = sub {
        return {
            pid    => 111_111,
            host   => '127.0.0.1',
            port   => 7917,
            status => 'running',

t/09-runtime-manager.t  view on Meta::CPAN

{
    local $Developer::Dashboard::Platform::OS_NAME = 'MSWin32';
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub {
        my ( undef, $port ) = @_;
        return () if $port != 7890;
        return (7123);
    };
    ok(
        $manager->_wait_for_windows_web_shutdown( undef, undef, [$$] ),
        '_wait_for_windows_web_shutdown reports the web runtime alive when a tracked listener pid is still running',
    );
    ok(
        $manager->_wait_for_windows_web_shutdown( undef, 7890, [] ),
        '_wait_for_windows_web_shutdown reports the web runtime alive while the listen port still has an owning pid',
    );
    ok(
        !$manager->_wait_for_windows_web_shutdown( undef, 7891, [] ),
        '_wait_for_windows_web_shutdown reports shutdown complete when there is no saved pid, listener pid, or live port owner',
    );
}

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::stop_web = sub {

t/100-runtimemanager-coverage.t  view on Meta::CPAN

    my $reaped = 0;
    for ( 1 .. 40 ) {
        $reaped = $manager->_reap_any_child_processes;
        last if $reaped;
        select undef, undef, undef, 0.05;
    }
    is( $reaped, 1, '_reap_any_child_processes reaps one exited direct child then stops on -1' );
}

# --- _wait_for_windows_web_shutdown -----------------------------------------
ok( $manager->_wait_for_windows_web_shutdown( $$, undef, undef ),   '_wait_for_windows_web_shutdown true when the saved pid is alive' );
ok( !$manager->_wait_for_windows_web_shutdown( 999999, undef, [] ), '_wait_for_windows_web_shutdown false when nothing is alive' );
ok( $manager->_wait_for_windows_web_shutdown( undef, undef, [$$] ), '_wait_for_windows_web_shutdown true when a listener pid is alive' );
{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return (4321) };
    ok( $manager->_wait_for_windows_web_shutdown( undef, 7890, [] ), '_wait_for_windows_web_shutdown true when the port still has listeners' );
}
{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_listener_pids_for_port = sub { return () };
    ok( !$manager->_wait_for_windows_web_shutdown( undef, 7890, [] ),  '_wait_for_windows_web_shutdown false when the port has no listeners' );
    ok( !$manager->_wait_for_windows_web_shutdown( undef, undef, [] ), '_wait_for_windows_web_shutdown false when no port is supplied' );

t/100-runtimemanager-coverage.t  view on Meta::CPAN

    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_same_pid_namespace = sub { return 0 };
    is( $manager->_ensure_collector_pid_stopped( 'c', 123456 ), 1, '_ensure_collector_pid_stopped ignores a foreign-namespace pid' );
}
{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_same_pid_namespace = sub { return 1 };
    local *Developer::Dashboard::RuntimeManager::_send_signal        = sub { return 1 };
    local *Developer::Dashboard::RuntimeManager::_reap_child_process = sub { return 1 };

    my @seq = ( 1, 0 );    # alive at first check, then gone during the TERM loop
    local *Developer::Dashboard::RuntimeManager::_pid_is_running = sub { return @seq ? shift(@seq) : 0 };
    is( $manager->_ensure_collector_pid_stopped( 'c', 123456 ), 1, '_ensure_collector_pid_stopped returns after the collector dies on TERM' );

    @seq = ( (1) x 21, 0 );    # survives TERM loop, then dies after KILL
    is( $manager->_ensure_collector_pid_stopped( 'c', 123456 ), 1, '_ensure_collector_pid_stopped escalates to KILL when TERM is not enough' );

    local *Developer::Dashboard::RuntimeManager::_pid_is_running = sub { return 1 };
    my $err = eval { $manager->_ensure_collector_pid_stopped( 'c', 123456 ); 1 } ? '' : $@;
    like( $err, qr/Collector 'c' did not stop after TERM and KILL/, '_ensure_collector_pid_stopped dies when the collector never stops' );
}

t/100-runtimemanager-coverage.t  view on Meta::CPAN

{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_pid_is_running = sub { return 0 };
    is( $manager->_wait_for_unix_web_shutdown(), 0, '_wait_for_unix_web_shutdown false when nothing is pending' );
    is( $manager->_wait_for_unix_web_shutdown( pid => 'x' ), 0, '_wait_for_unix_web_shutdown false for a non-numeric pid' );
    is( $manager->_wait_for_unix_web_shutdown( pid => 0 ),   0, '_wait_for_unix_web_shutdown false for a pid below one' );
}
{
    no warnings 'redefine';
    local *Developer::Dashboard::RuntimeManager::_pid_is_running = sub { return 1 };
    is( $manager->_wait_for_unix_web_shutdown( pid => $$ ), 1, '_wait_for_unix_web_shutdown true while the wrapper pid is alive' );
    is( $manager->_wait_for_unix_web_shutdown( ajax_pids => [ undef, $$ ] ),   1, '_wait_for_unix_web_shutdown true while an ajax worker is alive' );
    is( $manager->_wait_for_unix_web_shutdown( legacy_pids => [ undef, $$ ] ), 1, '_wait_for_unix_web_shutdown true while a legacy web pid is alive' );
}
is( $manager->_wait_for_unix_web_shutdown( listener_pids => [ undef, 'x', $$ ] ), 1, '_wait_for_unix_web_shutdown true while a listener pid still answers signals' );
is( $manager->_wait_for_unix_web_shutdown( listener_pids => [ 999999 ] ),         0, '_wait_for_unix_web_shutdown false when a listener pid is gone' );

# --- _managed_ajax_processes ------------------------------------------------
{
    no warnings 'redefine';
    my $root = $paths->state_root;
    local *Developer::Dashboard::RuntimeManager::_find_processes_by_prefix = sub {
        return (

t/103-collectorrunner-coverage.t  view on Meta::CPAN

            last;
        }
        select undef, undef, undef, 0.01;
    }

    # A bare "got undef" says nothing about which of the two very different
    # failures happened: the child never reached its exec, or its environ could
    # not be read. Note that undef never means "the environ was empty" -- a
    # genuinely empty /proc/<pid>/environ reads back as '' with length 0.
    if ( !defined $probe_environ ) {
        my $alive = -e "/proc/$child" ? 'alive' : 'gone';
        my $state = eval {
            open my $st, '<', "/proc/$child/stat" or die;
            my $line = <$st>;
            close $st;
            ( split / /, $line )[2];
        } // '?';
        diag("probe gave up after $probe_polls polls; child is $alive in state $state and never exposed its exec'd argv[0]");
    }
    # Pin the precondition the coverage below depends on. Without this the
    # marker assertion is satisfied by any environ that lacks the key, so the
    # empty-environ leg can stop being exercised without a single test failing.
    is( $probe_environ, '', 'the probe child exposes a readable zero-length environ' );
    is( $runner->_read_process_env_marker( $child, 'ANY' ), undef, '_read_process_env_marker returns undef for an empty environ' );
    kill 9, $child;
    waitpid( $child, 0 );
}

t/103-collectorrunner-coverage.t  view on Meta::CPAN

    if ( !$child ) { POSIX::_exit(0); }
    my $pidfile = $runner->_pidfile($name);
    open my $fh, '>', $pidfile or die $!;
    print {$fh} $child;
    close $fh;
    $runner->_write_loop_state( $name, { pid => $child, name => $name, process_name => $runner->_process_title($name), status => 'running' } );
    select undef, undef, undef, 0.1;
    is( $runner->stop_loop($name), $child, 'stop_loop returns the pid of an already-exited loop it reaps' );
}

# Foreign-namespace loop: pid is alive but reported in another namespace.
{
    my $name  = 'stop.foreign';
    my $child = fork();
    die "fork failed: $!" if !defined $child;
    if ( !$child ) { $SIG{TERM} = 'DEFAULT'; select undef, undef, undef, 30; POSIX::_exit(0); }
    my $pidfile = $runner->_pidfile($name);
    open my $fh, '>', $pidfile or die $!;
    print {$fh} $child;
    close $fh;
    {

t/106-runtimemanager-coverage-2.t  view on Meta::CPAN

    is_deeply(
        [ map { $_->{pid} } $manager->_managed_ajax_processes ],
        [102],
        '_managed_ajax_processes keeps markerless workers when procfs is unavailable'
    );
}

# ---------------------------------------------------------------------------
# _collector_supervisor_running: exercise every short-circuit stage of the
# five-term guard on source line 1634, including the previously-missing
# "alive but a different pid namespace" arm.
# ---------------------------------------------------------------------------
{
    no warnings 'redefine';
    my ( $ns, $super ) = ( 1, 1 );
    local *Developer::Dashboard::RuntimeManager::_same_pid_namespace   = sub { return $ns };
    local *Developer::Dashboard::RuntimeManager::_is_collector_supervisor = sub { return $super };

    my $pidfile = $manager->_collector_supervisor_pidfile;
    my $plant   = sub {
        my ($content) = @_;

t/106-runtimemanager-coverage-2.t  view on Meta::CPAN


=head1 NAME

t/106-runtimemanager-coverage-2.t - branch and condition closure for the runtime lifecycle manager

=head1 PURPOSE

This test drives the residual branch and condition arms of
C<Developer::Dashboard::RuntimeManager> that the rest of the suite never
reaches: the empty-runtime-root arms of the managed Ajax worker filter, the
alive-but-foreign-namespace arm of the collector supervisor discovery guard,
the environment-tuning fallbacks, the Windows PowerShell listener parser, the
process-state and process-environ readers, and the Windows background
state-assembly path. It exists so those arms are exercised by a real,
observable behavior rather than an annotation.

=head1 WHY IT EXISTS

The all-metric coverage gate requires C<lib/> to report 100.0 on statement,
subroutine, branch, AND condition. The runtime manager accumulated a handful of
short-circuit arms and environ/stat edge cases that only occur against live

t/109-saved-ajax-process-tree.t  view on Meta::CPAN

            $run_error = $@ || 'unknown stream error';
        };
        alarm 0;
    }
    $grandchild_pid ||= _read_pid_marker($marker);

    # Feature: Saved-Ajax cancellation owns the complete POSIX process tree.
    # Scenario: Browser disconnects after a saved-Ajax worker forks a descendant.
    # Given the worker and TERM-ignoring grandchild are both running,
    ok( defined $worker_pid && $worker_pid > 0, 'BDD Given: saved-Ajax worker reports its pid before disconnect' );
    ok( $grandchild_was_live, 'BDD Given: saved-Ajax grandchild is alive before disconnect cleanup' );
    # When the response writer reports the browser disconnect,
    is( $run_error, '', 'BDD When: disconnect cleanup returns without an internal runtime error' );
    ok( $stream_result && $stream_result->{disconnected}, 'ATDD: saved-Ajax result records the writer disconnect' );
    # Then no runnable descendant remains after the bounded cleanup.
    ok(
        defined $grandchild_pid && _wait_until( sub { !_process_is_live($grandchild_pid) }, 2 ),
        'ATDD Then: disconnect cleanup terminates the saved-Ajax grandchild process',
    );

    if ( defined $grandchild_pid && _process_is_live($grandchild_pid) ) {

t/110-saved-ajax-group-coverage.t  view on Meta::CPAN

    is( $ready, 'up', 'group fixture reports the leader ready after its grandchild started' );

    my $grandchild_pid;
    if ( open my $marker_fh, '<', $marker ) {
        my $line = <$marker_fh> // '';
        close $marker_fh;
        $line =~ s/\s+//g;
        $grandchild_pid = $line =~ /^\d+$/ ? $line + 0 : undef;
    }
    push @cleanup_pids, $grandchild_pid;
    ok( defined $grandchild_pid && kill( 0, $grandchild_pid ), 'group fixture grandchild is alive before termination' );

    is( $runtime->_terminate_saved_ajax_process( $leader, $leader ), 1, '_terminate_saved_ajax_process terminates a live owned process group' );
    waitpid( $leader, 0 );
    ok( !kill( 0, $leader ), '_terminate_saved_ajax_process leaves the group leader dead' );

    my $grandchild_gone = 0;
    for ( 1 .. 100 ) {
        if ( !kill 0, $grandchild_pid ) { $grandchild_gone = 1; last; }
        select undef, undef, undef, 0.05;
    }

t/110-saved-ajax-group-coverage.t  view on Meta::CPAN

        syswrite $ready_w, 'up';
        select undef, undef, undef, 30;
        POSIX::_exit(0);
    }
    push @cleanup_pids, $leader;
    close $ready_w;
    is( _await_ready($ready_r), 'up', 'descendant fixture reports the leader ready after its descendant started' );

    my $descendant_pid = _read_marker_pid($descendant_marker);
    push @cleanup_pids, $descendant_pid;
    ok( defined $descendant_pid && kill( 0, $descendant_pid ), 'descendant fixture process is alive before termination' );

    is( $runtime->_terminate_saved_ajax_process( $leader, $leader ), 1, '_terminate_saved_ajax_process completes when only a descendant is still shutting down' );
    ok( -e $cleanup_marker, '_terminate_saved_ajax_process holds the SIGKILL escalation while an owned descendant is still cleaning up' );

    my $descendant_gone = 0;
    for ( 1 .. 100 ) {
        if ( !kill 0, $descendant_pid ) { $descendant_gone = 1; last; }
        select undef, undef, undef, 0.05;
    }
    ok( $descendant_gone, '_terminate_saved_ajax_process leaves no descendant behind once the group drains' );

t/110-saved-ajax-group-coverage.t  view on Meta::CPAN

        syswrite $ready_w, 'up';
        select undef, undef, undef, 30;
        POSIX::_exit(0);
    }
    push @cleanup_pids, $leader;
    close $ready_w;
    is( _await_ready($ready_r), 'up', 'TERM-ignoring fixture reports the leader ready after its descendant started' );

    my $descendant_pid = _read_marker_pid($marker);
    push @cleanup_pids, $descendant_pid;
    ok( defined $descendant_pid && kill( 0, $descendant_pid ), 'TERM-ignoring descendant is alive before termination' );

    my $reaped_status = 'untouched';
    my $started       = Time::HiRes::time();
    is( $runtime->_terminate_saved_ajax_process( $leader, $leader, \$reaped_status ), 1, '_terminate_saved_ajax_process escalates for a TERM-ignoring owned group' );
    my $elapsed = Time::HiRes::time() - $started;

    cmp_ok( $elapsed, '>=', 0.4, '_terminate_saved_ajax_process waits the whole configured window before the SIGKILL escalation' );
    is( $reaped_status, 'untouched', '_terminate_saved_ajax_process leaves the status untouched when the worker had to be SIGKILLed' );
    waitpid( $leader, 0 );
    ok( !kill( 0, $leader ), '_terminate_saved_ajax_process leaves the TERM-ignoring group leader dead' );

t/121-actionrunner-coverage-2.t  view on Meta::CPAN

    for ( 1 .. $supervisor_wait_loops ) {
        my $reaped = waitpid( $pid, WNOHANG );
        return $? if $reaped == $pid;
        select undef, undef, undef, 0.05;
    }
    return;
}

# ---------------------------------------------------------------------------
# run_command_action(): the deadline escalation must force SIGKILL when the
# command child is still alive after the SIGTERM grace window
# (line 208, `waitpid(...) == 0` true side).
#
# Determinism matters here, because the naive "trap TERM and sleep" recipe races
# the interpreter/shell startup: core time() only has one-second granularity, so
# a deadline of `time() + timeout_ms/1000` can elapse in the very next loop turn
# when the supervisor happens to start just before a second boundary. If SIGTERM
# lands before the command child has installed its handler, the child dies, the
# escalation waitpid reaps it, and the forced-KILL side is never taken.
#
# This case removes that race in two ways. First, timeout_ms is 1500, so with
# integer time() the deadline can only fire once time() has advanced twice --
# i.e. at least a full second of wall clock after the fork, far longer than the
# command child needs to boot. Second, the command child is a real script whose
# TERM handler writes a marker file and then keeps running, so the assertions can
# prove from the outside that SIGTERM was delivered *and* survived: the marker
# only exists if the child was still alive when the escalation waitpid ran, which
# is exactly the branch under test.
#
# The command string leads with the shell's `exec` builtin on purpose. /bin/sh is
# dash on Debian-family hosts and it does not always collapse a single `-c`
# command into an exec, so without it the shell would stay between the supervisor
# and the script: SIGTERM would kill the shell instead of the script, and the
# script would be orphaned rather than signalled.
# ---------------------------------------------------------------------------
{
    my $marker = File::Spec->catfile( $home, 'term-was-absorbed.marker' );
    my $script = File::Spec->catfile( $home, 'ignore-term.pl' );
    open my $script_fh, '>', $script or die "Unable to write $script: $!";
    print {$script_fh} <<'CHILD';
#!/usr/bin/env perl
use strict;
use warnings;

# The supervisor's terminate window must find this process alive, so record the
# delivered SIGTERM and deliberately keep running until SIGKILL arrives.
my $marker = $ENV{DD_TEST_TERM_MARKER};
$SIG{TERM} = sub {
    open my $fh, '>', $marker or return;
    print {$fh} "termed\n";
    close $fh;
    return;
};
sleep 5 while 1;
CHILD

t/121-actionrunner-coverage-2.t  view on Meta::CPAN

    );

    ok( $result->{background}, 'background command action reports itself as backgrounded' );
    ok( $result->{pid} > 0, 'background command action returns a supervisor pid' );
    like( $result->{started_at}, qr/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}Z$/, 'background command action stamps an ISO-8601 start time' );

    my $raw_status = reap_supervisor( $result->{pid} );
    ok( defined $raw_status, 'the detached background supervisor exits on its own' );
    is( ( defined $raw_status ? $raw_status >> 8 : -1 ), 124, 'the supervisor reports the timeout exit code after the deadline elapses' );

    ok( -e $marker, 'the command child received SIGTERM and stayed alive through the terminate window' );
    ok( !$runner->_pid_is_running( $result->{pid} ), 'the supervisor is gone once the forced SIGKILL escalation completes' );
}

# ---------------------------------------------------------------------------
# The opposite side of the same escalation check: a command child that honours
# SIGTERM is already reaped when the escalation waitpid runs, so no SIGKILL is
# sent (line 208, `waitpid(...) == 0` false side).
#
# `exec sleep 60` makes the shell replace itself with the sleep, so the process
# the supervisor signals is the process that dies -- there is no shell left

t/13-integration-assets.t  view on Meta::CPAN

    like( $timeout_e2e, qr/collector", "run"/, 'Windows timeout E2E script triggers the blocking collector through dashboard collector run' );
    like( $timeout_e2e, qr/ConvertFrom-Json/, 'Windows timeout E2E script parses the structured collector run result' );
    like( $timeout_e2e, qr/timed_out/, 'Windows timeout E2E script asserts the timed_out flag on the collector result' );
    like( $timeout_e2e, qr/\b124\b/, 'Windows timeout E2E script asserts the canonical timeout exit code 124' );
    like( $timeout_e2e, qr/collector", "status"/, 'Windows timeout E2E script verifies the cached collector status after the timeout' );
    like( $timeout_e2e, qr/last_exit_code/, 'Windows timeout E2E script asserts the persisted last_exit_code reflects the timeout' );
    like( $timeout_e2e, qr/\[System\.Diagnostics\.Stopwatch\]::StartNew/, 'Windows timeout E2E script measures elapsed wall-clock time to prove the blocking command was interrupted' );
    like( $timeout_e2e, qr/Assert-MarkerProcessesGone/, 'Windows timeout E2E script asserts the blocker and its descendant are both really gone after the timeout' );
    like( $timeout_e2e, qr/\@\(\s*Get-ProcessesMatching/, 'Windows timeout E2E script forces array semantics on process queries so the zero-survivor success path cannot trip strict mode' );
    unlike( $timeout_e2e, qr/\$survivors\s*=\s*Get-ProcessesMatching/, 'Windows timeout E2E script never assigns a process query straight to a variable it then counts' );
    like( $timeout_e2e, qr/agent-alive-ok/, 'Windows timeout E2E script proves the collector agent survives the timeout by running a healthy collector afterwards' );
    like( $timeout_e2e, qr/__END__/, 'Windows timeout E2E script carries POD trailer' );
    unlike( $timeout_e2e, qr/Developer-Dashboard-\d+\.\d+\.tar\.gz/, 'Windows timeout E2E script avoids hard-coded release tarball versions' );

    open my $dockur_fh, '<', 'integration/windows/run-dockur-collector-timeout-e2e.sh' or die $!;
    my $dockur = do { local $/; <$dockur_fh> };
    close $dockur_fh;
    like( $dockur, qr/^#!/, 'Dockur timeout E2E driver carries a shebang' );
    like( $dockur, qr/set -euo pipefail/, 'Dockur timeout E2E driver fails fast instead of continuing past a broken step' );
    like( $dockur, qr/run-collector-timeout-e2e\.ps1/, 'Dockur timeout E2E driver ships the in-guest harness into the guest' );
    like( $dockur, qr/ddagent-alive\.txt/, 'Dockur timeout E2E driver verifies the in-guest job agent is alive before submitting work' );
    like( $dockur, qr/ddjob-/, 'Dockur timeout E2E driver submits work through the shared-folder job protocol' );
    like( $dockur, qr/Developer-Dashboard-\*\.tar\.gz|\*\.tar\.gz/, 'Dockur timeout E2E driver uses version-agnostic tarball discovery' );
    like( $dockur, qr/cpanm/, 'Dockur timeout E2E driver installs the built tarball in the guest before running the E2E' );
    like( $dockur, qr/_await_windows_command|DD_E2E_REQUIRE_SYMBOL|require_symbol/, 'Dockur timeout E2E driver proves the installed guest product carries the Windows timeout implementation' );
    like( $dockur, qr/DD_STATE_ROOT_USER|HOME/, 'Dockur timeout E2E driver gives the guest install the environment a non-interactive Windows session lacks' );
    like( $dockur, qr/exit:/, 'Dockur timeout E2E driver reads the job status file the guest agent writes' );
    like( $dockur, qr/tr -d/, 'Dockur timeout E2E driver strips the guest CRLF from the status file before reading the exit code' );
    like( $dockur, qr/git-common-dir/, 'Dockur timeout E2E driver finds the shared folder in the main checkout when it runs from a ticket worktree' );
    like( $dockur, qr/\[A-Za-z0-9\._-\]\+/, 'Dockur timeout E2E driver validates the job namespace before interpolating it into the generated guest script' );
    like( $dockur, qr/__END__/, 'Dockur timeout E2E driver carries POD trailer' );

t/14-coverage-closure-extra.t  view on Meta::CPAN

        );
        exit( $ok ? 0 : 1 );
    }
    waitpid( $child, 0 );
    my @persisted = $runner->_state_active_worker_pids($persist_name);
    ok( scalar(@persisted) >= 1, 'loop persists active_worker_pids immediately on spawn so a just-started worker survives a crash-and-stop' );
    for my $wpid (@persisted) { kill 9, -$wpid; kill 9, $wpid; }
}

{
    # Fix D (Facet 1c): a command child that ignores SIGTERM stays alive in the
    # worker's process group after the worker (group leader) exits; pass 2 must
    # send the group SIGKILL unconditionally to reap it.
    my $gc_file = File::Spec->catfile( $home, "trapgc.$$" );
    unlink $gc_file;
    my $worker = fork();
    die "fork failed: $!" if !defined $worker;
    if ( !$worker ) {
        setsid();
        my $gc = fork();
        if ( !defined $gc ) { exit 1 }

t/14-coverage-closure-extra.t  view on Meta::CPAN

            interval      => 0,
            job           => { command => 'printf scrub', cwd => $home },
            name          => $loop_name,
            schedule_mode => 'interval',
            single_tick   => 1,
            title         => $runner->_process_title($loop_name),
        );
        exit( $ok ? 0 : 1 );
    }
    waitpid( $child_pid, 0 );
    is( $? >> 8, 0, '_run_loop_child keeps a coverage-instrumented child alive long enough to execute one scrubbed tick' );
    open my $seen_fh, '<', $seen_file or die "Unable to read $seen_file: $!";
    my $seen = json_decode( do { local $/; <$seen_fh> } );
    close $seen_fh;
    is( $seen->{perl5opt}, '', '_run_loop_child clears PERL5OPT inside managed collector children when coverage instrumentation is active' );
    is( $seen->{harness_perl_switches}, '', '_run_loop_child clears HARNESS_PERL_SWITCHES inside managed collector children when coverage instrumentation is active' );
}

{
    my $loop_name = 'coverage.loop.error';
    my $child_pid = fork();

t/141-smart-router-guard-container-reclaim.t  view on Meta::CPAN

# A fixed "now" and a fixed staleness window keep every decision below a pure
# function of its inputs, so this file never depends on a docker daemon, on the
# host clock, or on which pids happen to exist while it runs.
my $NOW         = 1_786_000_000;
my $STALE_AFTER = 3600;

my $OWN = 'dd-smart-router-two-stage-4242-1785999900';

# Purpose: build the pid-liveness probe the decision function consults, from an
#          explicit set of pids the caller wants to pretend are still running.
# Input:   a list of pids to report as alive.
# Output:  a coderef taking one pid and returning true only for those pids.
sub alive_only {
    my (@alive) = @_;
    my %alive = map { $_ => 1 } @alive;
    return sub {
        my ($pid) = @_;
        return $alive{$pid} ? 1 : 0;
    };
}

subtest 'the reclaim decision only ever targets this guard\'s own container names' => sub {
    my @names = (
        'dd-smart-router-two-stage-111-1785000000',    # a real leak
        'dd-smart-router-two-stage',                   # prefix without the pid/epoch tail
        'dd-smart-router-two-stage-111',               # pid but no epoch
        'dd-smart-router-two-stage-abc-1785000000',    # non-numeric pid
        'dd-smart-router-two-stage-111-xyz',           # non-numeric epoch
        'my-dd-smart-router-two-stage-111-1785000000', # merely contains the prefix
        'dd-blank-env-integration-111-1785000000',     # another dashboard container
        'postgres',                                    # somebody else's container entirely
    );

    my @stale = Local::DockerGuard::stale_guard_containers(
        names       => \@names,
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
    );

    is_deeply(
        \@stale,
        ['dd-smart-router-two-stage-111-1785000000'],
        'only names matching the guard\'s own dd-smart-router-two-stage-<pid>-<epoch> shape are reclaimed',
    );
};

subtest 'the container this run owns is never reclaimed' => sub {
    my @stale = Local::DockerGuard::stale_guard_containers(
        names       => [ $OWN, 'dd-smart-router-two-stage-111-1785000000' ],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
    );

    is_deeply(
        \@stale,
        ['dd-smart-router-two-stage-111-1785000000'],
        'the running guard\'s own container is excluded even though its pid probe is not consulted',
    );
};

subtest 'a container whose owning process is gone is reclaimed immediately' => sub {
    # The leak this ticket exists for: the run was SIGKILLed, so its END block
    # never ran, and its pid is long gone. It must not have to age out first.
    my $fresh_leak = sprintf 'dd-smart-router-two-stage-%d-%d', 777, $NOW - 5;

    my @stale = Local::DockerGuard::stale_guard_containers(
        names       => [$fresh_leak],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
    );

    is_deeply(
        \@stale,
        [$fresh_leak],
        'a seconds-old container whose embedded pid is dead is reclaimed without waiting for the age window',
    );
};

subtest 'a concurrently running guard is left alone' => sub {
    my $concurrent = sprintf 'dd-smart-router-two-stage-%d-%d', 888, $NOW - 60;

    my @stale = Local::DockerGuard::stale_guard_containers(
        names       => [$concurrent],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(888),
    );

    is_deeply( \@stale, [], 'a recent container whose owning pid is still alive is never removed underneath it' );
};

subtest 'the age window backstops pid recycling' => sub {
    # A dead run's pid can be handed to an unrelated live process, which would
    # make the liveness probe vouch for a container nobody owns. No guard run
    # lasts an hour, so age settles it.
    my $recycled = sprintf 'dd-smart-router-two-stage-%d-%d', 999, $NOW - $STALE_AFTER;

    my @stale = Local::DockerGuard::stale_guard_containers(
        names       => [$recycled],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(999),
    );

    is_deeply(
        \@stale,
        [$recycled],
        'a container older than the staleness window is reclaimed even when its embedded pid resolves to a live process',
    );

    my $just_inside = sprintf 'dd-smart-router-two-stage-%d-%d', 999, $NOW - $STALE_AFTER + 1;

    my @kept = Local::DockerGuard::stale_guard_containers(
        names       => [$just_inside],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(999),
    );

    is_deeply( \@kept, [], 'a live-owned container one second inside the window is still left alone' );
};

subtest 'a container stamped in the future is judged by its owner, not by its clock' => sub {
    my $skewed = sprintf 'dd-smart-router-two-stage-%d-%d', 555, $NOW + 120;

    my @alive_owner = Local::DockerGuard::stale_guard_containers(
        names       => [$skewed],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(555),
    );

    is_deeply( \@alive_owner, [], 'a future-stamped container with a live owner is kept' );

    my @dead_owner = Local::DockerGuard::stale_guard_containers(
        names       => [$skewed],
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
    );

    is_deeply( \@dead_owner, [$skewed], 'a future-stamped container with a dead owner is still reclaimed' );
};

subtest 'reclaim_guard_containers lists, removes, and verifies through one injected runner' => sub {
    my $leak_one = 'dd-smart-router-two-stage-111-1785000000';
    my $leak_two = 'dd-smart-router-two-stage-222-1785000001';
    my @calls;
    my @listings = (
        join( "\n", $leak_one, $OWN, 'postgres', $leak_two ) . "\n",
        "$OWN\npostgres\n",
    );

    my @removed = Local::DockerGuard::reclaim_guard_containers(
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
        runner      => sub {
            my (@argv) = @_;
            push @calls, [@argv];
            return ( shift(@listings), q{}, 0 ) if $argv[1] eq 'ps';
            return ( q{}, q{}, 0 );
        },
    );

    is_deeply( [ sort @removed ], [ sort $leak_one, $leak_two ], 'both leaked containers are reported as reclaimed' );

t/141-smart-router-guard-container-reclaim.t  view on Meta::CPAN

    is( scalar @calls, 4, 'no other docker command is issued' );
};

subtest 'nothing is removed and no verification pass runs when there is no leak' => sub {
    my @calls;

    my @removed = Local::DockerGuard::reclaim_guard_containers(
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
        runner      => sub {
            my (@argv) = @_;
            push @calls, [@argv];
            return ( "$OWN\npostgres\n", q{}, 0 );
        },
    );

    is_deeply( \@removed, [], 'a clean host reclaims nothing' );
    is( scalar @calls, 1, 'a clean host issues exactly one docker ps and no docker rm' );
};

t/141-smart-router-guard-container-reclaim.t  view on Meta::CPAN

subtest 'a container that survives its removal is a loud failure, not a silent one' => sub {
    my $leak = 'dd-smart-router-two-stage-111-1785000000';

    my $error = do {
        local $@;
        eval {
            Local::DockerGuard::reclaim_guard_containers(
                own         => $OWN,
                now         => $NOW,
                stale_after => $STALE_AFTER,
                pid_alive   => alive_only(),
                runner      => sub {
                    my (@argv) = @_;
                    return ( "$leak\n", q{}, 0 ) if $argv[1] eq 'ps';
                    return ( q{}, "permission denied while removing $leak\n", 1 );
                },
            );
            1;
        };
        $@;
    };

t/141-smart-router-guard-container-reclaim.t  view on Meta::CPAN

    # docker rm -f can exit non-zero because the container had already gone.
    # The outcome is what matters, so a non-zero exit with the container
    # actually absent afterwards must not fail the guard.
    my $leak = 'dd-smart-router-two-stage-111-1785000000';
    my @listings = ( "$leak\n", q{} );

    my @removed = Local::DockerGuard::reclaim_guard_containers(
        own         => $OWN,
        now         => $NOW,
        stale_after => $STALE_AFTER,
        pid_alive   => alive_only(),
        runner      => sub {
            my (@argv) = @_;
            return ( shift(@listings), q{}, 0 ) if $argv[1] eq 'ps';
            return ( q{}, "Error: No such container: $leak\n", 1 );
        },
    );

    is_deeply( \@removed, [$leak], 'a container that is gone afterwards counts as reclaimed whatever docker rm exited with' );
};

subtest 'an unreadable container inventory stops the guard instead of pretending the host is clean' => sub {
    my $error = do {
        local $@;
        eval {
            Local::DockerGuard::reclaim_guard_containers(
                own         => $OWN,
                now         => $NOW,
                stale_after => $STALE_AFTER,
                pid_alive   => alive_only(),
                runner      => sub { return ( q{}, "cannot connect to the docker daemon\n", 1 ) },
            );
            1;
        };
        $@;
    };

    like( $error, qr/docker ps/, 'the failing command is named' );
    like( $error, qr/cannot connect to the docker daemon/, 'the daemon error text is surfaced' );
};

subtest 'the default pid probe distinguishes a missing process from an unkillable one' => sub {
    ok( Local::DockerGuard::pid_alive($$), 'this test process is reported alive' );
    ok( Local::DockerGuard::pid_alive(1),  'pid 1 is reported alive even though this uid may not signal it' );
    ok( !Local::DockerGuard::pid_alive(0), 'pid 0 is never treated as an owner' );
    ok( !Local::DockerGuard::pid_alive(-1), 'a negative pid is never treated as an owner' );
    ok( !Local::DockerGuard::pid_alive(undef), 'an undefined pid is never treated as an owner' );

    my $unused = _unused_pid();
  SKIP: {
        skip 'no unused pid could be found on this host', 1 if !$unused;
        ok( !Local::DockerGuard::pid_alive($unused), 'a pid with no process behind it is reported dead' );
    }
};

subtest 'the smart-router guard actually reclaims before it creates its own container' => sub {
    my $guard = _slurp( File::Spec->catfile( $ROOT, 't', '44-smart-router-two-stage.t' ) );

    like(
        $guard,
        qr/unshift \s+ \@INC, \s* File::Spec->catdir\( \s* \$ROOT, \s* 't', \s* 'lib' \s* \);/mx,
        'the guard puts the repository test library on @INC by absolute path, not relative to the caller\'s cwd',

t/144-collector-fixture-recognition-race.t  view on Meta::CPAN

    wait_for_managed_loop( $runner, $untitled, 'fixture.untitled', timeout => 0.2 ),
    0,
    'wait_for_managed_loop gives up on its wall-clock budget instead of blocking for ever',
);

# The exact CI reading this file exists to explain. stop_loop takes its
# unmanaged branch, never signals the child, and returns the recorded pid, so
# waitpid reports 0 - "the child is still there" - where the fixture expects -1.
is( $runner->stop_loop('fixture.untitled'), $untitled, 'stop_loop returns the recorded pid even for a loop it does not recognize' );
is( waitpid( $untitled, WNOHANG ), 0, 'an unrecognized loop child is left running and unreaped, which is the "got 0, expected -1" failure' );
ok( kill( 0, $untitled ), 'the unrecognized loop child really is still alive rather than exited-but-unreaped' );
reap_fixture_child($untitled);

# running_loops is worse than a missed probe: it deletes the pidfile of any
# same-namespace pid it cannot recognize. A fixture that polls running_loops
# before its child is recognizable therefore destroys its own fixture on the
# first iteration, and no number of further iterations can recover it.
my $swept = fork_fixture_child( 0, undef );
my $swept_pidfile = write_bare_pidfile( 'fixture.swept', $swept );
my @swept_rows = $runner->running_loops;
is( scalar( grep { $_->{name} eq 'fixture.swept' } @swept_rows ), 0, 'running_loops does not list a loop whose child has not adopted the managed title' );

t/145-collector-start-write-order.t  view on Meta::CPAN

# observed exactly, though, by asking what is on disk AT THE MOMENT the loop
# state is written. running_loops keys on the pidfile and identifies the pid from
# the recorded state, so the invariant is simply: the pidfile must never exist
# while the state does not.
my $pidfile_existed_when_state_was_written;
my $state_written = 0;

{
    no warnings 'redefine';

    # A pid that is alive but is not a loop child, so nothing is actually forked.
    local *Developer::Dashboard::CollectorRunner::_fork_process = sub { return $$ };

    my $real_write_state = \&Developer::Dashboard::CollectorRunner::_write_loop_state;
    local *Developer::Dashboard::CollectorRunner::_write_loop_state = sub {
        my ( $self, $loop_name, @rest ) = @_;
        $state_written++;
        $pidfile_existed_when_state_was_written = -f $self->_pidfile($loop_name) ? 1 : 0;
        return $self->$real_write_state( $loop_name, @rest );
    };

t/46-runtime-manager-supervisor.t  view on Meta::CPAN

        $cleaned = 1;
        return 1;
    };
    $manager->_stop_collector_supervisor;
    is_deeply(
        \@signals,
        [
            [ 'TERM', 4242 ],
            [ 'KILL', 4242 ],
        ],
        '_stop_collector_supervisor escalates from TERM to KILL when the supervisor stays alive',
    );
    is( scalar @sleeps, 21, '_stop_collector_supervisor waits through the TERM loop and the post-KILL confirmation loop' );
    is( $reaped_pid, 4242, '_stop_collector_supervisor reaps the stopped supervisor pid after escalation' );
    ok( $cleaned, '_stop_collector_supervisor still cleans up supervisor state files after escalation' );
}

{
    my $state = $manager->_write_collector_supervisor_state(
        {
            pid           => 3210,

t/46-runtime-manager-supervisor.t  view on Meta::CPAN


Test file in the Developer Dashboard codebase. This file verifies collector
watchdog supervisor behaviour, detached lifecycle handling, and coverage for
the RuntimeManager resilience path.
Open this file when you need the implementation, regression coverage, or
runtime entrypoint for that responsibility rather than guessing which part of
the tree owns it.

=head1 WHY IT EXISTS

The collector watchdog is meant to keep managed collectors alive, restart them
after unexpected exits, and escalate to human attention when repeated crashes
continue. These tests keep that resilience path covered so the implementation
does not silently regress.

=head1 WHEN TO USE

This test runs as part of the normal repository test suite and should also be
run when changing collector lifecycle code, watchdog state handling, or
detached helper startup paths.



( run in 4.788 seconds using v1.01-cache-2.11-cpan-14f38c9f855 )