dataPath = $dataPath ?? (getenv("HOME")."/.var/serverctl"); $this->stateFile = $this->dataPath . "/state.json"; $this->setupAutoEnv(); $this->setupServerEnvs(); } private function setupServerEnvs() { $file = getenv("HOME")."/.serverenvs"; if (!file_exists($file)) { $this->serverEnvs = []; return; } $parsed = parse_ini_file($file, true); $this->serverEnvs = $parsed; } private function setupAutoEnv() { $ifs = net_get_interfaces(); if ($ifs) { $dockerHostIp = null; if (array_key_exists('docker0', $ifs)) { foreach ($ifs['docker0']['unicast'] as $uc) { if ($uc['family'] == 2) { $dockerHostIp = $uc['address']; break; } } } else { // Find a usable interface foreach ($ifs as $if=>$conf) { if (!array_key_exists('unicast', $conf)) continue; foreach ($conf['unicast'] as $uc) { if ($uc['family'] == 2) { $dockerHostIp = $uc['address']; break; } } } } $this->autoEnv['DOCKER_HOST'] = $dockerHostIp; } } private function updateStateFile(string $instanceId, ?array $options) { if (!file_exists($this->stateFile)) { touch($this->stateFile); } $fd = fopen($this->stateFile, 'r+'); if (!flock($fd, LOCK_EX)) { throw new \RuntimeException("flock fail"); } // Read the state from the file, seek to end first to get size fseek($fd, 0, SEEK_END); $flen = ftell($fd); fseek($fd, 0, SEEK_SET); if ($flen > 0) { $body = fread($fd, $flen); // Parse the state $state = (array)((@json_decode($body, true))??[]); } else { $state = []; } // Update node in state if ($options === null) { unset($state[$instanceId]); } else { $state[$instanceId] = $options; } // Reencode the state, truncate the file and write it $body = json_encode($state, JSON_PRETTY_PRINT|JSON_UNESCAPED_SLASHES); fseek($fd, 0, SEEK_SET); ftruncate($fd, 0); fwrite($fd, $body); // TODO: release lock flock($fd, LOCK_UN); } /** * Start a service * * Instance options: * name: Instance name (default) * portoffset: Port number offset (0) * * @param array $service The service definition from the registry * @param array $options Instance options */ public function startService(array $service, array $options) { $args = []; $serviceName = $service['name']; $instanceName = $options['name']??'default'; $portOffset = intval($options['portoffset']??0); $temporary = $option['temporary']??false; if ($temporary) { $instanceName = sprintf("%04x%04x", rand(0,0xFFFF), rand(0,0xFFFF)); } $containerName = "sm_".$serviceName."_".$instanceName; $args[] = 'run'; $args[] = '--rm'; // remove container after run $args[] = '-d'; $args[] = '--name'; $args[] = $containerName; // Map the ports $ports = (array)($service['ports']??[]); $mappedPorts = []; foreach ($ports as $port) { $portNumber = intval($port['port']) + $portOffset; $target = array_key_exists('target',$port)?intval($port['target']):intval($port['port']); $proto = $port['proto']??"tcp"; $args[] = '-p'; $args[] = $portNumber.":".$target."/".$proto; $mappedPorts[$port['info']] = $portNumber."/".$proto; } // Get the paths to persist $volumes = (array)($service['persistence']??[]); $volumePath = $this->getServiceDataPath($service)."/".$instanceName; foreach ($volumes as $volume) { // volume { path, hint } $path = $volume['path']; $hint = $volume['hint'] ?? crc32($path); $args[] = '-v'; // add volume $args[] = $volumePath."/".$hint.":".$path; } $parsedEnv = []; // Prepared environment from serviceenvs $key = $serviceName . ":" . $instanceName; if (array_key_exists($key, $this->serverEnvs)) { foreach ($this->serverEnvs[$key] as $env=>$value) { $value = $this->expandString($value); array_push($args, "-e", sprintf("%s=%s", $env, $value)); $parsedEnv[$env] = $value; } } // Get environment $envs = (array)($service['environment']??[]); foreach ($envs as $env=>$value) { if (array_key_exists($env, $parsedEnv)) continue; $args[] = '-e'; $envval = getenv($env, true); if ($envval) $value = $envval; $value = $this->expandString($value); $args[] = sprintf("%s=%s", $env, $value); $parsedEnv[$env] = $value; } $args[] = $service['image']; $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); //echo "$ {$cmdl}\n"; exec($cmdl, $out, $ret); if ($ret != 0) { echo join("\n",$out)."\n"; throw new RuntimeException("Docker returned non-zero exit code"); } $this->updateStateFile($containerName, [ 'command' => $cmdl, 'instance' => $instanceName, 'environment' => $parsedEnv, 'service' => $service, 'ports' => $mappedPorts, 'temporary' => $temporary, ]); return [ 'ports' => $mappedPorts ]; } /** * Stop a service */ public function stopService(array $service, string $instanceName) { $args = []; $serviceName = $service['name']; $containerName = "sm_".$serviceName."_".$instanceName; $args[] = 'stop'; $args[] = $containerName; $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); //echo "$ {$cmdl}\n"; exec($cmdl, $out, $ret); if ($ret != 0) { echo join("\n",$out)."\n"; throw new RuntimeException("Docker returned non-zero exitcode"); } $this->updateStateFile($containerName, null); } public function execute(array $service, string $instanceName, array $command) { $args = []; $serviceName = $service['name']; $instanceName = $options['name']??'default'; $containerName = "sm_".$serviceName."_".$instanceName; $args[] = 'exec'; $args[] = '-it'; $args[] = $containerName; array_push($args, ...$command); $cmdl = 'docker '.join(' ',array_map('escapeshellarg', $args)); $cmdl = preg_replace_callback('<\$\{(.+?)\}>', function ($v) use ($service) { return $service['environment'][$v[1]]??null; }, $cmdl); echo "$ {$cmdl}\n"; passthru($cmdl); } /** * Get running services */ public function getRunningServices(): array { if (!file_exists($this->stateFile)) { return []; } $fd = fopen($this->stateFile, 'r+'); if (!flock($fd, LOCK_SH)) { throw new \RuntimeException("flock fail"); } // Read the state from the file, seek to end first to get size fseek($fd, 0, SEEK_END); $flen = ftell($fd); fseek($fd, 0, SEEK_SET); $ret = []; if ($flen > 0) { $body = fread($fd, $flen); // Parse the state $ret = (array)((@json_decode($body, true))??[]); } else { return []; } return $ret; } public function getServiceDataPath(array $service) { return $this->dataPath."/".$service['name']; } private function expandString(string $input): string { return preg_replace_callback('<\$\{(.+?)\}>i', function ($m) { $k = $m[1]; if (array_key_exists($k, $this->autoEnv)) { return $this->autoEnv[$k]; } return getenv($k); }, $input); } }