mirror of
https://github.com/NNTmux/newznab-tmux.git
synced 2026-08-28 17:01:16 +00:00
Update to latest tmux pane handling
This commit is contained in:
@@ -4,10 +4,12 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use App\Models\Settings;
|
||||
use App\Services\Tmux\TmuxPaneManager;
|
||||
use App\Services\Tmux\TmuxSessionManager;
|
||||
use Illuminate\Console\Command;
|
||||
use RuntimeException;
|
||||
|
||||
class TmuxAttach extends Command
|
||||
{
|
||||
@@ -49,12 +51,20 @@ class TmuxAttach extends Command
|
||||
|
||||
// Select monitor pane before attaching so user lands there
|
||||
$paneManager = new TmuxPaneManager($sessionName);
|
||||
$paneManager->selectWindow(0);
|
||||
$paneManager->selectPane('0.0');
|
||||
try {
|
||||
$monitorPane = $paneManager->paneForRole(TmuxPaneRole::Monitor, '0.0');
|
||||
} catch (RuntimeException $exception) {
|
||||
$this->error('❌ '.$exception->getMessage());
|
||||
|
||||
// Execute tmux attach
|
||||
passthru("tmux attach -t {$sessionName}");
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return Command::SUCCESS;
|
||||
if (! $paneManager->selectWindow(0) || ! $paneManager->selectPane($monitorPane)) {
|
||||
$this->error('❌ Unable to select the tmux monitor pane.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
return $sessionManager->attachSession() ? Command::SUCCESS : Command::FAILURE;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,9 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use App\Models\Settings;
|
||||
use App\Services\Tmux\TmuxPaneManager;
|
||||
use App\Services\Tmux\TmuxSessionManager;
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
@@ -77,7 +79,7 @@ class TmuxHealthCheck extends Command
|
||||
|
||||
if ($monitorPaneDead) {
|
||||
if (! $quiet) {
|
||||
$this->warn('⚠️ Monitor pane (0.0) is dead.');
|
||||
$this->warn('⚠️ Monitor pane is dead.');
|
||||
}
|
||||
|
||||
if ($autoRestart) {
|
||||
@@ -92,7 +94,7 @@ class TmuxHealthCheck extends Command
|
||||
}
|
||||
|
||||
if (! $quiet) {
|
||||
$this->info('✅ Monitor pane (0.0) is alive and running.');
|
||||
$this->info('✅ Monitor pane is alive and running.');
|
||||
}
|
||||
|
||||
$this->logHealthCheck('info', 'healthy');
|
||||
@@ -111,12 +113,20 @@ class TmuxHealthCheck extends Command
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the monitor pane (0.0) is dead.
|
||||
* Check if the monitor pane is dead.
|
||||
*/
|
||||
private function isMonitorPaneDead(): bool
|
||||
{
|
||||
$paneManager = new TmuxPaneManager($this->sessionName);
|
||||
|
||||
try {
|
||||
$monitorPane = $paneManager->paneForRole(TmuxPaneRole::Monitor, '0.0');
|
||||
} catch (\RuntimeException) {
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux display-message -p -t {$this->sessionName}:0.0 '#{pane_dead}'"
|
||||
['tmux', 'display-message', '-p', '-t', $monitorPane, '#{pane_dead}']
|
||||
);
|
||||
|
||||
if (! $result->successful()) {
|
||||
@@ -130,7 +140,7 @@ class TmuxHealthCheck extends Command
|
||||
}
|
||||
|
||||
$commandResult = Process::timeout(10)->run(
|
||||
"tmux display-message -p -t {$this->sessionName}:0.0 '#{pane_current_command}'"
|
||||
['tmux', 'display-message', '-p', '-t', $monitorPane, '#{pane_current_command}']
|
||||
);
|
||||
|
||||
if (! $commandResult->successful()) {
|
||||
|
||||
@@ -193,7 +193,7 @@ class TmuxMonitor extends Command
|
||||
$this->taskRunner->runBackfill($runVar);
|
||||
|
||||
// Update releases
|
||||
$this->taskRunner->runReleasesUpdate(array_merge($runVar, ['pane' => '0.3']));
|
||||
$this->taskRunner->runReleasesUpdate($runVar);
|
||||
|
||||
// Post-processing and cleanup tasks
|
||||
$this->runPostProcessingTasks($runVar);
|
||||
@@ -207,7 +207,7 @@ class TmuxMonitor extends Command
|
||||
private function runBasicTasks(array $runVar): void
|
||||
{
|
||||
// Update releases
|
||||
$this->taskRunner->runReleasesUpdate(array_merge($runVar, ['pane' => '0.1']));
|
||||
$this->taskRunner->runReleasesUpdate($runVar);
|
||||
|
||||
// Post-processing and cleanup tasks
|
||||
$this->runPostProcessingTasks($runVar);
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Console\Commands;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use App\Models\Collection;
|
||||
use App\Models\Settings;
|
||||
use App\Services\Tmux\TmuxLayoutBuilder;
|
||||
@@ -36,6 +37,8 @@ class TmuxStart extends Command
|
||||
|
||||
private TmuxLayoutBuilder $layoutBuilder;
|
||||
|
||||
private bool $sessionCreated = false;
|
||||
|
||||
/**
|
||||
* Execute the console command.
|
||||
*/
|
||||
@@ -86,12 +89,13 @@ class TmuxStart extends Command
|
||||
|
||||
// Build the tmux layout
|
||||
if (! $this->layoutBuilder->buildLayout($sequential)) {
|
||||
$this->error('❌ Failed to build tmux layout');
|
||||
$this->error('❌ Failed to build tmux layout: '.($this->layoutBuilder->lastError() ?? 'unknown error'));
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
|
||||
$this->info('✅ Tmux layout created');
|
||||
$this->sessionCreated = true;
|
||||
|
||||
// Set running flag
|
||||
Settings::query()->where('name', 'running')->update(['value' => 1]);
|
||||
@@ -101,17 +105,23 @@ class TmuxStart extends Command
|
||||
$this->info('🚀 Starting monitor...');
|
||||
$this->startMonitor($sessionName);
|
||||
|
||||
// Select monitor pane (0.0) so attach lands there
|
||||
// Select monitor pane so attach lands there
|
||||
$paneManager = new TmuxPaneManager($sessionName);
|
||||
$paneManager->selectWindow(0);
|
||||
$paneManager->selectPane('0.0');
|
||||
if (! $paneManager->selectWindow(0)
|
||||
|| ! $paneManager->selectPane($paneManager->paneForRole(TmuxPaneRole::Monitor, '0.0'))) {
|
||||
throw new \RuntimeException($paneManager->lastError() ?? 'Unable to select the tmux monitor pane.');
|
||||
}
|
||||
|
||||
$this->info("✅ Tmux session '{$sessionName}' started successfully");
|
||||
|
||||
// Attach if requested
|
||||
if ($this->option('attach')) {
|
||||
$this->info('📎 Attaching to session...');
|
||||
$this->sessionManager->attachSession();
|
||||
if (! $this->sessionManager->attachSession()) {
|
||||
$this->error('❌ Unable to attach to the tmux session.');
|
||||
|
||||
return Command::FAILURE;
|
||||
}
|
||||
} else {
|
||||
$this->info("💡 To attach to the session, run: tmux attach -t {$sessionName}");
|
||||
$this->info('💡 Or use: php artisan tmux:attach');
|
||||
@@ -120,6 +130,11 @@ class TmuxStart extends Command
|
||||
return Command::SUCCESS;
|
||||
|
||||
} catch (\Exception $e) {
|
||||
if ($this->sessionCreated && isset($this->sessionManager)) {
|
||||
$this->sessionManager->killSession();
|
||||
Settings::query()->where('name', 'running')->update(['value' => 0]);
|
||||
}
|
||||
|
||||
$this->error('❌ Failed to start tmux: '.$e->getMessage());
|
||||
logger()->error('Tmux start error', [
|
||||
'message' => $e->getMessage(),
|
||||
@@ -192,7 +207,9 @@ class TmuxStart extends Command
|
||||
$this->warn(' ⚠ Using artisan command (not recommended for pane)');
|
||||
}
|
||||
|
||||
// Spawn monitor in pane 0.0
|
||||
$paneManager->respawnPane('0.0', $command);
|
||||
$monitorPane = $paneManager->paneForRole(TmuxPaneRole::Monitor, '0.0');
|
||||
if (! $paneManager->respawnPane($monitorPane, $command)) {
|
||||
throw new \RuntimeException('Unable to start the tmux monitor pane.');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Enums;
|
||||
|
||||
enum TmuxPaneRole: string
|
||||
{
|
||||
case Monitor = 'monitor';
|
||||
case Binaries = 'binaries';
|
||||
case Backfill = 'backfill';
|
||||
case Releases = 'releases';
|
||||
case Sequential = 'sequential';
|
||||
case FixNames = 'fix_names';
|
||||
case RemoveCrap = 'remove_crap';
|
||||
case PostAdditional = 'post_additional';
|
||||
case PostTv = 'post_tv';
|
||||
case PostMetadata = 'post_metadata';
|
||||
case PostMovies = 'post_movies';
|
||||
case IrcScraper = 'irc_scraper';
|
||||
case Htop = 'htop';
|
||||
case Nmon = 'nmon';
|
||||
case Vnstat = 'vnstat';
|
||||
case Tcptrack = 'tcptrack';
|
||||
case BandwidthMonitor = 'bandwidth_monitor';
|
||||
case Mytop = 'mytop';
|
||||
case Redis = 'redis';
|
||||
case Console = 'console';
|
||||
}
|
||||
@@ -4,8 +4,11 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Tmux;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use App\Models\Settings;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
use Throwable;
|
||||
|
||||
/**
|
||||
* Service for building tmux window layouts based on sequential mode
|
||||
@@ -20,6 +23,10 @@ class TmuxLayoutBuilder
|
||||
|
||||
protected string $sessionName;
|
||||
|
||||
private ?string $lastError = null;
|
||||
|
||||
private bool $sessionCreated = false;
|
||||
|
||||
/**
|
||||
* Pane name icons mapping - uses Nerd Font symbols
|
||||
* Requires a Nerd Font to display properly (FiraCode NF, JetBrains Mono NF, etc.)
|
||||
@@ -61,7 +68,7 @@ class TmuxLayoutBuilder
|
||||
public function __construct(TmuxSessionManager $sessionManager)
|
||||
{
|
||||
$this->sessionManager = $sessionManager;
|
||||
$this->sessionName = $sessionManager->getSessionName();
|
||||
$this->sessionName = $sessionManager->sessionName();
|
||||
$this->paneManager = new TmuxPaneManager($this->sessionName);
|
||||
}
|
||||
|
||||
@@ -78,55 +85,90 @@ class TmuxLayoutBuilder
|
||||
*/
|
||||
public function buildLayout(int $sequentialMode): bool
|
||||
{
|
||||
return match ($sequentialMode) {
|
||||
1 => $this->buildBasicLayout(),
|
||||
2 => $this->buildStrippedLayout(),
|
||||
default => $this->buildFullLayout(),
|
||||
};
|
||||
try {
|
||||
match ($sequentialMode) {
|
||||
1 => $this->buildBasicLayout(),
|
||||
2 => $this->buildStrippedLayout(),
|
||||
default => $this->buildFullLayout(),
|
||||
};
|
||||
|
||||
return true;
|
||||
} catch (Throwable $exception) {
|
||||
$this->lastError = $exception->getMessage();
|
||||
|
||||
if ($this->sessionCreated) {
|
||||
$this->sessionManager->killSession();
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build full non-sequential layout (mode 0)
|
||||
*/
|
||||
protected function buildFullLayout(): bool
|
||||
protected function buildFullLayout(): void
|
||||
{
|
||||
// Window 0: Monitor + Binaries + Backfill + Releases
|
||||
if (! $this->sessionManager->createSession($this->getPaneDisplayName('Monitor'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select pane 0.0, then split for binaries (right side, 67%)
|
||||
$this->paneManager->selectPane('0.0');
|
||||
$this->paneManager->splitHorizontal('0', '67%', $this->getPaneDisplayName('update_binaries'));
|
||||
|
||||
// Select pane 0.1 (binaries), then split for backfill (bottom, 67%)
|
||||
$this->paneManager->selectPane('0.1');
|
||||
$this->paneManager->splitVertical('0', '67%', $this->getPaneDisplayName('backfill'));
|
||||
|
||||
// Split again for releases (bottom, 50%)
|
||||
$this->paneManager->splitVertical('0', '50%', $this->getPaneDisplayName('update_releases'));
|
||||
$monitor = $this->createSessionPane(TmuxPaneRole::Monitor, $this->getPaneDisplayName('Monitor'));
|
||||
$binaries = $this->splitHorizontal(
|
||||
$monitor,
|
||||
67,
|
||||
TmuxPaneRole::Binaries,
|
||||
$this->getPaneDisplayName('update_binaries'),
|
||||
);
|
||||
$backfill = $this->splitVertical(
|
||||
$binaries,
|
||||
67,
|
||||
TmuxPaneRole::Backfill,
|
||||
$this->getPaneDisplayName('backfill'),
|
||||
);
|
||||
$this->splitVertical(
|
||||
$backfill,
|
||||
50,
|
||||
TmuxPaneRole::Releases,
|
||||
$this->getPaneDisplayName('update_releases'),
|
||||
);
|
||||
|
||||
// Window 1: Fix names + Remove crap
|
||||
$this->paneManager->createWindow(1, ' Utils');
|
||||
$this->paneManager->setPaneTitle('1.0', $this->getPaneDisplayName('fixReleaseNames'));
|
||||
$this->paneManager->selectPane('1.0');
|
||||
$this->paneManager->splitHorizontal('1', '50%', $this->getPaneDisplayName('removeCrapReleases'));
|
||||
$fixNames = $this->createWindowPane(
|
||||
1,
|
||||
' Utils',
|
||||
TmuxPaneRole::FixNames,
|
||||
$this->getPaneDisplayName('fixReleaseNames'),
|
||||
);
|
||||
$this->splitHorizontal(
|
||||
$fixNames,
|
||||
50,
|
||||
TmuxPaneRole::RemoveCrap,
|
||||
$this->getPaneDisplayName('removeCrapReleases'),
|
||||
);
|
||||
|
||||
// Window 2: Postprocessing (Left: Additional + TV + Metadata, Right: Movies + XXX)
|
||||
$this->paneManager->createWindow(2, ' Post');
|
||||
$this->paneManager->setPaneTitle('2.0', $this->getPaneDisplayName('postprocessing_additional'));
|
||||
|
||||
// Split horizontally to create left and right halves (don't set title yet)
|
||||
$this->paneManager->splitHorizontal('2', '50%', '');
|
||||
|
||||
// Left side (2.0): split vertically for TV and Metadata
|
||||
$this->paneManager->selectPane('2.0');
|
||||
$this->paneManager->splitVertical('2', '67%', $this->getPaneDisplayName('postprocessing_tv'));
|
||||
$this->paneManager->splitVertical('2', '50%', $this->getPaneDisplayName('postprocessing_amazon'));
|
||||
|
||||
// Right side: After left splits, right side is now 2.3
|
||||
$this->paneManager->selectPane('2.3');
|
||||
$this->paneManager->setPaneTitle('2.3', $this->getPaneDisplayName('postprocessing_movies'));
|
||||
// Window 2: Postprocessing (left stack and right-side movies)
|
||||
$additional = $this->createWindowPane(
|
||||
2,
|
||||
' Post',
|
||||
TmuxPaneRole::PostAdditional,
|
||||
$this->getPaneDisplayName('postprocessing_additional'),
|
||||
);
|
||||
$this->splitHorizontal(
|
||||
$additional,
|
||||
50,
|
||||
TmuxPaneRole::PostMovies,
|
||||
$this->getPaneDisplayName('postprocessing_movies'),
|
||||
);
|
||||
$tv = $this->splitVertical(
|
||||
$additional,
|
||||
67,
|
||||
TmuxPaneRole::PostTv,
|
||||
$this->getPaneDisplayName('postprocessing_tv'),
|
||||
);
|
||||
$this->splitVertical(
|
||||
$tv,
|
||||
50,
|
||||
TmuxPaneRole::PostMetadata,
|
||||
$this->getPaneDisplayName('postprocessing_amazon'),
|
||||
);
|
||||
|
||||
// Window 3: IRC Scraper
|
||||
$this->createIRCScraperWindow();
|
||||
@@ -134,90 +176,115 @@ class TmuxLayoutBuilder
|
||||
// Additional windows (optional monitoring tools)
|
||||
$this->createOptionalWindows();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build basic sequential layout (mode 1)
|
||||
*/
|
||||
protected function buildBasicLayout(): bool
|
||||
protected function buildBasicLayout(): void
|
||||
{
|
||||
// Window 0: Monitor + Releases
|
||||
if (! $this->sessionManager->createSession($this->getPaneDisplayName('Monitor'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select pane 0.0, then split for releases
|
||||
$this->paneManager->selectPane('0.0');
|
||||
$this->paneManager->splitHorizontal('0', '67%', $this->getPaneDisplayName('update_releases'));
|
||||
$monitor = $this->createSessionPane(TmuxPaneRole::Monitor, $this->getPaneDisplayName('Monitor'));
|
||||
$this->splitHorizontal(
|
||||
$monitor,
|
||||
67,
|
||||
TmuxPaneRole::Releases,
|
||||
$this->getPaneDisplayName('update_releases'),
|
||||
);
|
||||
|
||||
// Window 1: Utils (Fix names + Remove crap)
|
||||
$this->paneManager->createWindow(1, ' Utils');
|
||||
$this->paneManager->setPaneTitle('1.0', $this->getPaneDisplayName('fixReleaseNames'));
|
||||
$this->paneManager->selectPane('1.0');
|
||||
$this->paneManager->splitHorizontal('1', '50%', $this->getPaneDisplayName('removeCrapReleases'));
|
||||
$fixNames = $this->createWindowPane(
|
||||
1,
|
||||
' Utils',
|
||||
TmuxPaneRole::FixNames,
|
||||
$this->getPaneDisplayName('fixReleaseNames'),
|
||||
);
|
||||
$this->splitHorizontal(
|
||||
$fixNames,
|
||||
50,
|
||||
TmuxPaneRole::RemoveCrap,
|
||||
$this->getPaneDisplayName('removeCrapReleases'),
|
||||
);
|
||||
|
||||
// Window 2: Postprocessing (Left: Additional + TV + Metadata, Right: Movies + XXX)
|
||||
$this->paneManager->createWindow(2, ' Post');
|
||||
$this->paneManager->setPaneTitle('2.0', $this->getPaneDisplayName('postprocessing_additional'));
|
||||
|
||||
// Split horizontally to create left and right halves (don't set title yet)
|
||||
$this->paneManager->splitHorizontal('2', '50%', '');
|
||||
|
||||
// Left side (2.0): split vertically for TV and Metadata
|
||||
$this->paneManager->selectPane('2.0');
|
||||
$this->paneManager->splitVertical('2', '67%', $this->getPaneDisplayName('postprocessing_tv'));
|
||||
$this->paneManager->splitVertical('2', '50%', $this->getPaneDisplayName('postprocessing_amazon'));
|
||||
|
||||
// Right side: After left splits, right side is now 2.3
|
||||
$this->paneManager->selectPane('2.3');
|
||||
$this->paneManager->setPaneTitle('2.3', $this->getPaneDisplayName('postprocessing_movies'));
|
||||
// Window 2: Postprocessing
|
||||
$additional = $this->createWindowPane(
|
||||
2,
|
||||
' Post',
|
||||
TmuxPaneRole::PostAdditional,
|
||||
$this->getPaneDisplayName('postprocessing_additional'),
|
||||
);
|
||||
$this->splitHorizontal(
|
||||
$additional,
|
||||
50,
|
||||
TmuxPaneRole::PostMovies,
|
||||
$this->getPaneDisplayName('postprocessing_movies'),
|
||||
);
|
||||
$tv = $this->splitVertical(
|
||||
$additional,
|
||||
67,
|
||||
TmuxPaneRole::PostTv,
|
||||
$this->getPaneDisplayName('postprocessing_tv'),
|
||||
);
|
||||
$this->splitVertical(
|
||||
$tv,
|
||||
50,
|
||||
TmuxPaneRole::PostMetadata,
|
||||
$this->getPaneDisplayName('postprocessing_amazon'),
|
||||
);
|
||||
|
||||
// Window 3: IRC Scraper
|
||||
$this->createIRCScraperWindow();
|
||||
|
||||
$this->createOptionalWindows();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Build stripped sequential layout (mode 2)
|
||||
*/
|
||||
protected function buildStrippedLayout(): bool
|
||||
protected function buildStrippedLayout(): void
|
||||
{
|
||||
// Window 0: Monitor + Sequential
|
||||
if (! $this->sessionManager->createSession($this->getPaneDisplayName('Monitor'))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Select pane 0.0, then split for sequential
|
||||
$this->paneManager->selectPane('0.0');
|
||||
$this->paneManager->splitHorizontal('0', '67%', $this->getPaneDisplayName('sequential'));
|
||||
$monitor = $this->createSessionPane(TmuxPaneRole::Monitor, $this->getPaneDisplayName('Monitor'));
|
||||
$this->splitHorizontal(
|
||||
$monitor,
|
||||
67,
|
||||
TmuxPaneRole::Sequential,
|
||||
$this->getPaneDisplayName('sequential'),
|
||||
);
|
||||
|
||||
// Window 1: Metadata postprocessing
|
||||
$this->paneManager->createWindow(1, ' Utils');
|
||||
$this->paneManager->setPaneTitle('1.0', $this->getPaneDisplayName('fixReleaseNames'));
|
||||
$this->paneManager->selectPane('1.0');
|
||||
$this->paneManager->splitHorizontal('1', '50%', $this->getPaneDisplayName('postprocessing_amazon'));
|
||||
$fixNames = $this->createWindowPane(
|
||||
1,
|
||||
' Utils',
|
||||
TmuxPaneRole::FixNames,
|
||||
$this->getPaneDisplayName('fixReleaseNames'),
|
||||
);
|
||||
$this->splitHorizontal(
|
||||
$fixNames,
|
||||
50,
|
||||
TmuxPaneRole::PostMetadata,
|
||||
$this->getPaneDisplayName('postprocessing_amazon'),
|
||||
);
|
||||
|
||||
// Window 2: IRC Scraper
|
||||
$this->createIRCScraperWindow();
|
||||
|
||||
$this->createOptionalWindows();
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create IRC scraper window
|
||||
*/
|
||||
protected function createIRCScraperWindow(): bool
|
||||
protected function createIRCScraperWindow(): void
|
||||
{
|
||||
$this->paneManager->createWindow(3, ' IRC');
|
||||
$this->paneManager->setPaneTitle('3.0', $this->getPaneDisplayName('scrapeIRC'));
|
||||
|
||||
return true;
|
||||
$this->createWindowPane(
|
||||
3,
|
||||
' IRC',
|
||||
TmuxPaneRole::IrcScraper,
|
||||
$this->getPaneDisplayName('scrapeIRC'),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -239,45 +306,45 @@ class TmuxLayoutBuilder
|
||||
|
||||
// htop
|
||||
if ((int) Settings::settingValue('htop') === 1 && $this->commandExists('htop')) {
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('htop'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", 'htop');
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('htop'), TmuxPaneRole::Htop, $this->getPaneDisplayName('htop'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, 'htop'), 'respawn htop pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
// nmon
|
||||
if ((int) Settings::settingValue('nmon') === 1 && $this->commandExists('nmon')) {
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('nmon'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", 'nmon -t');
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('nmon'), TmuxPaneRole::Nmon, $this->getPaneDisplayName('nmon'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, 'nmon -t'), 'respawn nmon pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
// vnstat
|
||||
if ((int) Settings::settingValue('vnstat') === 1 && $this->commandExists('vnstat')) {
|
||||
$vnstatArgs = Settings::settingValue('vnstat_args') ?? '';
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('vnstat'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "watch -n10 'vnstat {$vnstatArgs}'");
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('vnstat'), TmuxPaneRole::Vnstat, $this->getPaneDisplayName('vnstat'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, "watch -n10 'vnstat {$vnstatArgs}'"), 'respawn vnstat pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
// tcptrack
|
||||
if ((int) Settings::settingValue('tcptrack') === 1 && $this->commandExists('tcptrack')) {
|
||||
$tcptrackArgs = Settings::settingValue('tcptrack_args') ?? '';
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('tcptrack'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "tcptrack {$tcptrackArgs}");
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('tcptrack'), TmuxPaneRole::Tcptrack, $this->getPaneDisplayName('tcptrack'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, "tcptrack {$tcptrackArgs}"), 'respawn tcptrack pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
// bwm-ng
|
||||
if ((int) Settings::settingValue('bwmng') === 1 && $this->commandExists('bwm-ng')) {
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('bwm-ng'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", 'bwm-ng');
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('bwm-ng'), TmuxPaneRole::BandwidthMonitor, $this->getPaneDisplayName('bwm-ng'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, 'bwm-ng'), 'respawn bwm-ng pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
// mytop
|
||||
if ((int) Settings::settingValue('mytop') === 1 && $this->commandExists('mytop')) {
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('mytop'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", 'mytop -u');
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('mytop'), TmuxPaneRole::Mytop, $this->getPaneDisplayName('mytop'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, 'mytop -u'), 'respawn mytop pane');
|
||||
$windowIndex++;
|
||||
}
|
||||
|
||||
@@ -286,13 +353,13 @@ class TmuxLayoutBuilder
|
||||
$redisArgs = Settings::settingValue('redis_args') ?? '';
|
||||
$refreshInterval = 30;
|
||||
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('redis'));
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('redis'), TmuxPaneRole::Redis, $this->getPaneDisplayName('redis'));
|
||||
|
||||
// Check if custom args provided for simple redis-cli output
|
||||
if (! empty($redisArgs) && $redisArgs !== 'NULL' && $this->commandExists('redis-cli')) {
|
||||
$redisHost = config('database.redis.default.host', '127.0.0.1');
|
||||
$redisPort = config('database.redis.default.port', 6379);
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "watch -n{$refreshInterval} -c 'redis-cli -h {$redisHost} -p {$redisPort} {$redisArgs}'");
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, "watch -n{$refreshInterval} -c 'redis-cli -h {$redisHost} -p {$redisPort} {$redisArgs}'"), 'respawn Redis pane');
|
||||
} else {
|
||||
// Use PHP-based Redis monitor service
|
||||
$redisHost = config('database.redis.default.host', '127.0.0.1');
|
||||
@@ -305,11 +372,11 @@ class TmuxLayoutBuilder
|
||||
if ($connectionInfo['use_sail']) {
|
||||
// Use sail to run inside Docker container
|
||||
$sail = base_path('sail');
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "{$sail} artisan redis:monitor --refresh={$refreshInterval}");
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, "{$sail} artisan redis:monitor --refresh={$refreshInterval}"), 'respawn Redis pane');
|
||||
} else {
|
||||
// Run directly, potentially with host override
|
||||
$envPrefix = $connectionInfo['override_host'] ? "REDIS_HOST={$connectionInfo['host']} " : '';
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", "{$envPrefix}php {$artisan} redis:monitor --refresh={$refreshInterval}");
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, "{$envPrefix}php {$artisan} redis:monitor --refresh={$refreshInterval}"), 'respawn Redis pane');
|
||||
}
|
||||
}
|
||||
$windowIndex++;
|
||||
@@ -317,8 +384,78 @@ class TmuxLayoutBuilder
|
||||
|
||||
// bash console
|
||||
if ((int) Settings::settingValue('console') === 1) {
|
||||
$this->paneManager->createWindow($windowIndex, $this->getPaneDisplayName('bash'));
|
||||
$this->paneManager->respawnPane("{$windowIndex}.0", 'bash -i');
|
||||
$pane = $this->createWindowPane($windowIndex, $this->getPaneDisplayName('bash'), TmuxPaneRole::Console, $this->getPaneDisplayName('bash'));
|
||||
$this->requireSuccess($this->paneManager->respawnPane($pane, 'bash -i'), 'respawn console pane');
|
||||
}
|
||||
}
|
||||
|
||||
public function lastError(): ?string
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
private function createSessionPane(TmuxPaneRole $role, string $title): string
|
||||
{
|
||||
$paneId = $this->sessionManager->createSession($title);
|
||||
if ($paneId === null) {
|
||||
throw new RuntimeException($this->sessionManager->lastError() ?? 'Unable to create tmux session.');
|
||||
}
|
||||
|
||||
$this->sessionCreated = true;
|
||||
$this->configurePane($paneId, $role, $title);
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function createWindowPane(int $index, string $windowName, TmuxPaneRole $role, string $title): string
|
||||
{
|
||||
$paneId = $this->paneManager->createWindow($index, $windowName);
|
||||
if ($paneId === null) {
|
||||
throw new RuntimeException($this->paneManager->lastError() ?? "Unable to create tmux window {$index}.");
|
||||
}
|
||||
|
||||
$this->configurePane($paneId, $role, $title);
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function splitHorizontal(string $target, int $percentage, TmuxPaneRole $role, string $title): string
|
||||
{
|
||||
$paneId = $this->paneManager->splitHorizontal($target, $percentage);
|
||||
if ($paneId === null) {
|
||||
throw new RuntimeException($this->paneManager->lastError() ?? "Unable to split tmux pane {$target}.");
|
||||
}
|
||||
|
||||
$this->configurePane($paneId, $role, $title);
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function splitVertical(string $target, int $percentage, TmuxPaneRole $role, string $title): string
|
||||
{
|
||||
$paneId = $this->paneManager->splitVertical($target, $percentage);
|
||||
if ($paneId === null) {
|
||||
throw new RuntimeException($this->paneManager->lastError() ?? "Unable to split tmux pane {$target}.");
|
||||
}
|
||||
|
||||
$this->configurePane($paneId, $role, $title);
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function configurePane(string $paneId, TmuxPaneRole $role, string $title): void
|
||||
{
|
||||
$this->requireSuccess($this->paneManager->setPaneTitle($paneId, $title), "set title for {$role->value} pane");
|
||||
$this->requireSuccess($this->paneManager->setPaneRole($paneId, $role), "tag {$role->value} pane");
|
||||
}
|
||||
|
||||
private function requireSuccess(bool $successful, string $operation): void
|
||||
{
|
||||
if (! $successful) {
|
||||
$error = $this->paneManager->lastError();
|
||||
$suffix = $error === null || $error === '' ? '' : ": {$error}";
|
||||
|
||||
throw new RuntimeException("Unable to {$operation}{$suffix}.");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -328,7 +465,7 @@ class TmuxLayoutBuilder
|
||||
protected function commandExists(string $command): bool
|
||||
{
|
||||
$result = Process::timeout(5)
|
||||
->run("which {$command} 2>/dev/null");
|
||||
->run(['which', $command]);
|
||||
|
||||
return $result->successful() && str_contains($result->output(), $command);
|
||||
}
|
||||
|
||||
@@ -4,15 +4,27 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Tmux;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use Illuminate\Contracts\Process\ProcessResult;
|
||||
use Illuminate\Support\Facades\Process;
|
||||
use RuntimeException;
|
||||
|
||||
/**
|
||||
* Service for managing individual tmux panes
|
||||
*/
|
||||
class TmuxPaneManager
|
||||
{
|
||||
private const ROLE_OPTION = '@nntmux_role';
|
||||
|
||||
protected string $sessionName;
|
||||
|
||||
/**
|
||||
* @var array<string, string>|null
|
||||
*/
|
||||
private ?array $roleTargets = null;
|
||||
|
||||
private ?string $lastError = null;
|
||||
|
||||
public function __construct(string $sessionName)
|
||||
{
|
||||
$this->sessionName = $sessionName;
|
||||
@@ -21,48 +33,37 @@ class TmuxPaneManager
|
||||
/**
|
||||
* Create a new window
|
||||
*/
|
||||
public function createWindow(int $index, string $name): bool
|
||||
public function createWindow(int $index, string $name): ?string
|
||||
{
|
||||
// Escape single quotes in the name for shell
|
||||
$escapedName = str_replace("'", "'\\''", $name);
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux new-window -t {$this->sessionName}:{$index} -n '{$escapedName}' 'printf \"\\033]2;{$escapedName}\\033\"'"
|
||||
['tmux', 'new-window', '-P', '-F', '#{pane_id}', '-t', "{$this->sessionName}:{$index}", '-n', $name, 'true']
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->createdPaneId($result->successful(), $result->output(), $result->errorOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a pane horizontally
|
||||
*/
|
||||
public function splitHorizontal(string $target, string $percentage, string $title = ''): bool
|
||||
public function splitHorizontal(string $target, int $percentage): ?string
|
||||
{
|
||||
// Escape single quotes in the title for shell
|
||||
$escapedTitle = str_replace("'", "'\\''", $title);
|
||||
$titleCmd = $title ? "printf \"\\033]2;{$escapedTitle}\\033\"" : '';
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux splitw -t {$this->sessionName}:{$target} -h -l {$percentage} '{$titleCmd}'"
|
||||
['tmux', 'split-window', '-P', '-F', '#{pane_id}', '-t', $this->target($target), '-h', '-l', "{$percentage}%", 'true']
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->createdPaneId($result->successful(), $result->output(), $result->errorOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
* Split a pane vertically
|
||||
*/
|
||||
public function splitVertical(string $target, string $percentage, string $title = ''): bool
|
||||
public function splitVertical(string $target, int $percentage): ?string
|
||||
{
|
||||
// Escape single quotes in the title for shell
|
||||
$escapedTitle = str_replace("'", "'\\''", $title);
|
||||
$titleCmd = $title ? "printf \"\\033]2;{$escapedTitle}\\033\"" : '';
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux splitw -t {$this->sessionName}:{$target} -v -l {$percentage} '{$titleCmd}'"
|
||||
['tmux', 'split-window', '-P', '-F', '#{pane_id}', '-t', $this->target($target), '-v', '-l', "{$percentage}%", 'true']
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->createdPaneId($result->successful(), $result->output(), $result->errorOutput());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -71,10 +72,10 @@ class TmuxPaneManager
|
||||
public function selectPane(string $target): bool
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux selectp -t {$this->sessionName}:{$target}"
|
||||
['tmux', 'select-pane', '-t', $this->target($target)]
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,10 +84,10 @@ class TmuxPaneManager
|
||||
public function selectWindow(int $window): bool
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux select-window -t {$this->sessionName}:{$window}"
|
||||
['tmux', 'select-window', '-t', "{$this->sessionName}:{$window}"]
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -94,17 +95,17 @@ class TmuxPaneManager
|
||||
*/
|
||||
public function respawnPane(string $target, string $command, bool $kill = false): bool
|
||||
{
|
||||
$killFlag = $kill ? '-k' : '';
|
||||
|
||||
// Escape the command for tmux by replacing double quotes with escaped quotes
|
||||
$escapedCommand = str_replace('"', '\\"', $command);
|
||||
$escapedCommand = str_replace('$', '\\$', $escapedCommand);
|
||||
$arguments = ['tmux', 'respawn-pane'];
|
||||
if ($kill) {
|
||||
$arguments[] = '-k';
|
||||
}
|
||||
array_push($arguments, '-t', $this->target($target), $command);
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux respawnp {$killFlag} -t {$this->sessionName}:{$target} \"{$escapedCommand}\""
|
||||
$arguments
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -112,13 +113,14 @@ class TmuxPaneManager
|
||||
*/
|
||||
public function sendKeys(string $target, string $keys, bool $enter = true): bool
|
||||
{
|
||||
$enterKey = $enter ? ' Enter' : '';
|
||||
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux send-keys -t {$this->sessionName}:{$target} '{$keys}'{$enterKey}"
|
||||
array_values(array_filter(
|
||||
['tmux', 'send-keys', '-t', $this->target($target), '--', $keys, $enter ? 'Enter' : null],
|
||||
static fn (?string $argument): bool => $argument !== null,
|
||||
))
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -127,10 +129,10 @@ class TmuxPaneManager
|
||||
public function killPane(string $target): bool
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux kill-pane -t {$this->sessionName}:{$target}"
|
||||
['tmux', 'kill-pane', '-t', $this->target($target)]
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -138,14 +140,11 @@ class TmuxPaneManager
|
||||
*/
|
||||
public function setPaneTitle(string $target, string $title): bool
|
||||
{
|
||||
// Escape single quotes in the title for shell
|
||||
$escapedTitle = str_replace("'", "'\\''", $title);
|
||||
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux select-pane -t {$this->sessionName}:{$target} -T '{$escapedTitle}'"
|
||||
['tmux', 'select-pane', '-t', $this->target($target), '-T', $title]
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
return $this->recordResult($result);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,7 +153,7 @@ class TmuxPaneManager
|
||||
public function getPaneTitle(string $target): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux display-message -p -t {$this->sessionName}:{$target} '#{pane_title}'"
|
||||
['tmux', 'display-message', '-p', '-t', $this->target($target), '#{pane_title}']
|
||||
);
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
@@ -166,9 +165,146 @@ class TmuxPaneManager
|
||||
public function capturePane(string $target, int $lines = 100): string
|
||||
{
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux capture-pane -p -t {$this->sessionName}:{$target} -S -{$lines}"
|
||||
['tmux', 'capture-pane', '-p', '-t', $this->target($target), '-S', "-{$lines}"]
|
||||
);
|
||||
|
||||
return $result->successful() ? $result->output() : '';
|
||||
}
|
||||
|
||||
public function setPaneRole(string $target, TmuxPaneRole $role): bool
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
['tmux', 'set-option', '-p', '-t', $this->target($target), self::ROLE_OPTION, $role->value]
|
||||
);
|
||||
|
||||
if ($this->recordResult($result)) {
|
||||
$this->roleTargets = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public function paneForRole(TmuxPaneRole $role, ?string $legacyTarget = null): string
|
||||
{
|
||||
$targets = $this->roleTargets();
|
||||
|
||||
if (isset($targets[$role->value])) {
|
||||
return $targets[$role->value];
|
||||
}
|
||||
|
||||
if ($legacyTarget !== null) {
|
||||
return $this->tagLegacyPane($role, $legacyTarget);
|
||||
}
|
||||
|
||||
throw new RuntimeException("Tmux pane role '{$role->value}' was not found in session '{$this->sessionName}'.");
|
||||
}
|
||||
|
||||
public function lastError(): ?string
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function roleTargets(): array
|
||||
{
|
||||
if ($this->roleTargets !== null) {
|
||||
return $this->roleTargets;
|
||||
}
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
['tmux', 'list-panes', '-s', '-t', $this->sessionName, '-F', "#{pane_id}\t#{".self::ROLE_OPTION.'}']
|
||||
);
|
||||
|
||||
if (! $result->successful()) {
|
||||
throw new RuntimeException(
|
||||
"Unable to list panes for tmux session '{$this->sessionName}': ".trim($result->errorOutput())
|
||||
);
|
||||
}
|
||||
|
||||
$targets = [];
|
||||
foreach (preg_split('/\R/', trim($result->output())) ?: [] as $line) {
|
||||
if ($line === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
[$paneId, $role] = array_pad(explode("\t", $line, 2), 2, '');
|
||||
if ($role === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (isset($targets[$role])) {
|
||||
throw new RuntimeException(
|
||||
"Tmux pane role '{$role}' is assigned more than once in session '{$this->sessionName}'."
|
||||
);
|
||||
}
|
||||
|
||||
$targets[$role] = $paneId;
|
||||
}
|
||||
|
||||
return $this->roleTargets = $targets;
|
||||
}
|
||||
|
||||
private function target(string $target): string
|
||||
{
|
||||
return str_starts_with($target, '%') ? $target : "{$this->sessionName}:{$target}";
|
||||
}
|
||||
|
||||
private function tagLegacyPane(TmuxPaneRole $role, string $legacyTarget): string
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
['tmux', 'display-message', '-p', '-t', $this->target($legacyTarget), '#{pane_id}']
|
||||
);
|
||||
$paneId = trim($result->output());
|
||||
|
||||
if (! $result->successful() || ! preg_match('/^%[0-9]+$/', $paneId)) {
|
||||
throw new RuntimeException(
|
||||
"Tmux pane role '{$role->value}' and legacy target '{$legacyTarget}' were not found in session '{$this->sessionName}'."
|
||||
);
|
||||
}
|
||||
|
||||
if (! $this->setPaneRole($paneId, $role)) {
|
||||
throw new RuntimeException(
|
||||
"Unable to tag legacy tmux pane '{$legacyTarget}' as '{$role->value}': {$this->lastError}."
|
||||
);
|
||||
}
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function createdPaneId(bool $successful, string $output, string $errorOutput): ?string
|
||||
{
|
||||
if (! $successful) {
|
||||
$this->lastError = trim($errorOutput);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$paneId = trim($output);
|
||||
if (! preg_match('/^%[0-9]+$/', $paneId)) {
|
||||
$this->lastError = "Tmux returned an invalid pane ID: '{$paneId}'.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->lastError = null;
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
private function recordResult(ProcessResult $result): bool
|
||||
{
|
||||
if ($result->successful()) {
|
||||
$this->lastError = null;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
$this->lastError = trim($result->errorOutput());
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,6 +16,8 @@ class TmuxSessionManager
|
||||
|
||||
protected string $configFile;
|
||||
|
||||
private ?string $lastError = null;
|
||||
|
||||
public function __construct(?string $sessionName = null)
|
||||
{
|
||||
$this->sessionName = $sessionName ?? $this->getSessionName();
|
||||
@@ -32,13 +34,18 @@ class TmuxSessionManager
|
||||
?? config('tmux.session.default_name', 'nntmux');
|
||||
}
|
||||
|
||||
public function sessionName(): string
|
||||
{
|
||||
return $this->sessionName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if tmux session exists
|
||||
*/
|
||||
public function sessionExists(): bool
|
||||
{
|
||||
$result = Process::timeout(10)
|
||||
->run("tmux list-sessions 2>/dev/null | grep -q '^{$this->sessionName}:'");
|
||||
->run(['tmux', 'has-session', '-t', $this->sessionName]);
|
||||
|
||||
return $result->successful();
|
||||
}
|
||||
@@ -46,22 +53,50 @@ class TmuxSessionManager
|
||||
/**
|
||||
* Create a new tmux session
|
||||
*/
|
||||
public function createSession(string $windowName = 'Monitor'): bool
|
||||
public function createSession(string $windowName = 'Monitor'): ?string
|
||||
{
|
||||
if ($this->sessionExists()) {
|
||||
return false;
|
||||
$this->lastError = "Tmux session '{$this->sessionName}' already exists.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$configOption = file_exists($this->configFile) ? "-f {$this->configFile}" : '';
|
||||
|
||||
// Escape single quotes in the window name for shell
|
||||
$escapedWindowName = str_replace("'", "'\\''", $windowName);
|
||||
|
||||
$result = Process::timeout(30)->run(
|
||||
"tmux {$configOption} new-session -d -s {$this->sessionName} -n '{$escapedWindowName}' 'printf \"\\033]2;{$escapedWindowName}\\033\"'"
|
||||
$arguments = ['tmux'];
|
||||
if (file_exists($this->configFile)) {
|
||||
array_push($arguments, '-f', $this->configFile);
|
||||
}
|
||||
array_push(
|
||||
$arguments,
|
||||
'new-session',
|
||||
'-d',
|
||||
'-P',
|
||||
'-F',
|
||||
'#{pane_id}',
|
||||
'-s',
|
||||
$this->sessionName,
|
||||
'-n',
|
||||
$windowName,
|
||||
'true',
|
||||
);
|
||||
|
||||
return $result->successful();
|
||||
$result = Process::timeout(30)->run($arguments);
|
||||
|
||||
if (! $result->successful()) {
|
||||
$this->lastError = trim($result->errorOutput());
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$paneId = trim($result->output());
|
||||
if (! preg_match('/^%[0-9]+$/', $paneId)) {
|
||||
$this->lastError = "Tmux returned an invalid pane ID: '{$paneId}'.";
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
$this->lastError = null;
|
||||
|
||||
return $paneId;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -73,7 +108,7 @@ class TmuxSessionManager
|
||||
return true;
|
||||
}
|
||||
|
||||
$result = Process::timeout(30)->run("tmux kill-session -t {$this->sessionName}");
|
||||
$result = Process::timeout(30)->run(['tmux', 'kill-session', '-t', $this->sessionName]);
|
||||
|
||||
return $result->successful();
|
||||
}
|
||||
@@ -87,11 +122,9 @@ class TmuxSessionManager
|
||||
return false;
|
||||
}
|
||||
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux select-window -t {$this->sessionName}:0; tmux attach-session -d -t {$this->sessionName}"
|
||||
);
|
||||
passthru('tmux attach-session -t '.escapeshellarg($this->sessionName), $exitCode);
|
||||
|
||||
return $result->successful();
|
||||
return $exitCode === 0;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -106,7 +139,7 @@ class TmuxSessionManager
|
||||
}
|
||||
|
||||
$result = Process::timeout(10)->run(
|
||||
"tmux list-panes -s -t {$this->sessionName} -F '#{window_index}:#{pane_index} #{pane_title}'"
|
||||
['tmux', 'list-panes', '-s', '-t', $this->sessionName, '-F', "#{window_index}:#{pane_index}\t#{pane_title}"]
|
||||
);
|
||||
|
||||
if (! $result->successful()) {
|
||||
@@ -121,7 +154,7 @@ class TmuxSessionManager
|
||||
continue;
|
||||
}
|
||||
|
||||
[$position, $title] = explode(' ', $line, 2);
|
||||
[$position, $title] = array_pad(explode("\t", $line, 2), 2, '');
|
||||
$panes[$position] = $title;
|
||||
}
|
||||
|
||||
@@ -134,7 +167,7 @@ class TmuxSessionManager
|
||||
public function getPaneStatus(string $window, string $pane): ?string
|
||||
{
|
||||
$result = Process::timeout(5)->run(
|
||||
"tmux display-message -p -t {$this->sessionName}:{$window}.{$pane} '#{pane_current_command}'"
|
||||
['tmux', 'display-message', '-p', '-t', "{$this->sessionName}:{$window}.{$pane}", '#{pane_current_command}']
|
||||
);
|
||||
|
||||
return $result->successful() ? trim($result->output()) : null;
|
||||
@@ -149,4 +182,9 @@ class TmuxSessionManager
|
||||
|
||||
return $status !== null && $status !== 'bash' && $status !== 'sh';
|
||||
}
|
||||
|
||||
public function lastError(): ?string
|
||||
{
|
||||
return $this->lastError;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace App\Services\Tmux;
|
||||
|
||||
use App\Enums\TmuxPaneRole;
|
||||
use App\Models\Settings;
|
||||
|
||||
/**
|
||||
@@ -49,15 +50,18 @@ class TmuxTaskRunner
|
||||
*/
|
||||
public function runTask(string $taskName, array $config): bool
|
||||
{
|
||||
$pane = $config['pane'] ?? null;
|
||||
$role = $config['role'] ?? null;
|
||||
$command = $config['command'] ?? null;
|
||||
$enabled = $config['enabled'] ?? true;
|
||||
$workAvailable = $config['work_available'] ?? true;
|
||||
|
||||
if (! $pane || ! $command) {
|
||||
if (! $role instanceof TmuxPaneRole || ! $command) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$legacyPane = $config['legacy_pane'] ?? null;
|
||||
$pane = $this->paneManager->paneForRole($role, is_string($legacyPane) ? $legacyPane : null);
|
||||
|
||||
// Check if task is enabled and has work
|
||||
if (! $enabled) {
|
||||
return $this->disablePane($pane, $taskName, 'disabled in settings');
|
||||
@@ -183,7 +187,7 @@ class TmuxTaskRunner
|
||||
public function runIRCScraper(array $config): bool
|
||||
{
|
||||
$runScraper = (int) ($config['constants']['run_ircscraper'] ?? 0);
|
||||
$pane = '3.0';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::IrcScraper, '3.0');
|
||||
|
||||
if ($runScraper !== 1) {
|
||||
return $this->disablePane($pane, 'IRC Scraper', 'disabled in settings');
|
||||
@@ -206,7 +210,7 @@ class TmuxTaskRunner
|
||||
{
|
||||
$enabled = (int) ($config['settings']['binaries_run'] ?? 0);
|
||||
$killswitch = $config['killswitch']['pp'] ?? false;
|
||||
$pane = '0.1';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::Binaries, '0.1');
|
||||
|
||||
if (! $enabled) {
|
||||
return $this->disablePane($pane, 'Update Binaries', 'disabled in settings');
|
||||
@@ -243,7 +247,7 @@ class TmuxTaskRunner
|
||||
$enabled = (int) ($config['settings']['backfill'] ?? 0);
|
||||
$collKillswitch = $config['killswitch']['coll'] ?? false;
|
||||
$ppKillswitch = $config['killswitch']['pp'] ?? false;
|
||||
$pane = '0.2';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::Backfill, '0.2');
|
||||
|
||||
if (! $enabled) {
|
||||
return $this->disablePane($pane, 'Backfill', 'disabled in settings');
|
||||
@@ -287,7 +291,8 @@ class TmuxTaskRunner
|
||||
public function runReleasesUpdate(array $config): bool
|
||||
{
|
||||
$enabled = (int) ($config['settings']['releases_run'] ?? 0);
|
||||
$pane = $config['pane'] ?? '0.3';
|
||||
$legacyPane = (int) ($config['constants']['sequential'] ?? 0) === 1 ? '0.1' : '0.3';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::Releases, $legacyPane);
|
||||
|
||||
if (! $enabled) {
|
||||
return $this->disablePane($pane, 'Update Releases', 'disabled in settings');
|
||||
@@ -354,7 +359,7 @@ class TmuxTaskRunner
|
||||
// For now, delegate to existing methods
|
||||
$this->runBinariesUpdate($runVar);
|
||||
$this->runBackfill($runVar);
|
||||
$this->runReleasesUpdate(array_merge($runVar, ['pane' => '0.3']));
|
||||
$this->runReleasesUpdate($runVar);
|
||||
|
||||
return true;
|
||||
}
|
||||
@@ -366,7 +371,7 @@ class TmuxTaskRunner
|
||||
*/
|
||||
protected function runMainBasic(array $runVar): bool
|
||||
{
|
||||
return $this->runReleasesUpdate(array_merge($runVar, ['pane' => '0.1']));
|
||||
return $this->runReleasesUpdate($runVar);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -377,7 +382,7 @@ class TmuxTaskRunner
|
||||
protected function runMainSequential(array $runVar): bool
|
||||
{
|
||||
// Full sequential mode - runs group:update-all for each group
|
||||
$pane = '0.1';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::Sequential, '0.1');
|
||||
|
||||
$niceness = $this->getNiceness();
|
||||
$artisan = base_path('artisan');
|
||||
@@ -396,7 +401,7 @@ class TmuxTaskRunner
|
||||
{
|
||||
$enabled = (int) ($runVar['settings']['fix_names'] ?? 0);
|
||||
$work = (int) ($runVar['counts']['now']['processrenames'] ?? 0);
|
||||
$pane = '1.0';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::FixNames, '1.0');
|
||||
|
||||
if ($enabled !== 1) {
|
||||
return $this->disablePane($pane, 'Fix Release Names', 'disabled in settings');
|
||||
@@ -431,7 +436,7 @@ class TmuxTaskRunner
|
||||
protected function runRemoveCrapTask(array $runVar): bool
|
||||
{
|
||||
$option = $runVar['settings']['fix_crap_opt'] ?? 'Disabled';
|
||||
$pane = '1.1';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::RemoveCrap, '1.1');
|
||||
|
||||
// Handle disabled state
|
||||
if ($option === 'Disabled' || $option === 0 || $option === '0') {
|
||||
@@ -545,7 +550,7 @@ class TmuxTaskRunner
|
||||
protected function runPostProcessAdditional(array $runVar): bool
|
||||
{
|
||||
$postSetting = (int) ($runVar['settings']['post'] ?? 0);
|
||||
$pane = '2.0';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::PostAdditional, '2.0');
|
||||
|
||||
// Check if post processing is enabled (1 = additional, 2 = nfo, 3 = both)
|
||||
if ($postSetting === 0) {
|
||||
@@ -610,7 +615,7 @@ class TmuxTaskRunner
|
||||
protected function runTvTask(array $runVar): bool
|
||||
{
|
||||
$enabled = (int) ($runVar['settings']['post_non'] ?? 0);
|
||||
$pane = '2.1';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::PostTv, '2.1');
|
||||
|
||||
if ($enabled !== 1) {
|
||||
return $this->disablePane($pane, 'Post-process TV/Anime', 'disabled in settings');
|
||||
@@ -670,7 +675,7 @@ class TmuxTaskRunner
|
||||
protected function runMoviesTask(array $runVar): bool
|
||||
{
|
||||
$enabled = (int) ($runVar['settings']['post_non'] ?? 0);
|
||||
$pane = '2.3';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::PostMovies, '2.3');
|
||||
|
||||
if ($enabled !== 1) {
|
||||
return $this->disablePane($pane, 'Post-process Movies', 'disabled in settings');
|
||||
@@ -720,7 +725,8 @@ class TmuxTaskRunner
|
||||
protected function runAmazonTask(array $runVar): bool
|
||||
{
|
||||
$enabled = (int) ($runVar['settings']['post_amazon'] ?? 0);
|
||||
$pane = '2.2';
|
||||
$legacyPane = (int) ($runVar['constants']['sequential'] ?? 0) === 2 ? '1.1' : '2.2';
|
||||
$pane = $this->paneManager->paneForRole(TmuxPaneRole::PostMetadata, $legacyPane);
|
||||
|
||||
if ($enabled !== 1) {
|
||||
return $this->disablePane($pane, 'Post-process Metadata', 'disabled in settings');
|
||||
|
||||
Reference in New Issue
Block a user