base_dirs = array_map(fn($path) => normalize_path($path), $base_dirs); $this->build_dir = $build_dir; $this->worker_init_fn = $worker_init_fn; } function initWorker(bool $is_master_proc) { $GLOBALS['CONFIG_GLOBALS'] = $this; if($this->worker_init_fn !== null) { $fn = $this->worker_init_fn; $fn($is_master_proc); } } } enum ConfigUpdateMode : int { case Force = 1; case DetectChanged = 2; case Patch = 3; } class ConfigUpdateRequest { public ConfigUpdateMode $mode; public ?ConfigDirFiles $files; public ?string $result_file; private function __construct() {} static function force(?ConfigDirFiles $files = null) : ConfigUpdateRequest { $req = new ConfigUpdateRequest(); $req->mode = ConfigUpdateMode::Force; $req->files = $files; return $req; } static function detectChanged(string $result_file, ?ConfigDirFiles $files = null) : ConfigUpdateRequest { $req = new ConfigUpdateRequest(); $req->mode = ConfigUpdateMode::DetectChanged; $req->files = $files; $req->result_file = $result_file; return $req; } static function patch(ConfigDirFiles $files) : ConfigUpdateRequest { $req = new ConfigUpdateRequest(); $req->mode = ConfigUpdateMode::Patch; $req->files = $files; return $req; } } class ConfigManager { private ConfigGlobals $globals; private int $workers_num; private ConfigCache $cache; private ?ConfigCacheFileMap $file_map; function __construct(ConfigGlobals $globals, int $workers_num) { $this->globals = $globals; $this->workers_num = $workers_num; $this->cache = new ConfigCache($globals); $this->file_map = null; } function getGlobals() : ConfigGlobals { return $this->globals; } function getCache() : ConfigCache { return $this->cache; } function getArtifactFilesSpec() : array { return [$this->globals->base_dirs, ['.js']]; } function getFileMap() : ConfigCacheFileMap { return $this->_getFileMap(null); } private function _getFileMap(?ConfigDirFiles $files = null) : ConfigCacheFileMap { if($this->file_map === null) { $this->file_map = $this->_tryLoadMap(); if($this->file_map === null) { $this->file_map = self::_makeMap($files ?? $this->scanFiles(extension: '.js')); $this->_saveFileMap(); } } return $this->file_map; } function updateCache(ConfigUpdateRequest $req, bool $return_entries = false, bool $verbose = false) : ConfigCacheUpdateResult { config_log("Updating cache, mode '{$req->mode->name}'..."); if($req->files === null) $req->files = $this->scanFiles(extension: '.js', verbose: $verbose); $added_files = []; $removed_files = []; $this->_checkFileMap($req, $added_files, $removed_files); $affected_files = $this->_getAffectedFiles($req, $removed_files); //NOTE: at this poine taking into account only config files $affected_files->filter(fn($file) => str_ends_with($file, '.conf.js')); config_log("Affected files: {$affected_files->count()}"); $update_params = new ConfigCacheUpdateParams( globals: $this->globals, affected_files: $affected_files, verbose: $verbose, max_workers: $this->workers_num ); $update_result = self::_updateCache($update_params); $this->cache->clear(); $this->_updateFileMap($req, $affected_files, $added_files, $removed_files); if($return_entries) { foreach($affected_files as $file) { $entry = $this->cache->getOrLoadByPath($file); $update_result->affected_entries[] = $entry; } } return $update_result; } private function _updateCache(ConfigCacheUpdateParams $params) : ConfigCacheUpdateResult { if($params->affected_files->isEmpty()) return new ConfigCacheUpdateResult($params); $t = microtime(true); $result = _config_cache_update($params); if($result->errors) { $errors = array(); foreach($result->errors as $file => $error) { $errors[] = (count($errors) + 1) . ") Error in file '$file': $error"; if(count($errors) > $params->max_errors_num) break; } throw new Exception(implode("\n", $errors)); } if($params->verbose) config_log("Update cache: " . round(microtime(true) - $t,2) . " sec."); return $result; } private function _checkFileMap(ConfigUpdateRequest $req, array &$added_files, array &$removed_files) { $fs_cache_map = $this->getFileMap(); if($req->mode === ConfigUpdateMode::Force) { $added_files = $req->files->getAllFiles(); //let's rebuild the file map $fs_cache_map->init($added_files); config_log("File map init: ".count($added_files)); } else if($req->mode === ConfigUpdateMode::DetectChanged) { list($added_files, $removed_files) = $fs_cache_map->compare($req->files->getAllFiles()); config_log("File map compare, added: ".count($added_files).", removed: ".count($removed_files)); } } private function _updateFileMap(ConfigUpdateRequest $req, ConfigDirFiles $affected_files, array $added_files, array $removed_files) { $fs_cache_map = $this->getFileMap(); //TODO: traverse all affected files and update file map foreach($affected_files as $file) { $cache_entry = $this->cache->getOrLoadByPath($file); $fs_cache_map->updateDepsForEntry($cache_entry); } if($affected_files->count() > 0 || $added_files || $removed_files) { //in case of Force map was already cleared and initialized if($req->mode != ConfigUpdateMode::Force) $fs_cache_map->update($added_files, $removed_files); $this->_saveFileMap(); } } private function _getAffectedFiles(ConfigUpdateRequest $req, array $removed_files) : ConfigDirFiles { $fs_cache_map = $this->getFileMap(); $affected_files = null; if($req->mode === ConfigUpdateMode::Force) { $affected_files = $req->files; } else if($req->mode === ConfigUpdateMode::DetectChanged) { $affected_files = ConfigDirFiles::makeFor($this); foreach($req->files->getMap() as $base_dir => $files) { foreach($files as $file) { if(need_to_regen($req->result_file, [$file])) { $affected_files->add($base_dir, $file); $affected_by_file = $fs_cache_map->getAffectedFiles($file); foreach($affected_by_file as $dep) $affected_files->addFile($dep, unique: true); } } } //if there were removed files we need to rebuild affected files foreach($removed_files as $file) { if(!str_ends_with($file, '.conf.js')) { $affected_by_file = $fs_cache_map->getAffectedFiles($file); foreach($affected_by_file as $dep) $affected_files->addFile($dep, unique: true); } else { //TODO: in case config file was removed do we actually need to rebuild all configs? } } } else if($req->mode === ConfigUpdateMode::Patch) { $affected_files = ConfigDirFiles::makeFor($this); foreach($req->files as $file) { $affected_files->addFile($file, unique: true); if(!str_ends_with($file, '.conf.js')) { $affected_by_file = $fs_cache_map->getAffectedFiles($file); foreach($affected_by_file as $dep) $affected_files->addFile($dep, unique: true); } } } return $affected_files; } function parseFile(string $file) : ConfigParseResult { return config_parse($this->globals->base_dirs, $file); } function scanFiles(string $extension = '.conf.js', bool $verbose = false) : ConfigDirFiles { return config_scan_files($this->globals->base_dirs, $extension, $verbose); } private function _getMapPath() : string { return $this->globals->build_dir . '/file_map.data'; } private function _tryLoadMap() : ?ConfigCacheFileMap { if(!is_file($this->_getMapPath())) return null; return ConfigCacheFileMap::unserialize(ensure_read($this->_getMapPath())); } private static function _makeMap(ConfigDirFiles $files) : ConfigCacheFileMap { config_log("Creating file map"); $map = new ConfigCacheFileMap(); $map->init($files->getAllFiles()); return $map; } private function _saveFileMap() { $data = ConfigCacheFileMap::serialize($this->getFileMap()); config_log("Saving file map: " . kb($data)); ensure_write($this->_getMapPath(), $data); } } function config_log($msg) { echo "[CFG] $msg\n"; } function config_get_cache_path(ConfigGlobals $globals, string $file) : string { return config_get_tmp_build_path($globals, $file . '.cache'); } function config_get_cache_id_path(ConfigGlobals $globals, int $id) : string { return config_get_tmp_build_path($globals, $id . '.id'); } function config_get_cache_strid_path(ConfigGlobals $globals, string $strid) : string { return config_get_tmp_build_path($globals, $strid . '.strid'); } function config_get_cache_payload_path(ConfigGlobals $globals, string $file) : string { return config_get_tmp_build_path($globals, $file . '.payload'); } function config_load(ConfigGlobals $globals, string $conf_path, ?string $conf_dir = null) : object { $conf_dir ??= config_map_base_dir($globals->base_dirs, $conf_path); list($conf_id, $_) = config_ensure_header($conf_dir, $conf_path); if(!$conf_id) throw new Exception("Bad conf id: {$conf_id}"); $pres = config_parse(array($conf_dir), $conf_path); if($pres->error !== 0) throw new Exception("Error({$pres->error}) while loading JSON from {$conf_path}:\n" . $pres->error_descr); return config_load_from_kv_array($globals, $conf_dir, $conf_path, $pres->parsed_arr, $conf_id); } function config_load_from_kv_array( ConfigGlobals $globals, string $conf_dir, string $file, array $arr, ?int $id = null) : object { if(!isset($arr['class']) || !isset($arr['class'][0])) throw new Exception("Class is not set in file '$file'."); $id ??= config_file2id($conf_dir, $file); $cnf = null; try { list($klass, $class_id, $norm_arr) = config_apply_class_normalization($arr); if(!class_exists($klass)) throw new Exception("No such class '$klass'"); $cnf = new $klass; if(!is_a($cnf, $globals->base_class)) throw new Exception("'$klass' is not subclass of '".ltrim($globals->base_class, '\\')."'"); $cnf->import($norm_arr); } catch(Exception $e) { throw new Exception($e->getMessage() . " in file '{$file}'"/* . $e->getTraceAsString()*/); } $cnf->id = $id; $cnf->strid = config_file2strid($conf_dir, $file); return $cnf; } function config_bench_load(ConfigGlobals $globals, string $file) { $base_dir = config_map_base_dir($globals->base_dirs, $file); list($conf_id, $_) = config_ensure_header($base_dir, $file); if(!$conf_id) throw new Exception("Bad conf id: {$conf_id}"); $t = microtime(true); $parse_res = config_parse($globals->base_dirs, $file); if($parse_res->error !== 0) throw new Exception("Error({$parse_res->error}) while loading JSON from {$file}:\n" . $parse_res->error_descr); config_log("Parse: " . (microtime(true) - $t)); $t = microtime(true); $config = config_load_from_kv_array($globals, $base_dir, $file, $parse_res->parsed_arr, $conf_id); config_log("Load: " . (microtime(true) - $t)); }