Replace CI PHP suite execution script with Composer commands and integrate Edge Gateway Agent stack artifacts
This commit is contained in:
@@ -177,46 +177,193 @@ final class Logger
|
||||
}
|
||||
}
|
||||
|
||||
final class LocalStateStore
|
||||
{
|
||||
private SQLite3 $db;
|
||||
|
||||
public function __construct(string $path)
|
||||
{
|
||||
if (!class_exists('SQLite3')) {
|
||||
throw new RuntimeException('The edge-agent container requires sqlite3 support.');
|
||||
}
|
||||
|
||||
$directory = dirname($path);
|
||||
if (!is_dir($directory)) {
|
||||
@mkdir($directory, 0777, true);
|
||||
}
|
||||
|
||||
$this->db = new SQLite3($path);
|
||||
$this->db->busyTimeout(5000);
|
||||
$this->db->exec('PRAGMA journal_mode = WAL;');
|
||||
$this->db->exec('PRAGMA synchronous = NORMAL;');
|
||||
$this->db->exec(
|
||||
'CREATE TABLE IF NOT EXISTS kv (
|
||||
key TEXT PRIMARY KEY,
|
||||
value_json TEXT NOT NULL,
|
||||
updated_at TEXT NOT NULL
|
||||
)'
|
||||
);
|
||||
$this->db->exec(
|
||||
'CREATE TABLE IF NOT EXISTS outbox (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
item_type TEXT NOT NULL,
|
||||
endpoint TEXT NOT NULL,
|
||||
payload_json TEXT NOT NULL,
|
||||
created_at TEXT NOT NULL
|
||||
)'
|
||||
);
|
||||
}
|
||||
|
||||
public function getJson(string $key, mixed $default = null): mixed
|
||||
{
|
||||
$statement = $this->db->prepare('SELECT value_json FROM kv WHERE key = :key LIMIT 1');
|
||||
$statement->bindValue(':key', $key, SQLITE3_TEXT);
|
||||
$result = $statement->execute();
|
||||
$row = $result instanceof SQLite3Result ? $result->fetchArray(SQLITE3_ASSOC) : false;
|
||||
if (!is_array($row) || !array_key_exists('value_json', $row)) {
|
||||
return $default;
|
||||
}
|
||||
|
||||
$decoded = json_decode((string)$row['value_json'], true);
|
||||
return json_last_error() === JSON_ERROR_NONE ? $decoded : $default;
|
||||
}
|
||||
|
||||
public function setJson(string $key, mixed $value): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'INSERT INTO kv (key, value_json, updated_at)
|
||||
VALUES (:key, :value_json, :updated_at)
|
||||
ON CONFLICT(key) DO UPDATE SET value_json = excluded.value_json, updated_at = excluded.updated_at'
|
||||
);
|
||||
$statement->bindValue(':key', $key, SQLITE3_TEXT);
|
||||
$statement->bindValue(':value_json', json_encode($value, JSON_UNESCAPED_SLASHES), SQLITE3_TEXT);
|
||||
$statement->bindValue(':updated_at', date('c'), SQLITE3_TEXT);
|
||||
$statement->execute();
|
||||
}
|
||||
|
||||
public function enqueue(string $type, string $endpoint, array $payload): void
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'INSERT INTO outbox (item_type, endpoint, payload_json, created_at)
|
||||
VALUES (:item_type, :endpoint, :payload_json, :created_at)'
|
||||
);
|
||||
$statement->bindValue(':item_type', $type, SQLITE3_TEXT);
|
||||
$statement->bindValue(':endpoint', $endpoint, SQLITE3_TEXT);
|
||||
$statement->bindValue(':payload_json', json_encode($payload, JSON_UNESCAPED_SLASHES), SQLITE3_TEXT);
|
||||
$statement->bindValue(':created_at', date('c'), SQLITE3_TEXT);
|
||||
$statement->execute();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<int,array<string,mixed>>
|
||||
*/
|
||||
public function queuedItems(int $limit = 25): array
|
||||
{
|
||||
$statement = $this->db->prepare(
|
||||
'SELECT id, item_type, endpoint, payload_json, created_at
|
||||
FROM outbox
|
||||
ORDER BY id ASC
|
||||
LIMIT :limit'
|
||||
);
|
||||
$statement->bindValue(':limit', max(1, $limit), SQLITE3_INTEGER);
|
||||
$result = $statement->execute();
|
||||
$items = [];
|
||||
if (!$result instanceof SQLite3Result) {
|
||||
return $items;
|
||||
}
|
||||
|
||||
while (($row = $result->fetchArray(SQLITE3_ASSOC)) !== false) {
|
||||
$items[] = [
|
||||
'id' => (int)$row['id'],
|
||||
'type' => (string)$row['item_type'],
|
||||
'endpoint' => (string)$row['endpoint'],
|
||||
'payload' => json_decode((string)$row['payload_json'], true) ?: [],
|
||||
'created_at' => (string)$row['created_at'],
|
||||
];
|
||||
}
|
||||
|
||||
return $items;
|
||||
}
|
||||
|
||||
public function removeOutboxItem(int $id): void
|
||||
{
|
||||
$statement = $this->db->prepare('DELETE FROM outbox WHERE id = :id');
|
||||
$statement->bindValue(':id', $id, SQLITE3_INTEGER);
|
||||
$statement->execute();
|
||||
}
|
||||
|
||||
public function outboxSummary(): array
|
||||
{
|
||||
$countResult = $this->db->querySingle('SELECT COUNT(*) FROM outbox');
|
||||
$oldestResult = $this->db->querySingle('SELECT created_at FROM outbox ORDER BY id ASC LIMIT 1');
|
||||
$lastSyncAt = $this->getJson('last_sync_at');
|
||||
|
||||
return [
|
||||
'queued' => (int)$countResult,
|
||||
'oldest_queued_at' => is_string($oldestResult) && $oldestResult !== '' ? $oldestResult : null,
|
||||
'last_replayed_at' => is_string($lastSyncAt) && $lastSyncAt !== '' ? $lastSyncAt : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
final class TruckwashEdgeAgent
|
||||
{
|
||||
private const DEFAULT_INSTALL_DIR = '/opt/truckwash-edge-agent';
|
||||
private const DEFAULT_RUNTIME_DIR = '/opt/truckwash-edge-agent/runtime';
|
||||
private const DEFAULT_STATE_DATABASE = '/opt/truckwash-edge-agent/runtime/gateway-state.sqlite';
|
||||
private const DEFAULT_WORKER_BASE_URL = 'http://lan-worker:8090';
|
||||
private const DEFAULT_UPDATE_WINDOW = '02:00-04:00';
|
||||
|
||||
private AgentConfig $config;
|
||||
private HttpJsonClient $http;
|
||||
private HttpJsonClient $workerHttp;
|
||||
private Logger $logger;
|
||||
private LocalStateStore $stateStore;
|
||||
private string $installDir;
|
||||
private string $runtimeDir;
|
||||
private string $statePath;
|
||||
private string $lastOperationSnapshotPath;
|
||||
private string $lastHeartbeatMarkerPath;
|
||||
private string $stagedUpdatePath;
|
||||
private int $lastHeartbeatAt = 0;
|
||||
private string $agentInstanceId;
|
||||
|
||||
public function __construct(string $configPath)
|
||||
{
|
||||
$this->config = AgentConfig::load($configPath);
|
||||
$this->installDir = rtrim((string)$this->config->get('installDir', self::DEFAULT_INSTALL_DIR), DIRECTORY_SEPARATOR);
|
||||
$this->runtimeDir = rtrim((string)$this->config->get('runtimeDir', self::DEFAULT_RUNTIME_DIR), DIRECTORY_SEPARATOR);
|
||||
if (!is_dir($this->runtimeDir)) {
|
||||
@mkdir($this->runtimeDir, 0777, true);
|
||||
}
|
||||
|
||||
$this->http = new HttpJsonClient((string)$this->config->get('apiUrl'));
|
||||
$this->installDir = rtrim((string)$this->config->get('installDir', dirname($configPath)), DIRECTORY_SEPARATOR);
|
||||
$this->runtimeDir = $this->installDir . DIRECTORY_SEPARATOR . 'runtime';
|
||||
@mkdir($this->runtimeDir, 0777, true);
|
||||
$this->workerHttp = new HttpJsonClient((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL));
|
||||
$this->logger = new Logger($this->runtimeDir . DIRECTORY_SEPARATOR . 'agent.log');
|
||||
$this->stateStore = new LocalStateStore((string)$this->config->get('stateDatabasePath', self::DEFAULT_STATE_DATABASE));
|
||||
$this->statePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'current-operation.json';
|
||||
$this->lastOperationSnapshotPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'last-operation.json';
|
||||
$this->lastHeartbeatMarkerPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'last-heartbeat-ok.txt';
|
||||
$this->stagedUpdatePath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'staged-update.json';
|
||||
$this->agentInstanceId = $this->ensureAgentInstanceId();
|
||||
}
|
||||
|
||||
public function run(): void
|
||||
{
|
||||
$this->logger->info('Truckwash PHP edge agent starting.');
|
||||
$this->logger->info('Truckwash compose edge-agent starting.');
|
||||
$this->recoverPreviousOperationState();
|
||||
|
||||
while (true) {
|
||||
try {
|
||||
$this->reloadConfigFromDisk();
|
||||
$this->ensureClaimed();
|
||||
$this->flushOutbox();
|
||||
$this->heartbeat();
|
||||
$processedManagementOperation = $this->processManagementOperation();
|
||||
if (!$processedManagementOperation) {
|
||||
$this->processCommandQueue();
|
||||
}
|
||||
$this->flushOutbox();
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->error($throwable->getMessage());
|
||||
sleep(2);
|
||||
@@ -232,26 +379,34 @@ final class TruckwashEdgeAgent
|
||||
return;
|
||||
}
|
||||
|
||||
$installedVersion = (string)$this->config->get('installedVersion', 'compose-php-agent-v2');
|
||||
$response = $this->http->post('/edge-agent/claim', [
|
||||
'token' => (string)$this->config->get('installToken'),
|
||||
'hostname' => gethostname() ?: 'truckwash-edge',
|
||||
'installed_version' => (string)$this->config->get('installedVersion', 'php-agent-v1'),
|
||||
'installed_version' => $installedVersion,
|
||||
'metadata' => [
|
||||
'runtime' => 'php-cli',
|
||||
'runtime' => 'compose-php',
|
||||
'runtime_mode' => 'compose',
|
||||
'php_version' => PHP_VERSION,
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
'update_window' => (string)$this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW),
|
||||
'container_health' => $this->buildContainerHealth(),
|
||||
'outbox_status' => $this->buildOutboxStatus(),
|
||||
'last_sync_at' => $this->stateStore->getJson('last_sync_at'),
|
||||
'rollback_status' => $this->readRollbackStatus(),
|
||||
'staged_version' => $this->currentStagedUpdate(),
|
||||
],
|
||||
]);
|
||||
|
||||
$payload = $response['data'] ?? [];
|
||||
$gateway = $payload['gateway'] ?? [];
|
||||
$installedVersion = (string)$this->config->get('installedVersion', 'php-agent-v1');
|
||||
$gateway = is_array($payload['gateway'] ?? null) ? (array)$payload['gateway'] : [];
|
||||
$this->config->set('gatewayId', $gateway['id'] ?? null);
|
||||
$this->config->set('agentToken', $payload['agent_token'] ?? null);
|
||||
$this->config->set('installedVersion', $installedVersion);
|
||||
$this->config->set('targetVersion', (string)($gateway['target_version'] ?? $installedVersion));
|
||||
$this->config->set('agentInstanceId', $this->agentInstanceId);
|
||||
$this->config->save();
|
||||
$this->stateStore->setJson('last_sync_at', date('c'));
|
||||
$this->logger->info('Claimed gateway ' . (string)($gateway['id'] ?? 'unknown') . '.');
|
||||
}
|
||||
|
||||
@@ -268,22 +423,38 @@ final class TruckwashEdgeAgent
|
||||
}
|
||||
|
||||
$operationState = $this->readOperationState();
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/heartbeat', [
|
||||
$payload = [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'status' => 'ONLINE',
|
||||
'hostname' => gethostname() ?: 'truckwash-edge',
|
||||
'installed_version' => (string)$this->config->get('installedVersion', 'php-agent-v1'),
|
||||
'target_version' => (string)$this->config->get('targetVersion', $this->config->get('installedVersion', 'php-agent-v1')),
|
||||
'installed_version' => (string)$this->config->get('installedVersion', 'compose-php-agent-v2'),
|
||||
'target_version' => (string)$this->config->get(
|
||||
'targetVersion',
|
||||
$this->config->get('installedVersion', 'compose-php-agent-v2')
|
||||
),
|
||||
'metadata' => array_merge([
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
'runtime' => 'compose-php',
|
||||
'runtime_mode' => 'compose',
|
||||
'current_operation' => $operationState,
|
||||
'system_metrics' => [
|
||||
'memory_usage_bytes' => memory_get_usage(true),
|
||||
'memory_peak_bytes' => memory_get_peak_usage(true),
|
||||
'load_average' => function_exists('sys_getloadavg') ? sys_getloadavg() : [],
|
||||
],
|
||||
'system_metrics' => $this->buildSystemMetrics(),
|
||||
'container_health' => $this->buildContainerHealth(),
|
||||
'outbox_status' => $this->buildOutboxStatus(),
|
||||
'last_sync_at' => $this->stateStore->getJson('last_sync_at'),
|
||||
'update_window' => (string)$this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW),
|
||||
'staged_version' => $this->currentStagedUpdate(),
|
||||
'rollback_status' => $this->readRollbackStatus(),
|
||||
], $metadata),
|
||||
]);
|
||||
];
|
||||
|
||||
$posted = $this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/heartbeat',
|
||||
$payload,
|
||||
'heartbeat'
|
||||
);
|
||||
if (!$posted) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->lastHeartbeatAt = time();
|
||||
file_put_contents($this->lastHeartbeatMarkerPath, json_encode([
|
||||
@@ -296,10 +467,20 @@ final class TruckwashEdgeAgent
|
||||
private function processCommandQueue(): void
|
||||
{
|
||||
$gatewayId = (int)$this->config->get('gatewayId');
|
||||
$response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/poll', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20),
|
||||
]);
|
||||
if ($gatewayId <= 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/poll', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20),
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->warning('Command polling failed: ' . $throwable->getMessage());
|
||||
return;
|
||||
}
|
||||
|
||||
$command = $response['data'] ?? null;
|
||||
if (!is_array($command) || empty($command['id'])) {
|
||||
return;
|
||||
@@ -317,28 +498,46 @@ final class TruckwashEdgeAgent
|
||||
default => throw new RuntimeException('Unsupported command type: ' . $type),
|
||||
};
|
||||
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
]);
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result',
|
||||
[
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
],
|
||||
'command_result'
|
||||
);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
]);
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/commands/' . $jobId . '/result',
|
||||
[
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => false,
|
||||
'error' => $throwable->getMessage(),
|
||||
],
|
||||
'command_result'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private function processManagementOperation(): bool
|
||||
{
|
||||
$gatewayId = (int)$this->config->get('gatewayId');
|
||||
$response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/next', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20),
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
]);
|
||||
if ($gatewayId <= 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/next', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'wait_seconds' => (int)$this->config->get('operationPollTimeoutSeconds', 20),
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
]);
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->warning('Operation polling failed: ' . $throwable->getMessage());
|
||||
return false;
|
||||
}
|
||||
|
||||
$operation = $response['data'] ?? null;
|
||||
if (!is_array($operation) || empty($operation['id'])) {
|
||||
return false;
|
||||
@@ -363,7 +562,7 @@ final class TruckwashEdgeAgent
|
||||
$gatewayId,
|
||||
$operationId,
|
||||
'OPERATION_AGENT_STARTED',
|
||||
'PHP edge agent started processing the operation',
|
||||
'Compose edge-agent started processing the operation',
|
||||
10,
|
||||
['type' => $type, 'agent_instance_id' => $this->agentInstanceId],
|
||||
'starting'
|
||||
@@ -377,11 +576,15 @@ final class TruckwashEdgeAgent
|
||||
default => throw new RuntimeException('Unsupported operation type: ' . $type),
|
||||
};
|
||||
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
]);
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete',
|
||||
[
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => true,
|
||||
'result' => $result,
|
||||
],
|
||||
'operation_complete'
|
||||
);
|
||||
$this->snapshotOperationState('COMPLETED', ['result' => $result]);
|
||||
$this->clearOperationState();
|
||||
} catch (Throwable $throwable) {
|
||||
@@ -393,12 +596,16 @@ final class TruckwashEdgeAgent
|
||||
'context' => ['type' => $type, 'progress' => 100, 'agent_instance_id' => $this->agentInstanceId],
|
||||
]);
|
||||
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $throwable->getMessage(),
|
||||
]);
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/complete',
|
||||
[
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'ok' => false,
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $throwable->getMessage(),
|
||||
],
|
||||
'operation_complete'
|
||||
);
|
||||
$this->snapshotOperationState('FAILED', [
|
||||
'error_code' => $errorCode,
|
||||
'error_message' => $throwable->getMessage(),
|
||||
@@ -411,13 +618,17 @@ final class TruckwashEdgeAgent
|
||||
|
||||
private function postOperationEvent(int $gatewayId, int $operationId, array $payload): void
|
||||
{
|
||||
$this->http->post('/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/events', [
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'level' => (string)($payload['level'] ?? 'INFO'),
|
||||
'code' => $payload['code'] ?? null,
|
||||
'message' => (string)($payload['message'] ?? 'Operation event'),
|
||||
'context' => is_array($payload['context'] ?? null) ? (array)$payload['context'] : [],
|
||||
]);
|
||||
$this->sendControlPlaneEvent(
|
||||
'/edge-agent/gateways/' . $gatewayId . '/operations/' . $operationId . '/events',
|
||||
[
|
||||
'agent_token' => (string)$this->config->get('agentToken'),
|
||||
'level' => (string)($payload['level'] ?? 'INFO'),
|
||||
'code' => $payload['code'] ?? null,
|
||||
'message' => (string)($payload['message'] ?? 'Operation event'),
|
||||
'context' => is_array($payload['context'] ?? null) ? (array)$payload['context'] : [],
|
||||
],
|
||||
'operation_event'
|
||||
);
|
||||
}
|
||||
|
||||
private function emitOperationProgress(
|
||||
@@ -454,34 +665,50 @@ final class TruckwashEdgeAgent
|
||||
|
||||
private function runDiscovery(): array
|
||||
{
|
||||
return ['inventory' => []];
|
||||
try {
|
||||
$response = $this->workerHttp->post('/discover', [
|
||||
'hostname' => gethostname() ?: 'truckwash-edge',
|
||||
'gateway_id' => (int)$this->config->get('gatewayId', 0),
|
||||
], 20);
|
||||
$inventory = isset($response['inventory']) && is_array($response['inventory']) ? (array)$response['inventory'] : [];
|
||||
return ['inventory' => $inventory];
|
||||
} catch (Throwable) {
|
||||
$hostname = gethostname() ?: 'truckwash-edge';
|
||||
return ['inventory' => [[
|
||||
'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10),
|
||||
'local_ip' => gethostbyname($hostname),
|
||||
'model' => 'TruckWash Edge Gateway',
|
||||
'channel_count' => 1,
|
||||
'online' => true,
|
||||
'capabilities' => [
|
||||
'local_discovery' => true,
|
||||
'relay_commands' => true,
|
||||
'gateway_management_v2' => true,
|
||||
],
|
||||
'metadata' => [
|
||||
'hostname' => $hostname,
|
||||
'runtime' => 'compose-php',
|
||||
'php_version' => PHP_VERSION,
|
||||
],
|
||||
]]];
|
||||
}
|
||||
}
|
||||
|
||||
private function runManagementDiscovery(int $gatewayId, int $operationId, array $request): array
|
||||
{
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'DISCOVERY_COLLECTING', 'Collecting local gateway inventory', 35, [], 'discovering');
|
||||
$hostname = gethostname() ?: 'truckwash-edge';
|
||||
$fallbackInventory = [[
|
||||
'device_id' => 'gateway-runtime-' . substr(sha1($hostname), 0, 10),
|
||||
'local_ip' => gethostbyname($hostname),
|
||||
'model' => 'TruckWash Edge Gateway',
|
||||
'channel_count' => 1,
|
||||
'online' => true,
|
||||
'capabilities' => [
|
||||
'local_discovery' => true,
|
||||
'relay_commands' => true,
|
||||
'gateway_management_v2' => true,
|
||||
],
|
||||
'metadata' => [
|
||||
'hostname' => $hostname,
|
||||
'runtime' => 'php-cli',
|
||||
'php_version' => PHP_VERSION,
|
||||
],
|
||||
]];
|
||||
|
||||
$this->emitOperationProgress(
|
||||
$gatewayId,
|
||||
$operationId,
|
||||
'DISCOVERY_COLLECTING',
|
||||
'Collecting local gateway inventory from lan-worker',
|
||||
35,
|
||||
[],
|
||||
'discovering'
|
||||
);
|
||||
$inventory = isset($request['inventory']) && is_array($request['inventory']) && $request['inventory'] !== []
|
||||
? (array)$request['inventory']
|
||||
: $fallbackInventory;
|
||||
: (array)($this->runDiscovery()['inventory'] ?? []);
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'DISCOVERY_COMPLETED', 'Inventory collected', 90, [
|
||||
'device_count' => count($inventory),
|
||||
], 'finishing');
|
||||
@@ -496,59 +723,158 @@ final class TruckwashEdgeAgent
|
||||
throw new RuntimeException('Update request is missing target version');
|
||||
}
|
||||
|
||||
$agentArtifactUrl = trim((string)($request['artifactUrl'] ?? ''));
|
||||
$serviceUnitUrl = trim((string)($request['serviceUnitUrl'] ?? ''));
|
||||
$agentArtifactSha = trim((string)($request['artifactSha256'] ?? ''));
|
||||
$serviceUnitSha = trim((string)($request['serviceUnitSha256'] ?? ''));
|
||||
$updateWindow = trim((string)($request['updateWindow'] ?? $this->config->get('updateWindow', self::DEFAULT_UPDATE_WINDOW)));
|
||||
if ($updateWindow === '') {
|
||||
$updateWindow = self::DEFAULT_UPDATE_WINDOW;
|
||||
}
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_VALIDATED', 'Validated update request', 20, [
|
||||
$requiredArtifacts = [
|
||||
[
|
||||
'url' => (string)($request['artifactUrl'] ?? ''),
|
||||
'sha256' => (string)($request['artifactSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'agent.php',
|
||||
'label' => 'edge-agent runtime',
|
||||
'progress' => 35,
|
||||
'code' => 'UPDATE_DOWNLOAD_AGENT',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['lanWorkerArtifactUrl'] ?? ''),
|
||||
'sha256' => (string)($request['lanWorkerArtifactSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'lan-worker.php',
|
||||
'label' => 'lan-worker runtime',
|
||||
'progress' => 45,
|
||||
'code' => 'UPDATE_DOWNLOAD_WORKER',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['composeFileUrl'] ?? ''),
|
||||
'sha256' => (string)($request['composeFileSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'docker-compose.gateway.yml',
|
||||
'label' => 'compose stack',
|
||||
'progress' => 55,
|
||||
'code' => 'UPDATE_DOWNLOAD_COMPOSE',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['edgeAgentDockerfileUrl'] ?? ''),
|
||||
'sha256' => (string)($request['edgeAgentDockerfileSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'Dockerfile.edge-agent',
|
||||
'label' => 'edge-agent Dockerfile',
|
||||
'progress' => 60,
|
||||
'code' => 'UPDATE_DOWNLOAD_EDGE_DOCKERFILE',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['lanWorkerDockerfileUrl'] ?? ''),
|
||||
'sha256' => (string)($request['lanWorkerDockerfileSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'Dockerfile.lan-worker',
|
||||
'label' => 'lan-worker Dockerfile',
|
||||
'progress' => 65,
|
||||
'code' => 'UPDATE_DOWNLOAD_WORKER_DOCKERFILE',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['launcherScriptUrl'] ?? ''),
|
||||
'sha256' => (string)($request['launcherScriptSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'gateway-launcher.sh',
|
||||
'label' => 'gateway launcher',
|
||||
'progress' => 72,
|
||||
'code' => 'UPDATE_DOWNLOAD_LAUNCHER',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['stackServiceUnitUrl'] ?? ''),
|
||||
'sha256' => (string)($request['stackServiceUnitSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-gateway-stack.service',
|
||||
'label' => 'compose systemd unit',
|
||||
'progress' => 80,
|
||||
'code' => 'UPDATE_DOWNLOAD_STACK_SERVICE',
|
||||
],
|
||||
[
|
||||
'url' => (string)($request['serviceUnitUrl'] ?? ''),
|
||||
'sha256' => (string)($request['serviceUnitSha256'] ?? ''),
|
||||
'path' => $this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-agent.service',
|
||||
'label' => 'compatibility systemd unit',
|
||||
'progress' => 84,
|
||||
'code' => 'UPDATE_DOWNLOAD_LEGACY_SERVICE',
|
||||
],
|
||||
];
|
||||
|
||||
foreach (['composeFileUrl', 'launcherScriptUrl', 'stackServiceUnitUrl'] as $requiredKey) {
|
||||
if (trim((string)($request[$requiredKey] ?? '')) === '') {
|
||||
throw new RuntimeException('Update request is missing compose artifact ' . $requiredKey);
|
||||
}
|
||||
}
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_VALIDATED', 'Validated compose update request', 20, [
|
||||
'target_version' => $targetVersion,
|
||||
'update_window' => $updateWindow,
|
||||
], 'validating');
|
||||
|
||||
$changedFiles = [];
|
||||
if ($agentArtifactUrl !== '') {
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_DOWNLOAD_AGENT', 'Downloading PHP edge agent artifact', 40, [
|
||||
'url' => $agentArtifactUrl,
|
||||
], 'downloading-agent');
|
||||
foreach ($requiredArtifacts as $artifact) {
|
||||
if ($artifact['url'] === '') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$this->emitOperationProgress(
|
||||
$gatewayId,
|
||||
$operationId,
|
||||
(string)$artifact['code'],
|
||||
'Downloading ' . (string)$artifact['label'],
|
||||
(int)$artifact['progress'],
|
||||
['url' => (string)$artifact['url']],
|
||||
'downloading'
|
||||
);
|
||||
$changedFiles[] = $this->downloadToFileAtomic(
|
||||
$agentArtifactUrl,
|
||||
$this->installDir . DIRECTORY_SEPARATOR . 'agent.php',
|
||||
$agentArtifactSha
|
||||
(string)$artifact['url'],
|
||||
(string)$artifact['path'],
|
||||
(string)$artifact['sha256']
|
||||
);
|
||||
}
|
||||
|
||||
if ($serviceUnitUrl !== '') {
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_DOWNLOAD_SERVICE', 'Downloading service unit', 65, [
|
||||
'url' => $serviceUnitUrl,
|
||||
], 'downloading-service');
|
||||
$changedFiles[] = $this->downloadToFileAtomic(
|
||||
$serviceUnitUrl,
|
||||
$this->installDir . DIRECTORY_SEPARATOR . 'truckwash-edge-agent.service',
|
||||
$serviceUnitSha
|
||||
);
|
||||
}
|
||||
$this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'agent.php');
|
||||
$this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'lan-worker.php');
|
||||
$this->ensureExecutable($this->installDir . DIRECTORY_SEPARATOR . 'gateway-launcher.sh');
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_WRITE_CONFIG', 'Writing staged update metadata', 85, [], 'writing-config');
|
||||
$this->config->set('installedVersion', $targetVersion);
|
||||
$this->config->set('targetVersion', $targetVersion);
|
||||
$this->config->set('lastStagedUpdate', [
|
||||
$stagedAt = date('c');
|
||||
$stagedUpdate = [
|
||||
'target_version' => $targetVersion,
|
||||
'staged_at' => date('c'),
|
||||
'staged_at' => $stagedAt,
|
||||
'apply_after' => $this->nextUpdateWindowStartIso($updateWindow),
|
||||
'status' => 'STAGED',
|
||||
'update_window' => $updateWindow,
|
||||
'compose_project_name' => (string)($request['composeProjectName'] ?? $this->config->get('composeProjectName', 'truckwash-edge-gateway')),
|
||||
'stack_service_name' => (string)($request['stackServiceName'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')),
|
||||
'changed_files' => $changedFiles,
|
||||
]);
|
||||
$this->config->save();
|
||||
];
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGED', 'Artifacts staged successfully', 95, [
|
||||
'changed_files' => $changedFiles,
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGE_METADATA', 'Writing staged update metadata', 90, [], 'writing-config');
|
||||
$this->config->set('targetVersion', $targetVersion);
|
||||
$this->config->set('lastStagedUpdate', $stagedUpdate);
|
||||
$this->config->save();
|
||||
$this->stateStore->setJson('staged_update', $stagedUpdate);
|
||||
$this->stateStore->setJson('rollback_status', [
|
||||
'state' => 'IDLE',
|
||||
'reason' => null,
|
||||
'rolled_back_to' => null,
|
||||
'at' => null,
|
||||
]);
|
||||
file_put_contents($this->stagedUpdatePath, json_encode($stagedUpdate, JSON_PRETTY_PRINT | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UPDATE_STAGED', 'Compose rollout staged for the maintenance window', 96, [
|
||||
'target_version' => $targetVersion,
|
||||
'apply_after' => $stagedUpdate['apply_after'],
|
||||
], 'staged');
|
||||
|
||||
return [
|
||||
'installed_version' => $targetVersion,
|
||||
'applied' => false,
|
||||
'installed_version' => (string)$this->config->get('installedVersion', 'compose-php-agent-v2'),
|
||||
'staged_version' => $targetVersion,
|
||||
'target_version' => $targetVersion,
|
||||
'staged_at' => $stagedAt,
|
||||
'apply_after' => $stagedUpdate['apply_after'],
|
||||
'update_window' => $updateWindow,
|
||||
'restart_required' => true,
|
||||
'service_name' => (string)$this->config->get('serviceName', 'truckwash-edge-agent.service'),
|
||||
'service_name' => (string)($request['stackServiceName'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')),
|
||||
'changed_files' => $changedFiles,
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
'rollback_status' => $this->readRollbackStatus(),
|
||||
];
|
||||
}
|
||||
|
||||
@@ -557,7 +883,7 @@ final class TruckwashEdgeAgent
|
||||
$this->emitOperationProgress($gatewayId, $operationId, 'UNINSTALL_PREPARE', 'Preparing uninstall manifest', 40, [], 'preparing-uninstall');
|
||||
$manifestPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'uninstall-plan.json';
|
||||
$manifest = [
|
||||
'service_name' => (string)($request['service_name'] ?? $this->config->get('serviceName', 'truckwash-edge-agent.service')),
|
||||
'service_name' => (string)($request['service_name'] ?? $this->config->get('stackServiceName', 'truckwash-edge-gateway-stack.service')),
|
||||
'install_dir' => (string)($request['install_dir'] ?? $this->installDir),
|
||||
'generated_at' => date('c'),
|
||||
'agent_instance_id' => $this->agentInstanceId,
|
||||
@@ -584,7 +910,15 @@ final class TruckwashEdgeAgent
|
||||
{
|
||||
$localIp = (string)($request['localIp'] ?? $request['local_ip'] ?? '');
|
||||
$channel = (int)($request['channel'] ?? 0);
|
||||
return $this->fetchShellyState($localIp, $channel);
|
||||
|
||||
try {
|
||||
return $this->workerHttp->post('/relay/status', [
|
||||
'local_ip' => $localIp,
|
||||
'channel' => $channel,
|
||||
], 8) ?? [];
|
||||
} catch (Throwable) {
|
||||
return $this->fetchShellyState($localIp, $channel);
|
||||
}
|
||||
}
|
||||
|
||||
private function switchRelay(array $request): array
|
||||
@@ -593,15 +927,23 @@ final class TruckwashEdgeAgent
|
||||
$channel = (int)($request['channel'] ?? 0);
|
||||
$on = (bool)($request['on'] ?? false);
|
||||
|
||||
$rpcUrl = sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false');
|
||||
try {
|
||||
$this->http->getJson($rpcUrl, 8);
|
||||
return $this->workerHttp->post('/relay/switch', [
|
||||
'local_ip' => $localIp,
|
||||
'channel' => $channel,
|
||||
'on' => $on,
|
||||
], 8) ?? [];
|
||||
} catch (Throwable) {
|
||||
$legacyUrl = sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off');
|
||||
$this->http->getJson($legacyUrl, 8);
|
||||
}
|
||||
$rpcUrl = sprintf('http://%s/rpc/Switch.Set?id=%d&on=%s', $localIp, $channel, $on ? 'true' : 'false');
|
||||
try {
|
||||
$this->http->getJson($rpcUrl, 8);
|
||||
} catch (Throwable) {
|
||||
$legacyUrl = sprintf('http://%s/relay/%d?turn=%s', $localIp, $channel, $on ? 'on' : 'off');
|
||||
$this->http->getJson($legacyUrl, 8);
|
||||
}
|
||||
|
||||
return $this->fetchShellyState($localIp, $channel);
|
||||
return $this->fetchShellyState($localIp, $channel);
|
||||
}
|
||||
}
|
||||
|
||||
private function downloadToFileAtomic(string $url, string $path, string $expectedSha256 = ''): array
|
||||
@@ -690,6 +1032,153 @@ final class TruckwashEdgeAgent
|
||||
}
|
||||
}
|
||||
|
||||
private function buildSystemMetrics(): array
|
||||
{
|
||||
$diskTotal = @disk_total_space($this->installDir);
|
||||
$diskFree = @disk_free_space($this->installDir);
|
||||
$diskUsed = (is_numeric($diskTotal) && is_numeric($diskFree)) ? ((float)$diskTotal - (float)$diskFree) : null;
|
||||
$diskUsagePct = ($diskTotal && $diskUsed !== null && $diskTotal > 0)
|
||||
? (int)round(($diskUsed / (float)$diskTotal) * 100)
|
||||
: null;
|
||||
$memoryLimitBytes = $this->iniBytes((string)ini_get('memory_limit'));
|
||||
$memoryUsage = memory_get_usage(true);
|
||||
$memoryUsagePct = ($memoryLimitBytes !== null && $memoryLimitBytes > 0)
|
||||
? (int)round(($memoryUsage / $memoryLimitBytes) * 100)
|
||||
: null;
|
||||
$loadAverage = function_exists('sys_getloadavg') ? sys_getloadavg() : [];
|
||||
|
||||
return [
|
||||
'memory_usage_bytes' => $memoryUsage,
|
||||
'memory_peak_bytes' => memory_get_peak_usage(true),
|
||||
'memory_usage_pct' => $memoryUsagePct,
|
||||
'cpu_usage_pct' => is_array($loadAverage) && isset($loadAverage[0]) ? max(0, (int)round((float)$loadAverage[0] * 100)) : null,
|
||||
'load_average' => $loadAverage,
|
||||
'disk_usage_pct' => $diskUsagePct,
|
||||
'disk_used_bytes' => $diskUsed,
|
||||
'disk_total_bytes' => is_numeric($diskTotal) ? (int)$diskTotal : null,
|
||||
'disk_mount' => $this->installDir,
|
||||
'latency_ms' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildContainerHealth(): array
|
||||
{
|
||||
$services = [[
|
||||
'name' => 'edge-agent',
|
||||
'status' => 'healthy',
|
||||
'updated_at' => date('c'),
|
||||
]];
|
||||
|
||||
try {
|
||||
$workerHealth = $this->workerHttp->getJson(rtrim((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL), '/') . '/health', 5);
|
||||
$services[] = [
|
||||
'name' => 'lan-worker',
|
||||
'status' => (string)($workerHealth['status'] ?? 'healthy'),
|
||||
'updated_at' => (string)($workerHealth['timestamp'] ?? date('c')),
|
||||
];
|
||||
} catch (Throwable $throwable) {
|
||||
$services[] = [
|
||||
'name' => 'lan-worker',
|
||||
'status' => 'degraded',
|
||||
'updated_at' => date('c'),
|
||||
'error' => $throwable->getMessage(),
|
||||
];
|
||||
}
|
||||
|
||||
$healthyCount = count(array_filter($services, static fn(array $service): bool => (string)($service['status'] ?? '') === 'healthy'));
|
||||
$state = $healthyCount === count($services) ? 'ONLINE' : 'DEGRADED';
|
||||
|
||||
return [
|
||||
'state' => $state,
|
||||
'summary' => sprintf('%d/%d containers healthy', $healthyCount, count($services)),
|
||||
'services' => $services,
|
||||
];
|
||||
}
|
||||
|
||||
private function buildOutboxStatus(): array
|
||||
{
|
||||
$summary = $this->stateStore->outboxSummary();
|
||||
$queued = (int)($summary['queued'] ?? 0);
|
||||
return [
|
||||
'state' => $queued > 0 ? 'QUEUED' : 'IN_SYNC',
|
||||
'queued' => $queued,
|
||||
'oldest_queued_at' => $summary['oldest_queued_at'] ?? null,
|
||||
'last_replayed_at' => $summary['last_replayed_at'] ?? null,
|
||||
'summary' => $queued > 0 ? sprintf('%d outbound items queued', $queued) : 'Outbox is empty',
|
||||
];
|
||||
}
|
||||
|
||||
private function flushOutbox(): void
|
||||
{
|
||||
$items = $this->stateStore->queuedItems(25);
|
||||
foreach ($items as $item) {
|
||||
try {
|
||||
$this->http->post((string)$item['endpoint'], is_array($item['payload'] ?? null) ? (array)$item['payload'] : [], 20);
|
||||
$this->stateStore->removeOutboxItem((int)$item['id']);
|
||||
$this->stateStore->setJson('last_sync_at', date('c'));
|
||||
} catch (Throwable $throwable) {
|
||||
$this->logger->warning('Outbox replay blocked on ' . (string)$item['type'] . ': ' . $throwable->getMessage());
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function sendControlPlaneEvent(string $endpoint, array $payload, string $type): bool
|
||||
{
|
||||
try {
|
||||
$this->http->post($endpoint, $payload, 20);
|
||||
$this->stateStore->setJson('last_sync_at', date('c'));
|
||||
return true;
|
||||
} catch (Throwable $throwable) {
|
||||
$this->stateStore->enqueue($type, $endpoint, $payload);
|
||||
$this->logger->warning('Queued ' . $type . ' to local outbox after transport failure: ' . $throwable->getMessage());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private function readRollbackStatus(): array
|
||||
{
|
||||
$rollbackPath = $this->runtimeDir . DIRECTORY_SEPARATOR . 'rollback-status.json';
|
||||
if (is_file($rollbackPath)) {
|
||||
$decoded = json_decode((string)file_get_contents($rollbackPath), true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$rollback = $this->stateStore->getJson('rollback_status', null);
|
||||
if (is_array($rollback)) {
|
||||
return $rollback;
|
||||
}
|
||||
|
||||
return [
|
||||
'state' => 'IDLE',
|
||||
'reason' => null,
|
||||
'rolled_back_to' => null,
|
||||
'at' => null,
|
||||
];
|
||||
}
|
||||
|
||||
private function currentStagedUpdate(): ?array
|
||||
{
|
||||
if (is_file($this->stagedUpdatePath)) {
|
||||
$decoded = json_decode((string)file_get_contents($this->stagedUpdatePath), true);
|
||||
if (is_array($decoded)) {
|
||||
return $decoded;
|
||||
}
|
||||
}
|
||||
|
||||
$staged = $this->stateStore->getJson('staged_update', null);
|
||||
return is_array($staged) ? $staged : null;
|
||||
}
|
||||
|
||||
private function ensureExecutable(string $path): void
|
||||
{
|
||||
if (is_file($path)) {
|
||||
@chmod($path, 0755);
|
||||
}
|
||||
}
|
||||
|
||||
private function ensureAgentInstanceId(): string
|
||||
{
|
||||
$configured = trim((string)$this->config->get('agentInstanceId', ''));
|
||||
@@ -752,19 +1241,61 @@ final class TruckwashEdgeAgent
|
||||
$decoded = json_decode((string)file_get_contents($this->statePath), true);
|
||||
return is_array($decoded) ? $decoded : null;
|
||||
}
|
||||
|
||||
private function nextUpdateWindowStartIso(string $window): string
|
||||
{
|
||||
$parts = explode('-', $window, 2);
|
||||
$start = trim((string)($parts[0] ?? '02:00'));
|
||||
if (!preg_match('/^\d{2}:\d{2}$/', $start)) {
|
||||
$start = '02:00';
|
||||
}
|
||||
|
||||
$now = new DateTimeImmutable('now');
|
||||
[$hour, $minute] = array_map('intval', explode(':', $start));
|
||||
$candidate = $now->setTime($hour, $minute, 0);
|
||||
if ($candidate <= $now) {
|
||||
$candidate = $candidate->modify('+1 day');
|
||||
}
|
||||
|
||||
return $candidate->format(DateTimeInterface::ATOM);
|
||||
}
|
||||
|
||||
private function reloadConfigFromDisk(): void
|
||||
{
|
||||
$reloaded = AgentConfig::load($this->config->path);
|
||||
$this->config = $reloaded;
|
||||
$this->workerHttp = new HttpJsonClient((string)$this->config->get('workerBaseUrl', self::DEFAULT_WORKER_BASE_URL));
|
||||
}
|
||||
|
||||
private function iniBytes(string $value): ?int
|
||||
{
|
||||
$normalized = trim(strtolower($value));
|
||||
if ($normalized === '' || $normalized === '-1') {
|
||||
return null;
|
||||
}
|
||||
|
||||
$unit = substr($normalized, -1);
|
||||
$number = (float)$normalized;
|
||||
return match ($unit) {
|
||||
'g' => (int)round($number * 1024 * 1024 * 1024),
|
||||
'm' => (int)round($number * 1024 * 1024),
|
||||
'k' => (int)round($number * 1024),
|
||||
default => is_numeric($normalized) ? (int)$normalized : null,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
$configPath = null;
|
||||
foreach ($argv as $index => $argument) {
|
||||
if ($argument === '--config' && isset($argv[$index + 1])) {
|
||||
$configPath = $argv[$index + 1];
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!is_string($configPath) || trim($configPath) === '') {
|
||||
fwrite(STDERR, "Usage: agent.php --config /path/to/config.json\n");
|
||||
if ($configPath === null) {
|
||||
fwrite(STDERR, "Usage: php agent.php --config /path/to/config.json\n");
|
||||
exit(1);
|
||||
}
|
||||
|
||||
(new TruckwashEdgeAgent($configPath))->run();
|
||||
$agent = new TruckwashEdgeAgent($configPath);
|
||||
$agent->run();
|
||||
|
||||
Reference in New Issue
Block a user