Commit ac162632 authored by Jordan Breton's avatar Jordan Breton

Changed : WFWGlobalConf & removed cli/update

parent fa09a993
<?php
namespace wfw\cli\updator;
/**
* Gestionnaire de mise à jour
*/
interface IUpdator {
/**
* Vérifie la disponibilité d'une mise à jour
* @return array Liste des mises à jour à installer
*/
public function check():array;
/**
* Telecharge les mises à jour disponibles
* @param null|string $dest Destination des mises à jour
* @return void
*/
public function download(?string $dest=null):void;
/**
* @param null|string $source Dossier dans lequel se trouvent les mises à jour à installer.
*/
public function install(?string $source=null):void;
}
\ No newline at end of file
<?php
namespace wfw\cli\updator;
use wfw\cli\updator\conf\UpdatorConf;
use wfw\cli\updator\errors\UpdatorFailure;
/**
* Execute les différent manoeuvre pour les mises à jour.
*/
final class Updator implements IUpdator {
/** @var UpdatorConf $_conf */
private $_conf;
/**
* Updator constructor.
*
* @param UpdatorConf $conf Configurations
*/
public function __construct(UpdatorConf $conf) {
$this->_conf = $conf;
}
/**
* Vérifie la disponibilité d'une mise à jour
*
* @return array Liste des mises à jour à installer
*/
public function check(): array {
$crl = curl_init($this->_conf->getCheckUrl());
curl_setopt($crl,CURLOPT_RETURNTRANSFER,true);
$res = json_decode(curl_exec($crl),true)??[];
curl_close($crl);
$this->_conf->setLastCheck();
return $res;
}
/**
* Telecharge les mises à jour disponibles
*
* @param null|string $dest Destination des mises à jour
* @return void
*/
public function download(?string $dest = null): void {
$path = $this->_conf->getWorkingDir();
if($dest) $path = $dest;
if(is_dir("$path/updates")) $this->exec("rm -rf \"$path/updates\"");
if(is_file("$path/updates.zip")) unlink("$path/updates.zip");
$fp = fopen("$path/updates.zip",'w+');
if($fp === false)
throw new UpdatorFailure("Cann't open file $path/updates.zip in w+ mode !");
$ch = curl_init($this->_conf->getDownloadUrl());
curl_setopt($ch, CURLOPT_FILE, $fp);
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
curl_exec($ch);
if(curl_errno($ch)) throw new UpdatorFailure(curl_error($ch));
$statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if($statusCode !== 200)
throw new UpdatorFailure(
"Cannot download update from ".$this->_conf->getDownloadUrl()
." server responded with $statusCode status code."
);
}
/**
* @param null|string $source Dossier dans lequel se trouvent les mises à jour à installer.
*/
public function install(?string $source = null): void {
$path = $this->_conf->getWorkingDir()."/updates.zip";
if($source) $path = $source;
if(!is_file($path)) throw new UpdatorFailure("$path : no such file or directory !");
$this->exec("unzip \"$path\" -d \"".dirname($path).'"');
$updatesFolder = str_replace('.zip','',$path);
$updates = array_diff(scandir($updatesFolder), ['.','..']);
natcasesort($updates);
foreach($updates as $update){
$this->installUpdate("$updatesFolder/$update");
}
$this->_conf->setLastUpdate(array_pop($updates));
$this->exec('find '.$this->_conf->getWorkingDir().' -mindepth 1 -delete');
}
/**
* Crée un backup pour le projet courant
* @param string ...$opts options de backup (site dbs engine daemons cli
*/
protected function makeBackup(string ...$opts):void{
if($this->_conf->allowBackups()){
$this->exec("wfw ".$this->_conf->getProject()." backup -make ".implode(' ',$opts));
}
}
/**
* Envoie une commande au gestionnaire de services wfw
* @param string $cmd
* @param string ...$daemons
*/
protected function sctl(string $cmd,string ...$daemons):void{
if($this->_conf->allowSCTL()){
if(!$this->_conf->hasGlobalSCTL())
$this->exec("wfw ".$this->_conf->getProject()." service $cmd ".implode(' ',$daemons));
else
$this->exec("wfw self service $cmd ".implode(' ',$daemons));
}
}
/**
* @param string $dir Dossier parent contenant les fichiers de la mise à jour.
*/
private function installUpdate(string $dir):void{
$mainScript = "$dir/scripts/main.php";
if(file_exists($mainScript)){
try{
require_once($mainScript);
}catch(\Exception $e){
error_log($e.PHP_EOL,3,$this->_conf->getWorkingDir()."/errors.log");
}catch(\Error $e){
error_log($e.PHP_EOL,3,$this->_conf->getWorkingDir()."/errors.log");
}
}
$dirs = array_diff(scandir("$dir/src"),['.','..']);
foreach ($dirs as $d){
$this->exec("cp -R \"$dir/src/$d\" \"".dirname(__DIR__,2)."\"");
}
}
/**
* @param string $cmd Commande à executer
* @throws BackupFailure
*/
private function exec(string $cmd):void{
$outputs = []; $res = null;
exec($cmd,$outputs,$res);
if($res !== 0)
throw new UpdatorFailure(
"Error trying to exec '$cmd'".
" code $res, outputs : ".implode("\n",$outputs)
);
}
}
\ No newline at end of file
#!/usr/bin/php -q
<?php
use wfw\cli\updator\conf\UpdatorConf;
use wfw\cli\updator\Updator;
use wfw\engine\lib\cli\argv\ArgvOpt;
use wfw\engine\lib\cli\argv\ArgvOptMap;
use wfw\engine\lib\cli\argv\ArgvParser;
use wfw\engine\lib\cli\argv\ArgvReader;
require_once dirname(__FILE__,2)."/init.environment.php";
$argvReader = new ArgvReader(new ArgvParser(new ArgvOptMap([
new ArgvOpt('-check',"Obtient la liste des mises à jour à installer",
0,null,true),
new ArgvOpt('-download',"Télécharge les mises à jour disponibles",
0,null,true),
new ArgvOpt('-install',"Installe les mises à jour contenue dans ./downloads ou dans -path",
null,null,true),
new ArgvOpt('-update',"Télécharge et installe les mises à jours disponibles",
0,null,true),
new ArgvOpt('--debug',"Affiche plus de détails sur les erreurs",
0,null,true)
])),$argv);
try{
throw new Exception("For now this functionnality is deprecated and should not be used anymore !");
$confs = new UpdatorConf(
dirname(__DIR__,2)."/engine/config/conf.json",
dirname(__DIR__,2)."/site/config/conf.json"
);
$updator = new Updator($confs);
$check = false; $download = false; $install = false;
if($argvReader->exists('-update') || count($argv) === 1){
$check = true; $download = true; $install = true;
}else{
$check = $argvReader->exists('-check');
$download = $argvReader->exists('-download');
$install = $argvReader->exists('-install');
}
if($check){
$availables = $updator->check();
if(count($availables) > 0){
fwrite(STDOUT,"\e[33mSome updates have to be installed :\e[0m\n");
foreach($availables as $update){
fwrite(STDOUT,"$update\n");
}
}else{
fwrite(STDOUT,"\e[32mNo update available.\e[0m\n");
}
}
if($download){
$updator->download();
fwrite(STDOUT,"\e[32mUpdates downloaded, ready to install.\e[0m\n");
}
if($install){
$updator->install();
fwrite(STDOUT,"\e[32mUpdated !\e[0m\n");
}
}catch(\InvalidArgumentException $e){
fwrite(STDOUT,"\e[33mWFW_updator WRONG_USAGE\e[0m : {$e->getMessage()}".PHP_EOL);
exit(1);
}catch(\Exception $e){
if($argvReader->exists('--debug'))
fwrite(STDOUT,"\e[31mWFW_updator ERROR\e[0m : ".PHP_EOL."$e".PHP_EOL);
else
fwrite(
STDOUT,
"\e[31mWFW_updator ERROR\e[0m (try --debug for more) : {$e->getMessage()}".PHP_EOL
);
exit(2);
}
exit(0);
\ No newline at end of file
<?php
namespace wfw\cli\updator\conf;
use stdClass;
use wfw\engine\core\conf\FileBasedConf;
use wfw\engine\core\conf\io\adapters\JSONConfIOAdapter;
use wfw\engine\core\conf\MemoryConf;
use wfw\engine\lib\PHP\types\PHPString;
/**
* COnfiguration de l'updator
*/
final class UpdatorConf {
private const WORKING_DIR = "working_dir";
private const SERVER = "server/addr";
private const CHECK = "server/check";
private const DOWNLOAD = "server/download";
private const SCTL = "permissions/allow_sctl";
private const BACKUPS = "permissions/allow_backups";
private const GLOBAL_SCTL = "global_sctl";
private const PROJECT = "project";
/** @var FileBasedConf $_conf */
private $_conf;
/** @var string $_basepath */
private $_basepath;
/** @var FileBasedConf $_engineConf */
private $_engineConf;
/** @var FileBasedConf $_siteConf */
private $_siteConf;
/**
* BackupManagerConf constructor.
*
* @param string $engineConf Chemin d'accés aux configurations par defaut
* @param null|string $siteConf Chemin d'accés aux configurations du projet
* @param string $basepath Chemin de base pour la résolution de chemins relatifs
* @throws \InvalidArgumentException
*/
public function __construct(string $engineConf,?string $siteConf, ?string $basepath = null) {
if(is_null($basepath)) $basepath = dirname(__DIR__,2);
$this->_basepath = $basepath;
$confIo = new JSONConfIOAdapter();
$this->_engineConf = new FileBasedConf($engineConf,$confIo);
$conf = new MemoryConf(new stdClass());
$conf->merge($this->_engineConf);
if(file_exists($siteConf)){
$this->_siteConf = new FileBasedConf($siteConf,$confIo);
$conf->merge($this->_siteConf);
}
$backupConf = $conf->get('server/cli/updator');
if(is_null($backupConf))
throw new \InvalidArgumentException("Config key server/cli/updator not found");
if(!(new PHPString($backupConf))->startBy("/"))
$backupConf = "$basepath/$backupConf";
if(!file_exists($backupConf))
throw new \InvalidArgumentException("Conf file $backupConf not found");
$this->_conf = new FileBasedConf($backupConf,$confIo);
}
/**
* @return string Dossier de travail par défaut
*/
public function getWorkingDir():string{
$path = $this->_conf->getString(self::WORKING_DIR);
if(!(new PHPString($path))->startBy('/'))
$path = "$this->_basepath/$path";
return $path;
}
/**
* @return string retourne l'adresse du serveur de mise à jour
*/
public function getUpdateServer():string{
return $this->_conf->getString(self::SERVER);
}
/**
* @return string
*/
public function getCheckUrl():string{
return $this->getUpdateServer().$this->_conf->getString(self::CHECK);
}
/**
* @return string
*/
public function getDownloadUrl():string{
return $this->getUpdateServer().$this->_conf->getString(self::DOWNLOAD)
."?current=".urlencode($this->_engineConf->getString("server/framework/version"));
}
/**
* Met à jour la date de derniere verification dans le fichier de configuration engine.
*/
public function setLastCheck():void{
$this->_engineConf->set("server/framework/last_check",microtime(true));
$this->_engineConf->save();
}
/**
* Met à jour la date de derniere mise à jour et la version du framework dans le fichier
* engine.
* @param string $version Derniéer version installée
*/
public function setLastUpdate(string $version):void{
$this->_engineConf->set("server/framework/last_update",microtime(true));
$this->_engineConf->set("server/framework/version",$version);
$this->_engineConf->save();
}
/**
* Permet de savoir si ce projet authorise le contrôle de ses services par l'assistant de mise
* à jour.
* @return bool
*/
public function allowSCTL():bool{
return $this->_conf->getBoolean(self::SCTL);
}
/**
* Permet de savoir si le systeme courant dispose d'un systeme de gestion de services global
* @return bool
*/
public function hasGlobalSCTL():bool{
return $this->_conf->getBoolean(self::GLOBAL_SCTL);
}
/**
* Permet de savoir si ce projet authorise la création de backups par l'assistant de mises
* à jour
* @return bool
*/
public function allowBackups():bool{
return $this->_conf->getBoolean(self::BACKUPS);
}
/**
* Retourne le nom du projet utilisé pour les commandes sur wfw.
* @return string
*/
public function getProject():string{
return $this->_conf->getString(self::PROJECT);
}
}
\ No newline at end of file
{
"project" : "self",
"global_sctl" : true,
"working_dir" : "updator/downloads",
"server" : {
"addr" : "http://192.168.1.12:80/update",
"check" : "/check.php",
"download" : "/download.php"
},
"permissions" : {
"allow_sctl" : true,
"allow_backups" : true
}
}
\ No newline at end of file
<?php
namespace wfw\cli\updator\errors;
/**
* Exception levée par l'updator
*/
class UpdatorFailure extends \Exception{}
\ No newline at end of file
#!/usr/bin/php -q
<?php
use wfw\engine\core\conf\WFW;
use wfw\engine\lib\cli\argv\ArgvOpt;
use wfw\engine\lib\cli\argv\ArgvOptMap;
use wfw\engine\lib\cli\argv\ArgvParser;
use wfw\engine\lib\cli\argv\ArgvReader;
require_once(dirname(__DIR__)."/init.environment.php");
$argvReader = new ArgvReader(new ArgvParser(new ArgvOptMap([
new ArgvOpt("-update","Specify that the clean is for update. If not specified, the clean will be applyed for import.",0,null,true),
new ArgvOpt("-list","Display only the folder list that will be cleaned up, without performing cleaning step",0,null,true),
])),$argv);
WFW::collectModules();
if($argvReader->exists("-update")) $paths = WFW::cleanablePathsForUpdate();
else $paths = WFW::cleanablePaths();
if($argvReader->exists('-list')) foreach($paths as $path) fwrite(STDOUT,"$path\n");
else exec('rm -rf "'.implode('" "',$paths).'"');
\ No newline at end of file
......@@ -45,7 +45,7 @@ $argvReader=$argvReader = new ArgvReader(new ArgvParser(new ArgvOptMap([
new ArgvOpt(
'update','Met à jour les fichiers wfw du projet ciblé avec les fichiers contenus dans le dossier spécifié '
.'(update [-self(global) | -all(tous) | -projet,projet2,...(projets spécifiés)] [sources path]',
2,null,true
null,null,true
),
new ArgvOpt(
'maintenance',"Permet de mettre en maintenance un ou plusieurs projets."
......@@ -152,6 +152,8 @@ try{
$args = $argvReader->get('update');
$projects = $args[0];
$path = $args[1];
$args = array_flip(array_slice($args,2));
$prompt = !isset($args["-no-prompt"]);
$projects = strpos($projects,"-") === 0 ? substr($projects,1):$projects;
$projects = explode(",",$projects);
$pMap = []; $valids = array_merge(["self","all"],array_keys($data));
......@@ -180,6 +182,29 @@ try{
//next create a working dir in tmp folder
if(!is_dir($tmp = "$tmpDir/wfw"))mkdir($tmp,700);
foreach($pMap as $n=>$p){
if($prompt){
fwrite(STDOUT,"Do you really want to update $n (location : $p) ? (y/n) : ");
if(!filter_var(preg_replace(["/^y$/","/^n$/"],["yes","no"],fgets(STDIN)), FILTER_VALIDATE_BOOLEAN)){
fwrite(STDOUT,"$n will not be updated.\n");
continue;
}
}
fwrite(STDOUT,"$n will be updated...\n");
if(file_exists("$p/cli/wfw/WFWCleanerLauncher.php")){
fwrite(STDOUT,"Searching for $n directories to clean before import...\n");
$res = [];
exec("$p/cli/wfw/WFWCleanerLauncher.php -update -list",$res);
fwrite(STDOUT,"The following files and directories will be removed : \n");
foreach($res as $line) fwrite(STDOUT,"\t$line\n");
if($prompt){
fwrite(STDOUT,"Do you really want to continue ? (y/n) : ");
if(!filter_var(preg_replace(["/^y$/","/^n$/"],["yes","no"],fgets(STDIN)), FILTER_VALIDATE_BOOLEAN)){
fwrite(STDOUT,"$n will not be updated.\n");
continue;
}
}
}
fwrite(STDOUT,"Starting $n update (working dir : $tmp/$n)...\n");
if(!is_dir("$tmp/$n")) mkdir("$tmp/$n",700);
//this is the list of all confs file that exists in the framework and that may be
......@@ -191,7 +216,6 @@ try{
"mss" => "daemons/modelSupervisor/server/config/conf.json",
"sctl" => "daemons/sctl/config/conf.json",
"wfw" => "cli/wfw/config/conf.json",
"updator" => "cli/updator/config/conf.json",
"tester" => "cli/tester/config/conf.json",
"backup" => "cli/backup/config/conf.json"
];
......@@ -200,9 +224,13 @@ try{
foreach($confs as $c=>$v){
touch("$tmp/$n/$c",700);
file_put_contents("$tmp/$n/$c",'{}');
$fconf = new FileBasedConf("$tmp/$n/$c",new JSONConfIOAdapter());
$fconf->merge(new FileBasedConf("$path/$v"));
$fconf->merge(new FileBasedConf("$p/$v"));
try{
$fconf = new FileBasedConf("$tmp/$n/$c",$io = new JSONConfIOAdapter());
$fconf->merge(new FileBasedConf("$path/$v",$io));
$fconf->merge(new FileBasedConf("$p/$v",$io));
}catch(\Error | \Exception $e){
fwrite(STDOUT,"\e[33m$e\e[0m");
}
file_put_contents(
"$tmp/$n/$c",
json_encode($fconf->getRawConf(),JSON_PRETTY_PRINT)
......@@ -215,6 +243,12 @@ try{
$exec("wfw self service stop -all");
fwrite(STDOUT,"Daemons stoped.\n");
//removing files and folder
fwrite(STDOUT,"Removing files and folders that must be cleaned up...\n");
exec("$p/cli/wfw/WFWCleanerLauncher.php -update",$res,$state);
if($state > 0) fwrite(STDOUT,"An error occured while trying to cleanup $n.\n");
else fwrite(STDOUT,"$n successfully cleaned up.\n");
$exec("cp -R \"$path/.\" \"$p\"");
fwrite(STDOUT,"Updated files imported.\n");
foreach($confs as $c=>$v){
......@@ -243,11 +277,11 @@ try{
$exec("wfw self service start -all");
$exec("wfw self service restart sctl");
fwrite(STDOUT,"Daemons restarted.\n");
rmdir("$tmp/$n");
exec("rm -rf $tmp/$n");
fwrite(STDOUT,"Working dir $tmp/$n removed.\n");
fwrite(STDOUT,"$n successfully updated.\n\n");
}
rmdir($tmp);
exec("rm -rf \"$tmp\"");
fwrite(STDOUT,"$tmp removed.\n");
fwrite(STDOUT,"Done.\n");
}else if($argvReader->exists('add')){
......@@ -574,6 +608,7 @@ try{
$args = $argvReader->get('remove');
$project = $args[0];
array_shift($args);
$args = array_flip($args);
$prompt = !isset($args["-no-prompt"]);
if(!isset($data[$project]))
throw new InvalidArgumentException("$project is not a registered project !");
......@@ -709,6 +744,20 @@ try{
}
}
fwrite(STDOUT,"$path will be imported into $pPath...\n");
if(file_exists("$pPath/cli/wfw/WFWCleanerLauncher.php")){
fwrite(STDOUT,"Searching for $pName files and directories to clean before import...\n");
$res = [];
exec("$pPath/cli/wfw/WFWCleanerLauncher.php -list",$res);
fwrite(STDOUT,"The following files and directories will be removed : \n");
foreach($res as $line) fwrite(STDOUT,"\t$line\n");
if($prompt){
fwrite(STDOUT,"Do you really want to continue ?\n");
if(!filter_var(preg_replace(["/^y$/","/^n$/"],["yes","no"],fgets(STDIN)), FILTER_VALIDATE_BOOLEAN)){
fwrite(STDOUT,"$path will not be imported.\n");
exit(0);
}
}
}
$siteConfChanged = false;
if(!$keepConf && file_exists("$path/site/config/conf.json")){
$siteConfChanged = true;
......@@ -719,6 +768,12 @@ try{
$conf->save();
fwrite(STDOUT,"Project conf updated.\n");
}
fwrite(STDOUT,"Removing files and folders that must be cleaned up...\n");
exec("$pPath/cli/wfw/WFWCleanerLauncher.php",$res,$state);
if($state > 0) fwrite(STDOUT,"An error occured while trying to cleanup $pName.\n");
else fwrite(STDOUT,"$pName successfully cleaned up.\n");
fwrite(STDOUT,"Copying $path files and folder into $pPath...\n");
$exec("cp -R \"$path/.\" \"$pPath\"");
fwrite(STDOUT,"Files and folders copied.\n");
......@@ -766,9 +821,9 @@ try{
$exec("wfw self service restart -all");
fwrite(STDOUT,"Daemons restarted.\n");
fwrite(STDOUT,"$path successfully imported into $pPath.\n");
fwrite(STDOUT,"\e[33m[WARN] If some files have been removed in this project,"
/*fwrite(STDOUT,"\e[33m[WARN] If some files have been removed in this project,"
." they havn't been removed by the import command. You must do it manually.\n"
);
);*/
} else if($argvReader->exists('locate')){
$args = $argvReader->get('locate');
if(count($args) === 0) fwrite(STDOUT,dirname(__DIR__,2).PHP_EOL);
......
......@@ -19,11 +19,6 @@ $argvReader = new ArgvReader(new ArgvParser(new ArgvOptMap([
"Gestion de packages : package -help pour plus d'informations.",
null, null, true
),
/*new ArgvOpt(
'update',
"Gestion de mises à jour : updator -help pour plus d'informations.",
null,null,true
),*/
new ArgvOpt(
'service',
"Gestion des services wfw : service -help pour plus d'informations.",
......@@ -63,9 +58,6 @@ try{
case 'backup' :
$path = dirname(__DIR__).'/backup/BackupLauncher.php';
break;
/*case 'update' :
$path = CLI.'/updator/UpdatorLauncher.php';
break;*/
case 'package' :
$path = dirname(__DIR__).'/installer/PackageLauncher.php';
break;
......
<?php
namespace wfw\engine\core;
use wfw\engine\core\app\WebApp;
/**
* Main class called by the entry point (engine/webroot/indexp.php) if no wfw\site\core\Main class
* is defined.
*/
class Main {
/**
* Called by the index.php file that recieve the request.
* @param array $args Arguments
*/
public function __construct(array $args=[]){
$contextInfos = (require dirname(__DIR__,2)."/site/config/site.context.php")($args);
new WebApp(new $contextInfos["class"](...($contextInfos["args"] ?? [])));
}
}
\ No newline at end of file
......@@ -47,4 +47,9 @@ interface IModuleDescriptor {
* @return string[] List of class that implements ISecurityPolicy and that must be applyed.
*/
public static function securityPolicies():array;
/**
* @return string[] List of paths that are cleanable while importing project.
*/
public static function cleanablePaths():array;
}
\ No newline at end of file
......@@ -76,4 +76,34 @@ abstract class ModuleDescriptor implements IModuleDescriptor{
public static function commandHandlers(): array {
return [];
}
}
\ No newline at end of file
/**
* @return string[] List of paths that are cleanable while importing project.
*/
public static function cleanablePaths(): array {
return [];
}
/**
* Return non-recursive list of all direct subdirectories of one directory
*
* @param string $folder Directory (automaticaly resolved relatively to $from or root)
* @param array $excepts List of directory (relative to $folder) to not include into the result list
* @param null|string $from
* @return string[] Full path list.
* @throws \ReflectionException
*/
protected static function subdirectories(string $folder, array $excepts=[], ?string $from=null):array{
$root = $from ?? static::root();
if(is_dir("$root/$folder")) return array_map(
function($path)use($root,$folder){
return "$root/$folder/$path";
},
array_diff(
scandir("$root/$folder"),
array_merge([".",".."],$excepts)
)
);
else return [];
}
}
\ No newline at end of file
......@@ -16,7 +16,7 @@ final class WFW extends ModuleDescriptor implements IAppModulesCollector, ISecur
* @return array
*/
public static function confs(): array {
$projectFolder = dirname(__DIR__,3);
$projectFolder = self::root();
$engineFile = ["$projectFolder/engine/config/conf.json"];
$siteFile = ["$projectFolder/site/config/conf.json"];
return array_merge(
......@@ -33,6 +33,13 @@ final class WFW extends ModuleDescriptor implements IAppModulesCollector, ISecur
);
}
/**
* @return string
*/
public static function root(): string {
return dirname(__DIR__,3);
}
/**
* @param array $langs
* @return array
......@@ -40,7 +47,7 @@ final class WFW extends ModuleDescriptor implements IAppModulesCollector, ISecur
public static function langs(?array $langs=null): array {
$langs = is_array($langs) ? array_flip($langs) : null;
$files = [];
$projectFolder = dirname(__DIR__,3);
$projectFolder = self::root();
foreach(["engine","site"] as $folder){
$files[$folder] = [];
exec(
......@@ -98,7 +105,7 @@ final class WFW extends ModuleDescriptor implements IAppModulesCollector, ISecur
* @return array
*/
public static function commandHandlers(): array {
$projectFolder = dirname(__DIR__,3);
$projectFolder = self::root();
$siteFile = "$projectFolder/site/config/site.command.handlers.php";
$engineFile = "$projectFolder/engine/config/default.command.handlers.php";
$site = [];