#!/usr/bin/env php make('Illuminate\Contracts\Console\Kernel')->bootstrap(); // Configuration - adjust as needed $cfg = [ 'ami' => [ 'host' => env('AMI_HOST', '127.0.0.1'), 'scheme' => env('AMI_SCHEME', 'tcp://'), 'port' => env('AMI_PORT', 5038), 'username' => env('AMI_USERNAME', 'theAMIManager'), 'secret' => env('AMI_SECRET', 'DS3joFO49d0DNkkskjngj4j'), 'connect_timeout' => env('AMI_CONNECT_TIMEOUT', 10), 'read_timeout' => env('AMI_READ_TIMEOUT', 10) ], 'db' => [ 'dsn' => 'mysql:host=127.0.0.1;dbname=pbx_db_76140c;charset=utf8mb4', 'user' => 'egy_db_user_e7c0df', 'pass' => 'krewC9fy4xD8GQx1', 'options' => [ PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, PDO::ATTR_PERSISTENT => true, PDO::ATTR_EMULATE_PREPARES => false, ], ], 'batch' => [ 'size' => 100, 'interval' => 5, // seconds ], 'log_file' => __DIR__ . '/../storage/logs/default-listener.log', 'debug' => false, ]; // Simple logger function log_msg($msg) { global $cfg; $ts = date('Y-m-d H:i:s'); file_put_contents($cfg['log_file'], "[$ts] $msg\n", FILE_APPEND | LOCK_EX); // Also echo short messages to CLI for operator visibility // Keep echoing concise to avoid noisy output if (php_sapi_name() === 'cli') { echo "[$ts] $msg\n"; } } // Redis client helper (prefers phpredis, falls back to Predis) function getRedis() { static $client = null; if ($client !== null) return $client; // Try PHPRedis extension if (class_exists('Redis')) { $r = new Redis(); try { $r->connect('127.0.0.1', 6379, 2); // select logical DB 0 (you can change index if needed) @$r->select(0); $client = $r; return $client; } catch (Exception $e) { // fall through } } // Try Predis if available if (class_exists('Predis\Client')) { try { $predis = new Predis\Client(['host' => '127.0.0.1', 'port' => 6379]); $client = $predis; return $client; } catch (Exception $e) { // fall through } } throw new RuntimeException('No Redis client available (phpredis or predis)'); } // Batch buffer and flush (will push to Redis list `realtime:events`) $buffer = []; $lastFlush = time(); function bufferEvent(array $evt) { global $buffer, $cfg, $lastFlush; $buffer[] = $evt; if (count($buffer) >= $cfg['batch']['size'] || (time() - $lastFlush) >= $cfg['batch']['interval']) { flushBuffer(); } } function flushBuffer() { global $buffer, $lastFlush, $cfg; if (empty($buffer)) return; try { $redis = getRedis(); // Use pipeline when available for batch efficiency if (method_exists($redis, 'multi')) { // phpredis: multi pipeline $redis->multi(Redis::PIPELINE); foreach ($buffer as $r) { $payload = json_encode($r, JSON_UNESCAPED_UNICODE); $redis->rPush('realtime:events', $payload); // Optionally store per-extension last event if meta includes extension $meta = json_decode($r['meta'], true); if (isset($meta['Extension']) || isset($meta['extension'])) { $ext = $meta['Extension'] ?? $meta['extension']; $redis->hSet('realtime:ext:' . $ext, 'last_event', $payload); $redis->hSet('realtime:ext:' . $ext, 'updated_at', $r['timestamp']); } } $redis->exec(); } elseif (method_exists($redis, 'pipeline')) { // Predis $redis->pipeline(function($pipe) use ($buffer) { foreach ($buffer as $r) { $payload = json_encode($r, JSON_UNESCAPED_UNICODE); $pipe->rpush('realtime:events', $payload); $meta = json_decode($r['meta'], true); if (isset($meta['Extension']) || isset($meta['extension'])) { $ext = $meta['Extension'] ?? $meta['extension']; $pipe->hset('realtime:ext:' . $ext, 'last_event', $payload); $pipe->hset('realtime:ext:' . $ext, 'updated_at', $r['timestamp']); } } }); } else { // Fallback: push one by one foreach ($buffer as $r) { $payload = json_encode($r, JSON_UNESCAPED_UNICODE); $redis->rPush('realtime:events', $payload); $meta = json_decode($r['meta'], true); if (isset($meta['Extension']) || isset($meta['extension'])) { $ext = $meta['Extension'] ?? $meta['extension']; $redis->hSet('realtime:ext:' . $ext, 'last_event', $payload); $redis->hSet('realtime:ext:' . $ext, 'updated_at', $r['timestamp']); } } } log_msg('Flushed ' . count($buffer) . ' events to Redis'); $buffer = []; $lastFlush = time(); } catch (Exception $e) { log_msg('Redis flush failed: ' . $e->getMessage()); // Keep buffer and try later } } // Simple PAMI event listener implementation class SimpleListener implements IEventListener { public function handle(\PAMI\Message\Event\EventMessage $event) { $raw = $event->getRawContent(); $type = $event->getName(); $meta = json_encode($event->getKeys()); $row = [ 'event' => $type, 'raw' => $raw, 'timestamp' => date('Y-m-d H:i:s'), 'meta' => $meta, ]; // Buffer the event for Redis; avoid dumping every event to stdout bufferEvent($row); global $cfg; if (!empty($cfg['debug'])) { // When debug mode is on, print a concise one-line summary $ts = $row['timestamp']; $short = "$ts {$row['event']}"; echo $short . PHP_EOL; } } } // Setup AMI client $options = $cfg['ami']; $client = new ClientImpl($options); $client->registerEventListener(new SimpleListener()); // PCNTL signal handling for graceful shutdown if (function_exists('pcntl_async_signals')) { pcntl_async_signals(true); pcntl_signal(SIGTERM, function() use (&$client) { log_msg('Received SIGTERM, shutting down'); try { $client->close(); } catch (\Exception $e) {} flushBuffer(); exit(0); }); pcntl_signal(SIGINT, function() use (&$client) { log_msg('Received SIGINT, shutting down'); try { $client->close(); } catch (\Exception $e) {} flushBuffer(); exit(0); }); } // Register shutdown function register_shutdown_function(function() use (&$client) { try { $client->close(); } catch (\Exception $e) {} flushBuffer(); }); // Connect try { $client->open(); log_msg('AMI connected'); } catch (\Exception $e) { log_msg('Initial AMI connect failed: ' . $e->getMessage()); exit(1); } // Log script start log_msg('Default listener script started'); // Database usability check: try to connect and run a simple SELECT 1; retry until success function ensureDatabaseUsable() { global $cfg; $dbCfg = $cfg['db']; $tries = 0; while (true) { try { $tries++; $pdo = new PDO($dbCfg['dsn'], $dbCfg['user'], $dbCfg['pass'], $dbCfg['options']); $stmt = $pdo->query('SELECT 1'); if ($stmt !== false) { log_msg('Database usable after ' . $tries . ' attempt(s)'); return true; } } catch (\Exception $e) { if ($tries === 1) { log_msg('Database connection failed: ' . $e->getMessage() . ' - will retry'); } sleep(5); continue; } } } // Kick off DB usability check in background-ish manner (non-blocking to main loop) // We perform initial synchronous check so operator knows if DB is reachable try { ensureDatabaseUsable(); } catch (\Exception $e) { log_msg('Database usability check failed unexpectedly: ' . $e->getMessage()); } $lastPing = time(); // Main loop while (true) { try { usleep(1000); $client->process(); if ((time() - $lastPing) >= 10) { try { $client->send(new PingAction()); } catch (\Exception $e) { log_msg('Ping failed: ' . $e->getMessage() . ' - attempting reconnect'); try { $client->close(); } catch (\Exception $ex) {} sleep(1); try { $client->open(); $client->registerEventListener(new SimpleListener()); log_msg('Reconnected to AMI'); } catch (\Exception $re) { log_msg('Reconnect failed: ' . $re->getMessage()); } } $lastPing = time(); } // Periodic flush by time if ((time() - $lastFlush) >= $cfg['batch']['interval']) { flushBuffer(); } } catch (\Exception $e) { log_msg('Main loop exception: ' . $e->getMessage()); try { $client->close(); } catch (\Exception $ex) {} sleep(2); try { $client->open(); $client->registerEventListener(new SimpleListener()); } catch (\Exception $re) { log_msg('Failed to reopen AMI after exception: ' . $re->getMessage()); } } } // EOF