diff --git a/app/Console/Commands/TmuxHealthCheck.php b/app/Console/Commands/TmuxHealthCheck.php new file mode 100644 index 000000000..28def934e --- /dev/null +++ b/app/Console/Commands/TmuxHealthCheck.php @@ -0,0 +1,165 @@ +sessionName = $this->option('session') + ?? Settings::settingValue('tmux_session') + ?? config('tmux.session.default_name', 'nntmux'); + + $this->sessionManager = new TmuxSessionManager($this->sessionName); + + // Use Laravel's built-in quiet mode check + $quiet = $this->output->isQuiet(); + + // Step 1: Check if tmux session exists + if (! $this->sessionManager->sessionExists()) { + if (! $quiet) { + $this->warn("⚠️ Tmux session '{$this->sessionName}' does not exist."); + } + + + return Command::FAILURE; + } + + if (! $quiet) { + $this->info("✅ Tmux session '{$this->sessionName}' exists."); + } + + // Step 2: Check if monitor pane (0.0) is dead + $monitorPaneDead = $this->isMonitorPaneDead(); + + if ($monitorPaneDead) { + if (! $quiet) { + $this->warn('⚠️ Monitor pane (0.0) is dead.'); + } + + if ($this->option('auto-restart')) { + return $this->restartTmux(); + } + + return Command::FAILURE; + } + + if (! $quiet) { + $this->info('✅ Monitor pane (0.0) is alive and running.'); + } + + return Command::SUCCESS; + + } catch (\Exception $e) { + $this->error('❌ Health check failed: '.$e->getMessage()); + logger()->error('Tmux health check error', [ + 'message' => $e->getMessage(), + 'trace' => $e->getTraceAsString(), + ]); + + return Command::FAILURE; + } + } + + /** + * Check if the monitor pane (0.0) is dead. + */ + private function isMonitorPaneDead(): bool + { + // Use tmux display-message to check pane_dead flag + $result = Process::timeout(10)->run( + "tmux display-message -p -t {$this->sessionName}:0.0 '#{pane_dead}'" + ); + + if (! $result->successful()) { + // If we can't even query the pane, consider it dead + return true; + } + + $paneDeadFlag = trim($result->output()); + + // tmux returns '1' if pane is dead, '0' if alive + if ($paneDeadFlag === '1') { + return true; + } + + // Additional check: verify the pane is actually running a process + $commandResult = Process::timeout(10)->run( + "tmux display-message -p -t {$this->sessionName}:0.0 '#{pane_current_command}'" + ); + + if (! $commandResult->successful()) { + return true; + } + + $currentCommand = trim($commandResult->output()); + + // If the pane is just showing a shell with no process, it might be considered "idle" + // but not necessarily dead. The pane_dead flag is the authoritative check. + return false; + } + + /** + * Restart the tmux session. + */ + private function restartTmux(): int + { + $this->info('🔄 Restarting tmux session...'); + + // First, stop the existing session if it exists + if ($this->sessionManager->sessionExists()) { + $this->info('⏹️ Stopping existing session...'); + $this->call('tmux:stop', [ + '--session' => $this->sessionName, + '--force' => true, + ]); + + // Give it a moment to clean up + sleep(2); + } + + // Start a new session + $this->info('▶️ Starting new tmux session...'); + $exitCode = $this->call('tmux:start', [ + '--session' => $this->sessionName, + ]); + + if ($exitCode === Command::SUCCESS) { + $this->info("✅ Tmux session '{$this->sessionName}' restarted successfully."); + } else { + $this->error("❌ Failed to restart tmux session '{$this->sessionName}'."); + } + + return $exitCode; + } +} diff --git a/composer.lock b/composer.lock index b174de5cf..bfb80f7ed 100644 --- a/composer.lock +++ b/composer.lock @@ -2817,16 +2817,16 @@ }, { "name": "laravel/framework", - "version": "v12.45.1", + "version": "v12.46.0", "source": { "type": "git", "url": "https://github.com/laravel/framework.git", - "reference": "1ca7e2a2ee17ae5bc435af7cb52d2f130148e2fa" + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/laravel/framework/zipball/1ca7e2a2ee17ae5bc435af7cb52d2f130148e2fa", - "reference": "1ca7e2a2ee17ae5bc435af7cb52d2f130148e2fa", + "url": "https://api.github.com/repos/laravel/framework/zipball/9dcff48d25a632c1fadb713024c952fec489c4ae", + "reference": "9dcff48d25a632c1fadb713024c952fec489c4ae", "shasum": "" }, "require": { @@ -3035,7 +3035,7 @@ "issues": "https://github.com/laravel/framework/issues", "source": "https://github.com/laravel/framework" }, - "time": "2026-01-07T00:50:24+00:00" + "time": "2026-01-07T23:26:53+00:00" }, { "name": "laravel/horizon", @@ -14810,16 +14810,16 @@ }, { "name": "friendsofphp/php-cs-fixer", - "version": "v3.92.4", + "version": "v3.92.5", "source": { "type": "git", "url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git", - "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0" + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/9e7488b19403423e02e8403cc1eb596baf4673b0", - "reference": "9e7488b19403423e02e8403cc1eb596baf4673b0", + "url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", + "reference": "260cc8c4a1d2f6d2f22cd4f9c70aa72e55ebac58", "shasum": "" }, "require": { @@ -14902,7 +14902,7 @@ ], "support": { "issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues", - "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.4" + "source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.92.5" }, "funding": [ { @@ -14910,7 +14910,7 @@ "type": "github" } ], - "time": "2026-01-04T00:38:52+00:00" + "time": "2026-01-08T21:57:37+00:00" }, { "name": "hamcrest/hamcrest-php", diff --git a/routes/console.php b/routes/console.php index 8a049811b..0b3168c3c 100644 --- a/routes/console.php +++ b/routes/console.php @@ -49,3 +49,5 @@ if (config('nntmux.purge_inactive_users') === true) { Schedule::job(new RemoveInactiveAccounts)->daily(); Schedule::job(new PurgeDeletedAccounts)->daily(); } +// Check tmux health and auto-restart if monitor pane is dead +Schedule::command('tmux:health-check --auto-restart')->everyThirtyMinutes()->withoutOverlapping();