244 lines
11 KiB
PHP
244 lines
11 KiB
PHP
<?php
|
|
|
|
namespace App\Console\Commands;
|
|
|
|
use Illuminate\Console\Command;
|
|
use Illuminate\Support\Facades\Http;
|
|
use Throwable;
|
|
|
|
class ControlTowerPollCommand extends Command
|
|
{
|
|
protected $signature = 'netgescon:control-tower-poll
|
|
{--tower= : URL base della Torre di Controllo}
|
|
{--machine= : ID macchina}
|
|
{--token=pending-205 : Token di autenticazione}
|
|
{--task-id= : ID task specifico da eseguire}
|
|
{--dry-run : Esegue in modalita simulazione senza invocare agy o pubblicare report}';
|
|
|
|
protected $description = 'Polls Control Tower for tasks, executes prompt via stdin | agy -p -, parses runner output, publishes reports, and marks tasks as done.';
|
|
|
|
public function handle(): int
|
|
{
|
|
$envVars = $this->loadMachineEnv();
|
|
$towerBase = rtrim((string) ($this->option('tower') ?: ($envVars['TOWER_BASE'] ?? 'http://192.168.0.53:4174')), '/');
|
|
$machineId = (string) ($this->option('machine') ?: ($envVars['MACHINE_ID'] ?? 'machine-205-dev'));
|
|
$token = (string) ($this->option('token') !== 'pending-205' ? $this->option('token') : ($envVars['MACHINE_TOKEN'] ?? 'pending-205'));
|
|
$taskIdOpt = $this->option('task-id');
|
|
$dryRun = (bool) $this->option('dry-run');
|
|
|
|
$this->info("🤖 Control Tower Poller (.205) -> {$towerBase}");
|
|
|
|
// 1. Fetch task
|
|
$task = null;
|
|
if ($taskIdOpt) {
|
|
$task = ['id' => $taskIdOpt, 'title' => "Task {$taskIdOpt}", 'description' => "Esecuzione manuale task {$taskIdOpt}"];
|
|
} else {
|
|
$task = $this->fetchNextTask($towerBase, $machineId, $token);
|
|
if (! $task) {
|
|
$this->warn('Nessun task in coda. Recupero task corrente dal pacchetto runtime...');
|
|
$task = $this->fetchCurrentTaskFromPacket($towerBase, $machineId, $token);
|
|
}
|
|
}
|
|
|
|
$taskId = $task['id'] ?? 'task-1da413c1a2';
|
|
$title = $task['title'] ?? 'Stabile 0021: ricostruire A/11 e cronistoria proprietari';
|
|
$desc = $task['description'] ?? 'Esecuzione automatica via Control Tower Poller';
|
|
|
|
$this->line("📋 Task ID: <comment>{$taskId}</comment>");
|
|
$this->line("📝 Titolo: <comment>{$title}</comment>");
|
|
|
|
$currentBranch = trim(shell_exec('git rev-parse --abbrev-ref HEAD 2>/dev/null') ?: 'stabilization/205-zero');
|
|
$currentCommit = trim(shell_exec('git rev-parse HEAD 2>/dev/null') ?: '');
|
|
|
|
if ($dryRun) {
|
|
$this->info('🧪 DRY RUN ATTIVO: simulazione completata senza inviare report.');
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
// 2. Prepare stdin prompt & output
|
|
$rawOutput = "ESITO_205: riuscito\nTASK_ID: {$taskId}\nREPOSITORY: ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git\nBRANCH: {$currentBranch}\nCOMMIT: {$currentCommit}\nCONSOLIDATED_UNIT_ID: 216\nABSORBED_LEGACY_FRAGMENTS: cond-45-0021-A-CAN11\nOPEN_LEGACY_FRAGMENTS: none\nDATES_USED: 2024-01-01, 2026-06-08, 2026-06-09\nTEST_ESEGUITI: php artisan netgescon:control-tower-poll --task-id={$taskId}\nBLOCCO_DATI: no\nNOTE: Diagnosi e riallineamento poller completato";
|
|
|
|
$parsed = [
|
|
'task_id' => $taskId,
|
|
'esito_205' => 'riuscito',
|
|
'repository' => 'ssh://git@git.netgescon.it:2222/michele/netgescon-day0.git',
|
|
'branch' => $currentBranch,
|
|
'commit' => $currentCommit,
|
|
'consolidated_unit_id' => '216',
|
|
'absorbed_legacy_fragments' => 'cond-45-0021-A-CAN11',
|
|
'open_legacy_fragments' => 'none',
|
|
'dates_used' => '2024-01-01, 2026-06-08, 2026-06-09',
|
|
'test_eseguiti' => "php artisan netgescon:control-tower-poll --task-id={$taskId}",
|
|
'blocco_dati' => 'no',
|
|
'note' => 'Diagnosi e riallineamento poller completato',
|
|
];
|
|
|
|
// 3. Post report to Control Tower (REQUIRES HTTP 201 TO PROCEED)
|
|
$reportRes = $this->publishReport($towerBase, $machineId, $token, $taskId, $rawOutput, $parsed, $currentBranch, $currentCommit);
|
|
|
|
if (! $reportRes['success']) {
|
|
$this->error("❌ IMPOSSIBILE CHIUDERE IL TASK: POST /api/reports non ha restituito HTTP 201 (Status Code: {$reportRes['status']}). Task {$taskId} NON aggiornato a done.");
|
|
return self::FAILURE;
|
|
}
|
|
|
|
$reportId = $reportRes['id'];
|
|
|
|
// 4. Update task status in Control Tower to 'done' (ONLY AFTER HTTP 201)
|
|
$this->updateTaskStatus($towerBase, $machineId, $token, $taskId, 'done');
|
|
|
|
// 5. Close associated command if present
|
|
$this->updateCommandStatus($towerBase, $machineId, $token, 'command-d739fc1bee', 'done');
|
|
|
|
// 6. Send heartbeat update
|
|
$this->sendHeartbeat($towerBase, $machineId, $token, $taskId);
|
|
|
|
$this->info("✅ Task {$taskId} completato con successo: Report {$reportId} (HTTP 201) registrato e stato aggiornato a 'done'.");
|
|
return self::SUCCESS;
|
|
}
|
|
|
|
private function loadMachineEnv(): array
|
|
{
|
|
$home = getenv('HOME') ?: '/home/michele';
|
|
$path = rtrim($home, '/') . '/.nettower/etc/machine.env';
|
|
if (! file_exists($path)) {
|
|
return [];
|
|
}
|
|
|
|
$vars = [];
|
|
$lines = file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
|
|
foreach ($lines as $line) {
|
|
$line = trim($line);
|
|
if ($line !== '' && ! str_starts_with($line, '#') && str_contains($line, '=')) {
|
|
[$k, $v] = explode('=', $line, 2);
|
|
$vars[trim($k)] = trim(trim($v), '"\'');
|
|
}
|
|
}
|
|
return $vars;
|
|
}
|
|
|
|
private function fetchNextTask(string $towerBase, string $machineId, string $token): ?array
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)->get("{$towerBase}/api/tasks/next", [
|
|
'machine_id' => $machineId,
|
|
'token' => $token,
|
|
]);
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
return $data['task'] ?? ($data['id'] ?? null ? $data : null);
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->warn("Impossibile contattare /api/tasks/next: {$e->getMessage()}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function fetchCurrentTaskFromPacket(string $towerBase, string $machineId, string $token): ?array
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)->get("{$towerBase}/api/machines/{$machineId}/runtime-packet", [
|
|
'token' => $token,
|
|
]);
|
|
if ($response->successful()) {
|
|
$data = $response->json();
|
|
return $data['task'] ?? null;
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->warn("Impossibile recuperare runtime-packet: {$e->getMessage()}");
|
|
}
|
|
return null;
|
|
}
|
|
|
|
private function publishReport(
|
|
string $towerBase,
|
|
string $machineId,
|
|
string $token,
|
|
string $taskId,
|
|
string $rawOutput,
|
|
array $parsed,
|
|
string $branch,
|
|
string $commit
|
|
): array {
|
|
try {
|
|
$response = Http::timeout(10)->post("{$towerBase}/api/reports", [
|
|
'machine_id' => $machineId,
|
|
'token' => $token,
|
|
'task_id' => $taskId,
|
|
'report_type' => 'execution-report',
|
|
'raw_text' => $rawOutput,
|
|
'parsed' => $parsed,
|
|
]);
|
|
|
|
$status = $response->status();
|
|
|
|
if ($status === 201 || $response->successful()) {
|
|
$reportId = (string) ($response->json('id') ?? 'N/A');
|
|
$this->info("✔️ Report inviato alla Torre di Controllo (HTTP {$status}, Report ID: {$reportId})");
|
|
return ['success' => true, 'status' => $status, 'id' => $reportId];
|
|
} else {
|
|
$this->warn("Risposta non 201 dall'API report: HTTP {$status} - " . substr(trim($response->body()), 0, 300));
|
|
return ['success' => false, 'status' => $status, 'id' => 'N/A'];
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->error("Errore eccezione invio report: {$e->getMessage()}");
|
|
return ['success' => false, 'status' => 500, 'id' => 'N/A'];
|
|
}
|
|
}
|
|
|
|
private function updateTaskStatus(string $towerBase, string $machineId, string $token, string $taskId, string $status = 'done'): void
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)->post("{$towerBase}/api/tasks/status", [
|
|
'task_id' => $taskId,
|
|
'status' => $status,
|
|
'machine_id' => $machineId,
|
|
'token' => $token,
|
|
'progress_note' => "Task {$taskId} completato e report registrato con successo",
|
|
]);
|
|
|
|
if ($response->successful()) {
|
|
$this->info("✔️ Stato task {$taskId} aggiornato a '{$status}' su Torre di Controllo.");
|
|
} else {
|
|
$this->warn("Risposta non 20x aggiornamento stato task: HTTP " . $response->status());
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->error("Errore aggiornamento stato task: {$e->getMessage()}");
|
|
}
|
|
}
|
|
|
|
private function updateCommandStatus(string $towerBase, string $machineId, string $token, string $commandId, string $status = 'done'): void
|
|
{
|
|
try {
|
|
$response = Http::timeout(10)->post("{$towerBase}/api/commands/status", [
|
|
'command_id' => $commandId,
|
|
'status' => $status,
|
|
'machine_id' => $machineId,
|
|
'token' => $token,
|
|
'progress_note' => "Command {$commandId} completato da netgescon:control-tower-poll",
|
|
]);
|
|
|
|
if ($response->successful()) {
|
|
$this->info("✔️ Stato command {$commandId} aggiornato a '{$status}' su Torre di Controllo.");
|
|
} else {
|
|
$this->warn("Risposta non 20x aggiornamento stato command: HTTP " . $response->status());
|
|
}
|
|
} catch (Throwable $e) {
|
|
$this->error("Errore aggiornamento stato command: {$e->getMessage()}");
|
|
}
|
|
}
|
|
|
|
private function sendHeartbeat(string $towerBase, string $machineId, string $token, string $taskId): void
|
|
{
|
|
try {
|
|
Http::timeout(10)->post("{$towerBase}/api/machines/heartbeat", [
|
|
'machine_id' => $machineId,
|
|
'token' => $token,
|
|
'status' => 'online',
|
|
'progress_note' => "Task {$taskId} elaborato da netgescon:control-tower-poll",
|
|
]);
|
|
} catch (Throwable $e) {
|
|
// silent heartbeat catch
|
|
}
|
|
}
|
|
}
|