From bf73360f2cb21e17a5bf97228abc25665b6c8c9d Mon Sep 17 00:00:00 2001 From: Netkas Date: Sat, 28 Jan 2023 06:21:20 -0500 Subject: [PATCH] Implemented Symlinks --- src/ncc/CLI/Commands/ExecCommand.php | 2 +- src/ncc/Managers/ExecutionPointerManager.php | 40 +- src/ncc/Managers/PackageLockManager.php | 11 + src/ncc/Managers/PackageManager.php | 12 +- src/ncc/Managers/SymlinkManager.php | 358 ++++++++++++++++++ src/ncc/Objects/SymlinkDictionary.php | 8 + .../SymlinkDictionary/SymlinkEntry.php | 73 ++++ src/ncc/ncc.php | 9 +- 8 files changed, 503 insertions(+), 10 deletions(-) create mode 100644 src/ncc/Managers/SymlinkManager.php create mode 100644 src/ncc/Objects/SymlinkDictionary.php create mode 100644 src/ncc/Objects/SymlinkDictionary/SymlinkEntry.php diff --git a/src/ncc/CLI/Commands/ExecCommand.php b/src/ncc/CLI/Commands/ExecCommand.php index 5411b99..30826a9 100644 --- a/src/ncc/CLI/Commands/ExecCommand.php +++ b/src/ncc/CLI/Commands/ExecCommand.php @@ -118,7 +118,7 @@ Console::out(PHP_EOL . 'Example Usage:' . PHP_EOL); Console::out(' ncc exec --package com.example.program'); Console::out(' ncc exec --package com.example.program --exec-version 1.0.0'); - Console::out(' ncc exec --package com.example.program --exec-version 1.0.0 --unit setup'); + Console::out(' ncc exec --package com.example.program --exec-version 1.0.0 --exec-unit setup'); Console::out(' ncc exec --package com.example.program --exec-args --foo --bar --extra=test'); } } \ No newline at end of file diff --git a/src/ncc/Managers/ExecutionPointerManager.php b/src/ncc/Managers/ExecutionPointerManager.php index 5676ae9..6d68eb1 100644 --- a/src/ncc/Managers/ExecutionPointerManager.php +++ b/src/ncc/Managers/ExecutionPointerManager.php @@ -114,6 +114,27 @@ return hash('haval128,4', $package . $version); } + /** + * Returns the path to the execution pointer file + * + * @param string $package + * @param string $version + * @param string $name + * @return string + * @throws FileNotFoundException + */ + public function getEntryPointPath(string $package, string $version, string $name): string + { + $package_id = $this->getPackageId($package, $version); + $package_bin_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id; + $entry_point_path = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $name) . '.entrypoint'; + + if(!file_exists($entry_point_path)) + throw new FileNotFoundException('Cannot find entry point for ' . $package . '=' . $version . '.' . $name); + + return $entry_point_path; + } + /** * Adds a new Execution Unit to the * @@ -139,10 +160,12 @@ $package_id = $this->getPackageId($package, $version); $package_config_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id . '.inx'; $package_bin_path = $this->RunnerPath . DIRECTORY_SEPARATOR . $package_id; + $entry_point_path = $package_bin_path . DIRECTORY_SEPARATOR . hash('haval128,4', $unit->ExecutionPolicy->Name) . '.entrypoint'; Console::outDebug(sprintf('package_id=%s', $package_id)); Console::outDebug(sprintf('package_config_path=%s', $package_config_path)); Console::outDebug(sprintf('package_bin_path=%s', $package_bin_path)); + Console::outDebug(sprintf('entry_point_path=%s', $entry_point_path)); $filesystem = new Filesystem(); @@ -184,6 +207,16 @@ $execution_pointers->addUnit($unit, $bin_file); IO::fwrite($package_config_path, ZiProto::encode($execution_pointers->toArray(true))); + $entry_point = sprintf("#!%s\nncc exec --package=\"%s\" --exec-version=\"%s\" --exec-unit=\"%s\" --exec-args \"$@\"", + '/bin/bash', + $package, $version, $unit->ExecutionPolicy->Name + ); + + if(file_exists($entry_point_path)) + $filesystem->remove($entry_point_path); + IO::fwrite($entry_point_path, $entry_point); + chmod($entry_point_path, 0755); + if($temporary) { Console::outVerbose(sprintf('Adding temporary ExecutionUnit \'%s\' for %s', $unit->ExecutionPolicy->Name, $package)); @@ -333,10 +366,9 @@ } } - $process = new Process(array_merge([ - PathFinder::findRunner(strtolower($unit->ExecutionPolicy->Runner)), - $unit->FilePointer - ], $args)); + $process = new Process(array_merge( + [PathFinder::findRunner(strtolower($unit->ExecutionPolicy->Runner)), $unit->FilePointer], $args) + ); if($unit->ExecutionPolicy->Execute->WorkingDirectory !== null) { diff --git a/src/ncc/Managers/PackageLockManager.php b/src/ncc/Managers/PackageLockManager.php index 56d7daf..23215d6 100644 --- a/src/ncc/Managers/PackageLockManager.php +++ b/src/ncc/Managers/PackageLockManager.php @@ -125,6 +125,17 @@ { throw new PackageLockException('Cannot save the package lock file to disk', $e); } + + try + { + Console::outDebug('synchronizing symlinks'); + $symlink_manager = new SymlinkManager(); + $symlink_manager->sync(); + } + catch(Exception $e) + { + throw new PackageLockException('Failed to synchronize symlinks', $e); + } } /** diff --git a/src/ncc/Managers/PackageManager.php b/src/ncc/Managers/PackageManager.php index 86c4eca..b60d8b3 100644 --- a/src/ncc/Managers/PackageManager.php +++ b/src/ncc/Managers/PackageManager.php @@ -326,11 +326,14 @@ IO::fwrite($installation_paths->getDataPath() . DIRECTORY_SEPARATOR . 'exec', ZiProto::encode($unit_paths)); } + // After execution units are installed, create a symlink if needed if(isset($package->Header->Options['create_symlink']) && $package->Header->Options['create_symlink']) { - $paths = [ - DIRECTORY_SEPARATOR . 'usr' . DIRECTORY_SEPARATOR . 'bin' - ]; + if($package->MainExecutionPolicy === null) + throw new InstallationException('Cannot create symlink, no main execution policy is defined'); + + $SymlinkManager = new SymlinkManager(); + $SymlinkManager->add($package->Assembly->Package, $package->MainExecutionPolicy); } // Execute the post-installation stage after the installation is complete @@ -847,6 +850,9 @@ Console::outDebug(sprintf('warning: removing execution unit %s failed', $executionUnit->ExecutionPolicy->Name)); } } + + $symlink_manager = new SymlinkManager(); + $symlink_manager->sync(); } /** diff --git a/src/ncc/Managers/SymlinkManager.php b/src/ncc/Managers/SymlinkManager.php new file mode 100644 index 0000000..4f9d018 --- /dev/null +++ b/src/ncc/Managers/SymlinkManager.php @@ -0,0 +1,358 @@ +SymlinkDictionaryPath = PathFinder::getSymlinkDictionary(Scopes::System); + $this->load(); + } + catch(Exception $e) + { + Console::outWarning(sprintf('failed to load symlink dictionary from %s', $this->SymlinkDictionaryPath)); + } + finally + { + if($this->SymlinkDictionary === null) + $this->SymlinkDictionary = []; + + unset($e); + } + } + + /** + * Loads the symlink dictionary from the file + * + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + public function load(): void + { + if($this->SymlinkDictionary !== null) + return; + + Console::outDebug(sprintf('loading symlink dictionary from %s', $this->SymlinkDictionaryPath)); + + if(!file_exists($this->SymlinkDictionaryPath)) + { + Console::outDebug('symlink dictionary does not exist, creating new dictionary'); + $this->SymlinkDictionary = []; + $this->save(false); + return; + } + + try + { + $this->SymlinkDictionary = []; + + foreach(ZiProto::decode(IO::fread($this->SymlinkDictionaryPath)) as $entry) + { + $this->SymlinkDictionary[] = SymlinkEntry::fromArray($entry); + } + } + catch(Exception $e) + { + $this->SymlinkDictionary = []; + + Console::outDebug('symlink dictionary is corrupted, creating new dictionary'); + $this->save(false); + } + finally + { + unset($e); + } + } + + /** + * Saves the symlink dictionary to the file + * + * @param bool $throw_exception + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + private function save(bool $throw_exception=true): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to write to the system symlink dictionary'); + + Console::outDebug(sprintf('saving symlink dictionary to %s', $this->SymlinkDictionaryPath)); + + try + { + $dictionary = []; + foreach($this->SymlinkDictionary as $entry) + { + $dictionary[] = $entry->toArray(true); + } + + IO::fwrite($this->SymlinkDictionaryPath, ZiProto::encode($dictionary)); + } + catch(Exception $e) + { + if($throw_exception) + throw new IOException(sprintf('failed to save symlink dictionary to %s', $this->SymlinkDictionaryPath), $e); + + Console::outWarning(sprintf('failed to save symlink dictionary to %s', $this->SymlinkDictionaryPath)); + } + finally + { + unset($e); + } + } + + /** + * @return string + */ + public function getSymlinkDictionaryPath(): string + { + return $this->SymlinkDictionaryPath; + } + + /** + * @return array + */ + public function getSymlinkDictionary(): array + { + return $this->SymlinkDictionary; + } + + /** + * Checks if a package is defined in the symlink dictionary + * + * @param string $package + * @return bool + */ + public function exists(string $package): bool + { + foreach($this->SymlinkDictionary as $entry) + { + if($entry->Package === $package) + return true; + } + + return false; + } + + /** + * Adds a new entry to the symlink dictionary + * + * @param string $package + * @param string $unit + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + public function add(string $package, string $unit='main'): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to add to the system symlink dictionary'); + + if($this->exists($package)) + $this->remove($package); + + $entry = new SymlinkEntry(); + $entry->Package = $package; + $entry->ExecutionPolicyName = $unit; + + $this->SymlinkDictionary[] = $entry; + $this->save(); + } + + /** + * Removes an entry from the symlink dictionary + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + public function remove(string $package): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to remove from the system symlink dictionary'); + + if(!$this->exists($package)) + return; + + foreach($this->SymlinkDictionary as $key => $entry) + { + if($entry->Package === $package) + { + if($entry->Registered) + { + $filesystem = new Filesystem(); + + $symlink_name = explode('.', $entry->Package)[count(explode('.', $entry->Package)) - 1]; + $symlink = self::$BinPath . DIRECTORY_SEPARATOR . $symlink_name; + + if($filesystem->exists($symlink)) + $filesystem->remove($symlink); + + } + + unset($this->SymlinkDictionary[$key]); + $this->save(); + return; + } + } + + throw new IOException(sprintf('failed to remove package %s from the symlink dictionary', $package)); + } + + /** + * Sets the package as registered + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + private function setAsRegistered(string $package): void + { + foreach($this->SymlinkDictionary as $key => $entry) + { + if($entry->Package === $package) + { + $entry->Registered = true; + $this->SymlinkDictionary[$key] = $entry; + $this->save(); + return; + } + } + } + + /** + * Sets the package as unregistered + * + * @param string $package + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + private function setAsUnregistered(string $package): void + { + foreach($this->SymlinkDictionary as $key => $entry) + { + if($entry->Package === $package) + { + $entry->Registered = false; + $this->SymlinkDictionary[$key] = $entry; + $this->save(); + return; + } + } + } + + /** + * Syncs the symlink dictionary with the filesystem + * + * @return void + * @throws AccessDeniedException + * @throws IOException + */ + public function sync(): void + { + if(Resolver::resolveScope() !== Scopes::System) + throw new AccessDeniedException('Insufficient Permissions to sync the system symlink dictionary'); + + $filesystem = new Filesystem(); + $execution_pointer_manager = new ExecutionPointerManager(); + $package_lock_manager = new PackageLockManager(); + + foreach($this->SymlinkDictionary as $entry) + { + if($entry->Registered) + continue; + + $symlink_name = explode('.', $entry->Package)[count(explode('.', $entry->Package)) - 1]; + $symlink = self::$BinPath . DIRECTORY_SEPARATOR . $symlink_name; + + if($filesystem->exists($symlink)) + { + Console::outWarning(sprintf('Symlink %s already exists, skipping', $symlink)); + continue; + } + + try + { + $package_entry = $package_lock_manager->getPackageLock()->getPackage($entry->Package); + + if($package_entry == null) + { + Console::outWarning(sprintf('Package %s is not installed, skipping', $entry->Package)); + continue; + } + + $latest_version = $package_entry->getLatestVersion(); + + } + catch(Exception $e) + { + $filesystem->remove($symlink); + Console::outWarning(sprintf('Failed to get package %s, skipping', $entry->Package)); + continue; + } + + try + { + $entry_point_path = $execution_pointer_manager->getEntryPointPath($entry->Package, $latest_version, $entry->ExecutionPolicyName); + $filesystem->symlink($entry_point_path, $symlink); + } + catch(Exception $e) + { + $filesystem->remove($symlink); + Console::outWarning(sprintf('Failed to create symlink %s, skipping', $symlink)); + continue; + } + finally + { + unset($e); + } + + $this->setAsRegistered($entry->Package); + + } + } + } \ No newline at end of file diff --git a/src/ncc/Objects/SymlinkDictionary.php b/src/ncc/Objects/SymlinkDictionary.php new file mode 100644 index 0000000..c5ea087 --- /dev/null +++ b/src/ncc/Objects/SymlinkDictionary.php @@ -0,0 +1,8 @@ +ExecutionPolicyName = 'main'; + $this->Registered = false; + } + + /** + * Returns a string representation of the object + * + * @param bool $bytecode + * @return array + */ + public function toArray(bool $bytecode=false): array + { + return [ + ($bytecode ? Functions::cbc('package') : 'package') => $this->Package, + ($bytecode ? Functions::cbc('registered') : 'registered') => $this->Registered, + ($bytecode ? Functions::cbc('execution_policy_name') : 'execution_policy_name') => $this->ExecutionPolicyName + ]; + } + + /** + * Constructs a new SymlinkEntry from an array representation + * + * @param array $data + * @return SymlinkEntry + */ + public static function fromArray(array $data): SymlinkEntry + { + $entry = new SymlinkEntry(); + + $entry->Package = Functions::array_bc($data, 'package'); + $entry->Registered = (bool)Functions::array_bc($data, 'registered'); + $entry->ExecutionPolicyName = Functions::array_bc($data, 'execution_policy_name'); + + return $entry; + } + + } \ No newline at end of file diff --git a/src/ncc/ncc.php b/src/ncc/ncc.php index 192f311..1ce02ee 100644 --- a/src/ncc/ncc.php +++ b/src/ncc/ncc.php @@ -2,6 +2,9 @@ namespace ncc; + use ncc\Exceptions\AccessDeniedException; + use ncc\Exceptions\FileNotFoundException; + use ncc\Exceptions\IOException; use ncc\Exceptions\MalformedJsonException; use ncc\Exceptions\RuntimeException; use ncc\Objects\NccVersionInformation; @@ -34,8 +37,10 @@ * * @param boolean $reload Indicates if the cached version is to be ignored and the version file to be reloaded and validated * @return NccVersionInformation - * @throws Exceptions\FileNotFoundException - * @throws Exceptions\RuntimeException + * @throws AccessDeniedException + * @throws FileNotFoundException + * @throws IOException + * @throws RuntimeException */ public static function getVersionInformation(bool $reload=False): NccVersionInformation {