Interactive commands (REPLs like php artisan tinker, long-running like php artisan serve) require bidirectional communication with a persistent process. The current ProcessSessionManager stores process handles in PHP static memory, which is not shared across PHP-FPM workers. The TmuxSessionManager solves this but requires tmux installed on the system.
A new FileSessionManager that uses file-based IPC with a background PHP worker process per session. Zero external dependencies beyond PHP itself.
1. TmuxSessionManager — tmux installed (best, cross-worker, full PTY)
2. FileSessionManager — pure PHP fallback (cross-worker, PTY via background process)
3. ProcessSessionManager — single-worker only (artisan serve / Octane)
FileSessionManager (implements SessionManagerInterface)
session-worker.php (background PHP script, bundled in package)
proc_open + nohupproc_open with PTY modestorage/app/web-terminal/sessions/{session-id}/
├── stdin # Regular file, append-only (FPM workers write here)
├── stdout # Regular file, append-only (background worker writes here)
├── pid # Process ID of the background worker
└── exit_code # Written when command finishes (absent = still running)
SharedSessionData {
sessionId, command, pid, startedAt, lastActivity,
lastOutputPosition, // byte offset for incremental stdout reads
backend: 'file',
finished, exitCode
}
storage/app/web-terminal/sessions/{id}/stdin and stdout filesnohup php session-worker.php {id} {command} &pid filelastOutputPosition)stdout file, seek to last position, read new bytesstdin file using FILE_APPEND | LOCK_EXwhile (process is running):
read PTY stdout/stderr → append to stdout file
read stdin file from last offset → write to PTY stdin
usleep(10ms)
protected function getSessionManager(): SessionManagerInterface
{
if ($this->sessionManager === null) {
if ($this->preferTmux && TmuxSessionManager::isAvailable()) {
$this->sessionManager = new TmuxSessionManager;
} elseif (FileSessionManager::isAvailable()) {
$this->sessionManager = new FileSessionManager;
} else {
$this->sessionManager = new ProcessSessionManager;
}
}
return $this->sessionManager;
}
FileSessionManager::isAvailable() returns true when Process::isPtySupported() is true (standard on Linux/macOS).
isRunning() checks both exit_code file and PID liveness via /proc/{pid}. Dead PID without exit_code = failed session (exit code -1).cleanup() scans session directory, kills expired PIDs, removes directories.0700, files 0600.FILE_APPEND | LOCK_EX ensures atomic appends.How can I help you explore Laravel packages today?