├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── composer.json └── src ├── Actions ├── Action.php ├── ExecuteAction.php ├── HistoryAction.php ├── KernelInfoAction.php └── ShutdownAction.php ├── ConnectionSettings.php ├── Handlers ├── HbErrorHandler.php ├── HbMessagesHandler.php ├── IOPubMessagesHandler.php └── ShellMessagesHandler.php ├── JupyterBroker.php ├── KernelCore.php ├── KernelOutput.php ├── LoggerSettings.php ├── Shell.php ├── System ├── BsdSystem.php ├── LinuxSystem.php ├── MacSystem.php ├── System.php ├── UnixSystem.php └── WindowsSystem.php ├── UserFunctions └── functions.php └── kernel.php /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | Jupyter-PHP Contributing Guidelines 2 | ==================================== 3 | 4 | Contributing with Code 5 | ---------------------- 6 | 7 | In first place, if you want to contribute code to Jupyter-PHP, you must have installed PHP, preferably recent 8 | versions (PHP>=5.5, but recommended PHP>=7.0). 9 | 10 | If you don't know where to start, you can try searching inside the 11 | [issues list](https://github.com/Litipk/Jupyter-PHP/issues). Specially the ones marked with the 12 | [help wanted](https://github.com/Litipk/Jupyter-PHP/labels/help%20wanted) tag. 13 | 14 | Some extra advices related on good practices: 15 | * We try to follow the PSR standards, in special **PSR-1**, **PSR-2** and **PSR-4**. 16 | * Every new added feature should come with unit tests, stability and code correctness is an important priority of this project. 17 | 18 | 19 | Contributing with Documentation 20 | ------------------------------- 21 | 22 | If you want to contribute documentation, you're welcome! :) , BUT, actually the documentation is pretty bad, and it's perfectly 23 | undersandable that this task isn't very attractive. 24 | 25 | Contributing with Bug Reports 26 | ----------------------------- 27 | 28 | A very important part of every software project is the bugs tracking and solving process. You can send us bug reports through 29 | the [tracker](https://github.com/Litipk/Jupyter-PHP/issues). 30 | 31 | It's desirable that every bug report explains the bug in a way that the developers can easily reproduce it. The following points 32 | are very helpful in order to achieve reproducibility: 33 | * Example code. 34 | * Jupyter version 35 | * Jupyter-PHP version. 36 | * Python version 37 | * PHP version 38 | 39 | Contributing with New Ideas 40 | --------------------------- 41 | 42 | You can use the github [tracker](https://github.com/Litipk/Jupyter-PHP/issues) to send us new ideas, even if those ideas aren't 43 | bug reports. In such case, adding something like "SUGGESTION: ...", "IMPROVEMENT: ..." to the report bug it's a good idea. 44 | 45 | You can also discuss your ideas with us in our [Gitter chat room](https://gitter.im/Litipk/Jupyter-PHP). 46 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015-2017 Litipk 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Jupyter-PHP 2 | 3 | [![Author](http://img.shields.io/badge/author-@castarco-blue.svg?style=flat-square)](https://twitter.com/castarco) 4 | [![Software License](https://img.shields.io/badge/license-MIT-brightgreen.svg?style=flat-square)](LICENSE) 5 | [![Packagist Version](https://img.shields.io/packagist/v/Litipk/jupyter-php.svg?style=flat-square)](https://packagist.org/packages/Litipk/jupyter-php) 6 | 7 | ## Introduction 8 | 9 | **Jupyter-PHP** is a PHP kernel for [*Jupyter*](http://jupyter.org). This project is inspired on 10 | [https://github.com/dawehner/jupyter-php](https://github.com/dawehner/jupyter-php), some bits of code from the old 11 | *IPython* kernel have been used in this new *Jupyter* kernel. 12 | 13 | In addition to the Jupyter compatibility, the new Jupyter-PHP adds a lot of improvements, like the integration with 14 | [PsySH](http://psysh.org/) in order to provide: 15 | 16 | * *"memory"*: the old kernel was unable to keep variables to allow reuse them later. 17 | * online documentation with the `doc` command. *Not yet implemented* 18 | * code introspection with the `ls` command. *Not yet implemented* 19 | * errors introspection with the `wtf` command. *Not yet implemented* 20 | 21 | ## Getting started 22 | 23 | The recommended way is to go to the [*Jupyter-PHP installer*'s page](https://litipk.github.io/Jupyter-PHP-Installer/) 24 | and follow its instructions. 25 | 26 | ## Learn more 27 | 28 | * [Chat Room](https://gitter.im/Litipk/Jupyter-PHP) : If you want to have a real-time chat with other Jupyter-PHP users or developers, you can do it here. 29 | * [Group / Mail List](https://groups.io/g/jupyter-php) : If a chat room isn't enough to post your doubts or ideas, you can join to our mail list. 30 | 31 | ## How to contribute 32 | 33 | * First of all, you can take a look on the [bugtracker](https://github.com/Litipk/Jupyter-PHP-Installer/issues) and decide if there is something that you want to do :wink: . If you think there are missing improvements in this file, then you are invited to modify the TODO list. 34 | * You can also send us bug reports using the same bugtracker. 35 | * If you are really interested on helping to improve Litipk\BigNumbers, we recommend to read the [contributing guidelines](https://github.com/Litipk/Jupyter-PHP-Installer/blob/master/CONTRIBUTING.md). 36 | 37 | 38 | ## License 39 | 40 | Jupyter-PHP is licensed under the [MIT License](https://github.com/Litipk/Jupyter-PHP/blob/master/LICENSE). 41 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "litipk/jupyter-php", 3 | "description": "A PHP Kernel for Jupyter", 4 | "type": "project", 5 | "license": "MIT", 6 | "require": { 7 | "php": ">=7.0.0", 8 | "ext-zmq": "*", 9 | "composer/composer": "^1.5", 10 | "monolog/monolog": "^1.23", 11 | "psy/psysh": "^0.8", 12 | "ramsey/uuid": "^3.7", 13 | "react/zmq": "^0.3.0" 14 | }, 15 | "require-dev": { 16 | 17 | }, 18 | "autoload": { 19 | "psr-4": { "JupyterPHP\\": "src/"} 20 | }, 21 | "scripts": { 22 | 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/Actions/Action.php: -------------------------------------------------------------------------------- 1 | broker = $broker; 55 | $this->iopubSocket = $iopubSocket; 56 | $this->shellSocket = $shellSocket; 57 | $this->shellSoul = $shellSoul; 58 | } 59 | 60 | public function call(array $header, array $content, $zmqIds = []) 61 | { 62 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'busy'], $header); 63 | 64 | $this->header = $header; 65 | $this->code = $content['code']; 66 | $this->silent = $content['silent']; 67 | 68 | if (!$this->silent) { 69 | $this->execCount = $this->execCount + 1; 70 | 71 | $this->broker->send( 72 | $this->iopubSocket, 73 | 'execute_input', 74 | ['code' => $this->code, 'execution_count' => $this->execCount], 75 | $this->header 76 | ); 77 | } 78 | 79 | ($this->getClosure())(); 80 | 81 | $replyContent = [ 82 | 'status' => 'ok', 83 | 'execution_count' => $this->execCount, 84 | 'payload' => [], 85 | 'user_expressions' => new \stdClass 86 | ]; 87 | 88 | $this->broker->send($this->shellSocket, 'execute_reply', $replyContent, $this->header, [], $zmqIds); 89 | 90 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'idle'], $this->header); 91 | } 92 | 93 | public function notifyMessage(string $message) 94 | { 95 | $this->broker->send( 96 | $this->iopubSocket, 97 | 'execute_result', 98 | ['execution_count' => $this->execCount, 'data' => ['text/plain' => $message], 'metadata' => new \stdClass], 99 | $this->header 100 | ); 101 | } 102 | 103 | private function getClosure(): callable 104 | { 105 | $closure = function () { 106 | \extract($this->shellSoul->getScopeVariables()); 107 | 108 | try { 109 | $this->shellSoul->addCode($this->code); 110 | 111 | // evaluate the current code buffer 112 | \ob_start([$this->shellSoul, 'writeStdout'], 1); 113 | 114 | \set_error_handler([$this->shellSoul, 'handleError']); 115 | $_ = eval($this->shellSoul->flushCode() ?: Loop::NOOP_INPUT); 116 | \restore_error_handler(); 117 | 118 | \ob_end_flush(); 119 | 120 | $this->shellSoul->writeReturnValue($_); 121 | } catch (BreakException $_e) { 122 | $this->handleEvalException($_e); 123 | return; 124 | } catch (ThrowUpException $_e) { 125 | $this->handleEvalException($_e); 126 | throw $_e; 127 | } catch (\Error $_e) { 128 | $this->handleEvalException(new \ErrorException( 129 | $_e->getMessage(), 130 | $_e->getCode(), 131 | 1, 132 | $_e->getFile(), 133 | $_e->getLine(), 134 | $_e->getPrevious() 135 | )); 136 | } catch (\Exception $_e) { 137 | $this->handleEvalException($_e); 138 | } 139 | 140 | $this->shellSoul->setScopeVariables(\get_defined_vars()); 141 | }; 142 | 143 | return $closure; 144 | } 145 | 146 | private function handleEvalException(\Exception $_e) 147 | { 148 | \restore_error_handler(); 149 | if (\ob_get_level() > 0) { 150 | \ob_end_clean(); 151 | } 152 | $this->shellSoul->writeException($_e); 153 | } 154 | } 155 | -------------------------------------------------------------------------------- /src/Actions/HistoryAction.php: -------------------------------------------------------------------------------- 1 | broker = $broker; 29 | $this->shellSocket = $shellSocket; 30 | } 31 | 32 | public function call(array $header, array $content, $zmqIds = []) 33 | { 34 | $this->broker->send($this->shellSocket, 'history_reply', ['history' => []], $header, [], $zmqIds); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/Actions/KernelInfoAction.php: -------------------------------------------------------------------------------- 1 | broker = $broker; 32 | $this->shellSocket = $shellSocket; 33 | $this->iopubSocket = $iopubSocket; 34 | } 35 | 36 | public function call(array $header, array $content, $zmqIds = []) 37 | { 38 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'busy'], $header); 39 | 40 | $this->broker->send( 41 | $this->shellSocket, 42 | 'kernel_info_reply', 43 | [ 44 | 'protocol_version' => '5.0', 45 | 'implementation' => 'jupyter-php', 46 | 'implementation_version' => '0.1.0', 47 | 'banner' => 'Jupyter-PHP Kernel', 48 | 'language_info' => [ 49 | 'name' => 'PHP', 50 | 'version' => \phpversion(), 51 | 'mimetype' => 'text/x-php', 52 | 'file_extension' => '.php', 53 | 'pygments_lexer' => 'PHP', 54 | ], 55 | 'status' => 'ok', 56 | ], 57 | $header, 58 | [], 59 | $zmqIds 60 | ); 61 | 62 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'idle'], $header); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/Actions/ShutdownAction.php: -------------------------------------------------------------------------------- 1 | broker = $broker; 31 | $this->iopubSocket = $iopubSocket; 32 | $this->shellSocket = $shellSocket; 33 | } 34 | 35 | public function call(array $header, array $content, $zmqIds = []) 36 | { 37 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'busy'], $header); 38 | 39 | $replyContent = ['restart' => $content['restart']]; 40 | $this->broker->send($this->shellSocket, 'shutdown_reply', $replyContent, $header, [], $zmqIds); 41 | 42 | $this->broker->send($this->iopubSocket, 'status', ['execution_state' => 'idle'], $header); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/ConnectionSettings.php: -------------------------------------------------------------------------------- 1 | 1) { 28 | $connectionFileContents = \file_get_contents( 29 | $argv[1], 30 | null, 31 | null, 32 | 0, 33 | 2048 34 | ); 35 | 36 | if (false === $connectionFileContents) { 37 | throw new \RuntimeException('Connection Settings: Unable to open the connection file.'); 38 | } 39 | 40 | $connectionSettings = \json_decode($connectionFileContents, true); 41 | 42 | if (\json_last_error() !== JSON_ERROR_NONE) { 43 | throw new \RuntimeException('Connection Settings: Corrupted connection file.'); 44 | } 45 | 46 | return $connectionSettings; 47 | } else { 48 | throw new \RuntimeException('Connection Settings: Not specified.'); 49 | } 50 | } 51 | 52 | /** 53 | * @param null|array $connectionSettings 54 | * @return array[string]string 55 | */ 56 | public static function getConnectionUris(array $connectionSettings = null): array 57 | { 58 | if (null === $connectionSettings) { 59 | $connectionSettings = self::get(); 60 | } 61 | 62 | $connectionUri = $connectionSettings['transport'].'://'.$connectionSettings['ip'].':'; 63 | 64 | return [ 65 | 'stdin' => $connectionUri.$connectionSettings['stdin_port'], 66 | 'control' => $connectionUri.$connectionSettings['control_port'], 67 | 'hb' => $connectionUri.$connectionSettings['hb_port'], 68 | 'shell' => $connectionUri.$connectionSettings['shell_port'], 69 | 'iopub' => $connectionUri.$connectionSettings['iopub_port'] 70 | ]; 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/Handlers/HbErrorHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | } 25 | 26 | public function __invoke($e) 27 | { 28 | $this->logger->debug('Received message', ['processId' => \getmypid(), 'error' => $e]); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Handlers/HbMessagesHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 28 | $this->hbSocket = $hbSocket; 29 | } 30 | 31 | public function __invoke($msg) 32 | { 33 | $this->logger->debug('Received message', ['processId' => \getmypid(), 'msg' => $msg]); 34 | 35 | if (['ping'] === $msg) { 36 | $this->hbSocket->send($msg); 37 | } else { 38 | $this->logger->error('Unknown message', ['processId' => \getmypid(), 'msg' => $msg]); 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/Handlers/IOPubMessagesHandler.php: -------------------------------------------------------------------------------- 1 | logger = $logger; 24 | } 25 | 26 | public function __invoke($msg) 27 | { 28 | $this->logger->debug('Received message', ['processId' => \getmypid(), 'msg' => $msg]); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/Handlers/ShellMessagesHandler.php: -------------------------------------------------------------------------------- 1 | shellSoul = new Shell(); 53 | 54 | $this->executeAction = new ExecuteAction($broker, $iopubSocket, $shellSocket, $this->shellSoul); 55 | $this->historyAction = new HistoryAction($broker, $shellSocket); 56 | $this->kernelInfoAction = new KernelInfoAction($broker, $shellSocket, $iopubSocket); 57 | $this->shutdownAction = new ShutdownAction($broker, $iopubSocket, $shellSocket); 58 | 59 | $this->logger = $logger; 60 | 61 | $broker->send( 62 | $iopubSocket, 'status', ['execution_state' => 'starting'], [] 63 | ); 64 | 65 | $this->shellSoul->setOutput(new KernelOutput($this->executeAction, $this->logger->withName('KernelOutput'))); 66 | } 67 | 68 | public function __invoke(array $msg) 69 | { 70 | // Read ZMQ IDs until we reach the delimiter 71 | $zmqIds = array(); 72 | while (!empty($msg)) { 73 | $item = \array_shift($msg); 74 | if ($item === '') break; 75 | else \array_push($zmqIds, $item); 76 | } 77 | 78 | // Read the remaining items 79 | list($hmac, $header, $parentHeader, $metadata, $content) = $msg; 80 | 81 | $header = \json_decode($header, true); 82 | $content = \json_decode($content, true); 83 | 84 | $this->logger->debug('Received message', [ 85 | 'processId' => \getmypid(), 86 | 'zmqIds' => \htmlentities(\implode(", ", $zmqIds), ENT_COMPAT, "UTF-8"), 87 | 'hmac' => $hmac, 88 | 'header' => $header, 89 | 'parentHeader' => $parentHeader, 90 | 'metadata' => $metadata, 91 | 'content' => $content 92 | ]); 93 | 94 | if ('kernel_info_request' === $header['msg_type']) { 95 | $this->kernelInfoAction->call($header, $content, $zmqIds); 96 | } elseif ('execute_request' === $header['msg_type']) { 97 | $this->executeAction->call($header, $content, $zmqIds); 98 | } elseif ('history_request' === $header['msg_type']) { 99 | $this->historyAction->call($header, $content, $zmqIds); 100 | } elseif ('shutdown_request' === $header['msg_type']) { 101 | $this->shutdownAction->call($header, $content, $zmqIds); 102 | } elseif ('comm_open' === $header['msg_type']) { 103 | // TODO: Research about what should be done. 104 | } else { 105 | $this->logger->error('Unknown message type', ['processId' => \getmypid(), 'header' => $header]); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/JupyterBroker.php: -------------------------------------------------------------------------------- 1 | key = $key; 42 | $this->signatureScheme = $signatureScheme; 43 | $this->hashAlgorithm = \preg_split('/-/', $signatureScheme)[1]; 44 | $this->sessionId = $sessionId; 45 | $this->logger = $logger; 46 | } 47 | 48 | public function send( 49 | SocketWrapper $stream, 50 | $msgType, 51 | array $content = [], 52 | array $parentHeader = [], 53 | array $metadata = [], 54 | $zmqIds = [] 55 | ) { 56 | $header = $this->createHeader($msgType); 57 | 58 | $msgDef = [ 59 | \json_encode(empty($header) ? new \stdClass : $header), 60 | \json_encode(empty($parentHeader) ? new \stdClass : $parentHeader), 61 | \json_encode(empty($metadata) ? new \stdClass : $metadata), 62 | \json_encode(empty($content) ? new \stdClass : $content), 63 | ]; 64 | 65 | $finalMsg = $zmqIds; 66 | 67 | $finalMsg = \array_merge( 68 | $finalMsg, 69 | ['', $this->sign($msgDef)], 70 | $msgDef); 71 | 72 | if (null !== $this->logger) { 73 | $this->logger->debug('Sending message', ['processId' => \getmypid(), 'message' => $finalMsg]); 74 | } 75 | 76 | $stream->send($finalMsg); 77 | } 78 | 79 | private function createHeader(string $msgType): array 80 | { 81 | return [ 82 | 'date' => (new \DateTime('NOW'))->format('c'), 83 | 'msg_id' => Uuid::uuid4()->toString(), 84 | 'username' => "kernel", 85 | 'session' => $this->sessionId->toString(), 86 | 'msg_type' => $msgType, 87 | 'version' => '5.0', 88 | ]; 89 | } 90 | 91 | private function sign(array $message_list): string 92 | { 93 | $hm = \hash_init( 94 | $this->hashAlgorithm, 95 | HASH_HMAC, 96 | $this->key 97 | ); 98 | 99 | foreach ($message_list as $item) { 100 | \hash_update($hm, $item); 101 | } 102 | 103 | return \hash_final($hm); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/KernelCore.php: -------------------------------------------------------------------------------- 1 | broker = $jupyterBroker; 54 | $this->logger = $logger; 55 | 56 | $this->initSockets($connUris); 57 | $this->registerHandlers(); 58 | } 59 | 60 | public function run() 61 | { 62 | $this->reactLoop->run(); 63 | } 64 | 65 | /** 66 | * @param array [string]string $connUris 67 | */ 68 | private function initSockets(array $connUris) 69 | { 70 | // Create context 71 | $this->reactLoop = ReactFactory::create(); 72 | 73 | /** @var ReactZmqContext|\ZMQContext $reactZmqContext */ 74 | $reactZmqContext = new ReactZmqContext($this->reactLoop); 75 | 76 | $this->hbSocket = $reactZmqContext->getSocket(\ZMQ::SOCKET_REP); 77 | $this->hbSocket->bind($connUris['hb']); 78 | 79 | $this->iopubSocket = $reactZmqContext->getSocket(\ZMQ::SOCKET_PUB); 80 | $this->iopubSocket->bind($connUris['iopub']); 81 | 82 | $this->controlSocket = $reactZmqContext->getSocket(\ZMQ::SOCKET_ROUTER); 83 | $this->controlSocket->bind($connUris['control']); 84 | 85 | $this->stdinSocket = $reactZmqContext->getSocket(\ZMQ::SOCKET_ROUTER); 86 | $this->stdinSocket->bind($connUris['stdin']); 87 | 88 | $this->shellSocket = $reactZmqContext->getSocket(\ZMQ::SOCKET_ROUTER); 89 | $this->shellSocket->bind($connUris['shell']); 90 | 91 | $this->logger->debug('Initialized sockets', ['processId' => \getmypid()]); 92 | } 93 | 94 | private function registerHandlers() 95 | { 96 | $this->hbSocket->on( 97 | 'error', 98 | new HbErrorHandler($this->logger->withName('HbErrorHandler')) 99 | ); 100 | $this->hbSocket->on( 101 | 'messages', 102 | new HbMessagesHandler($this->hbSocket, $this->logger->withName('HbMessagesHandler')) 103 | ); 104 | $this->iopubSocket->on( 105 | 'messages', 106 | new IOPubMessagesHandler($this->logger->withName('IOPubMessagesHandler')) 107 | ); 108 | $this->shellSocket->on( 109 | 'messages', 110 | new ShellMessagesHandler( 111 | $this->broker, 112 | $this->iopubSocket, 113 | $this->shellSocket, 114 | $this->logger->withName('ShellMessagesHandler') 115 | ) 116 | ); 117 | 118 | $this->logger->debug('Registered handlers', ['processId' => \getmypid()]); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/KernelOutput.php: -------------------------------------------------------------------------------- 1 | executeAction = $executeAction; 40 | $this->logger = $logger; 41 | 42 | $this->formatter = new OutputFormatter(); 43 | $this->formatter->setDecorated(true); 44 | 45 | $this->initFormatters(); 46 | } 47 | 48 | /** 49 | * Writes a message to the output. 50 | * 51 | * @param string|array $messages The message as an array of lines or a single string 52 | * @param bool $newline Whether to add a newline 53 | * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL 54 | */ 55 | public function write($messages, $newline = false, $options = self::OUTPUT_NORMAL) 56 | { 57 | $types = self::OUTPUT_NORMAL | self::OUTPUT_RAW | self::OUTPUT_PLAIN; 58 | $type = $types & $options ?: self::OUTPUT_NORMAL; 59 | 60 | if (\is_string($messages)) { 61 | if ("" === $messages) { 62 | return; 63 | } 64 | 65 | $preparedMessage = $messages . ($newline ? '' : "\n"); 66 | switch ($type) { 67 | case OutputInterface::OUTPUT_NORMAL: 68 | $preparedMessage = $this->formatter->format($messages) . ($newline ? '' : "\n"); 69 | break; 70 | case OutputInterface::OUTPUT_RAW: 71 | break; 72 | case OutputInterface::OUTPUT_PLAIN: 73 | $preparedMessage = \strip_tags($this->formatter->format($messages)) . ($newline ? '' : "\n"); 74 | break; 75 | } 76 | } elseif (\is_array($messages)) { 77 | $preparedMessage = \implode("\n", $messages) . ($newline ? '' : "\n"); 78 | } else { 79 | return; // TODO: Throw an error? 80 | } 81 | 82 | $this->executeAction->notifyMessage($preparedMessage); 83 | } 84 | 85 | /** 86 | * Writes a message to the output and adds a newline at the end. 87 | * 88 | * @param string|array $messages The message as an array of lines of a single string 89 | * @param int $options A bitmask of options (one of the OUTPUT or VERBOSITY constants), 0 is considered the same as self::OUTPUT_NORMAL | self::VERBOSITY_NORMAL 90 | */ 91 | public function writeln($messages, $options = 0) 92 | { 93 | $this->write($messages, true, $options); 94 | } 95 | 96 | /** 97 | * Sets the verbosity of the output. 98 | * 99 | * @param int $level The level of verbosity (one of the VERBOSITY constants) 100 | */ 101 | public function setVerbosity($level) 102 | { 103 | // TODO: Implement setVerbosity() method. 104 | } 105 | 106 | /** 107 | * Gets the current verbosity of the output. 108 | * 109 | * @return int The current level of verbosity (one of the VERBOSITY constants) 110 | */ 111 | public function getVerbosity(): int 112 | { 113 | return self::VERBOSITY_NORMAL; 114 | } 115 | 116 | /** 117 | * Returns whether verbosity is quiet (-q). 118 | * 119 | * @return bool true if verbosity is set to VERBOSITY_QUIET, false otherwise 120 | */ 121 | public function isQuiet(): bool 122 | { 123 | return false; 124 | } 125 | 126 | /** 127 | * Returns whether verbosity is verbose (-v). 128 | * 129 | * @return bool true if verbosity is set to VERBOSITY_VERBOSE, false otherwise 130 | */ 131 | public function isVerbose(): bool 132 | { 133 | return false; 134 | } 135 | 136 | /** 137 | * Returns whether verbosity is very verbose (-vv). 138 | * 139 | * @return bool true if verbosity is set to VERBOSITY_VERY_VERBOSE, false otherwise 140 | */ 141 | public function isVeryVerbose(): bool 142 | { 143 | return false; 144 | } 145 | 146 | /** 147 | * Returns whether verbosity is debug (-vvv). 148 | * 149 | * @return bool true if verbosity is set to VERBOSITY_DEBUG, false otherwise 150 | */ 151 | public function isDebug(): bool 152 | { 153 | return false; 154 | } 155 | 156 | /** 157 | * Sets the decorated flag. 158 | * @param bool $decorated Whether to decorate the messages 159 | */ 160 | public function setDecorated($decorated) 161 | { 162 | // Interface compliance 163 | } 164 | 165 | /** 166 | * Gets the decorated flag. 167 | * 168 | * @return bool true if the output will decorate messages, false otherwise 169 | */ 170 | public function isDecorated(): bool 171 | { 172 | return true; 173 | } 174 | 175 | /** 176 | * Sets output formatter. 177 | * @param OutputFormatterInterface $formatter 178 | */ 179 | public function setFormatter(OutputFormatterInterface $formatter) 180 | { 181 | // Interface compliance 182 | } 183 | 184 | /** 185 | * Returns current output formatter instance. 186 | * @return OutputFormatterInterface 187 | */ 188 | public function getFormatter(): OutputFormatterInterface 189 | { 190 | return $this->formatter; 191 | } 192 | 193 | /** 194 | * Initialize output formatter styles. 195 | */ 196 | private function initFormatters() 197 | { 198 | $formatter = $this->getFormatter(); 199 | 200 | $formatter->setStyle('error', new OutputFormatterStyle('red', null, ['bold'])); 201 | $formatter->setStyle('warning', new OutputFormatterStyle('yellow', null, ['bold'])); 202 | $formatter->setStyle('aside', new OutputFormatterStyle('blue')); 203 | $formatter->setStyle('strong', new OutputFormatterStyle(null, null, array('bold'))); 204 | $formatter->setStyle('return', new OutputFormatterStyle('cyan')); 205 | $formatter->setStyle('urgent', new OutputFormatterStyle('red')); 206 | $formatter->setStyle('hidden', new OutputFormatterStyle('white')); 207 | 208 | // Visibility 209 | $formatter->setStyle('public', new OutputFormatterStyle(null, null, array('bold'))); 210 | $formatter->setStyle('protected', new OutputFormatterStyle('yellow')); 211 | $formatter->setStyle('private', new OutputFormatterStyle('red')); 212 | $formatter->setStyle('global', new OutputFormatterStyle('cyan', null, array('bold'))); 213 | $formatter->setStyle('const', new OutputFormatterStyle('cyan')); 214 | $formatter->setStyle('class', new OutputFormatterStyle('blue', null, array('underscore'))); 215 | $formatter->setStyle('function', new OutputFormatterStyle(null)); 216 | $formatter->setStyle('default', new OutputFormatterStyle(null)); 217 | 218 | // Types 219 | $formatter->setStyle('number', new OutputFormatterStyle('magenta')); 220 | $formatter->setStyle('string', new OutputFormatterStyle('green')); 221 | $formatter->setStyle('bool', new OutputFormatterStyle('cyan')); 222 | $formatter->setStyle('keyword', new OutputFormatterStyle('yellow')); 223 | $formatter->setStyle('comment', new OutputFormatterStyle('blue')); 224 | $formatter->setStyle('object', new OutputFormatterStyle('blue')); 225 | $formatter->setStyle('resource', new OutputFormatterStyle('yellow')); 226 | } 227 | } -------------------------------------------------------------------------------- /src/LoggerSettings.php: -------------------------------------------------------------------------------- 1 | 2) { 29 | return ('debug' === \trim(\strtolower($argv[2]))) 30 | ? Logger::DEBUG 31 | : Logger::WARNING; 32 | } else { 33 | return Logger::WARNING; 34 | } 35 | } 36 | } -------------------------------------------------------------------------------- /src/Shell.php: -------------------------------------------------------------------------------- 1 | getCurrentUserHome().'/Library/jupyter-php'; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/System/System.php: -------------------------------------------------------------------------------- 1 | checkIfCommandExists('whoami')) { 25 | return \exec('whoami'); 26 | } else { 27 | throw new \RuntimeException('Unable to obtain the current username.'); 28 | } 29 | } 30 | 31 | /** 32 | * @return string 33 | */ 34 | public function getCurrentUserHome(): string 35 | { 36 | if (\function_exists('posix_getpwuid') && \function_exists('posix_geteuid')) { 37 | $pwuData = \posix_getpwuid(\posix_geteuid()); 38 | return $pwuData['dir']; 39 | } elseif (\function_exists('getenv') && false !== \getenv('HOME')) { 40 | return \getenv('HOME'); 41 | } else { 42 | throw new \RuntimeException('Unable to obtain the current user home directory.'); 43 | } 44 | } 45 | 46 | /** 47 | * @param string $cmdName 48 | * @return boolean 49 | */ 50 | public function checkIfCommandExists(string $cmdName): bool 51 | { 52 | if (!\function_exists('exec')) { 53 | return false; 54 | } 55 | 56 | $sysResponse = \exec( 57 | 'PATH='.\getenv('PATH').'; '. 58 | "if command -v ".$cmdName." >/dev/null 2>&1; then echo \"true\"; else echo \"false\"; fi;" 59 | ); 60 | 61 | return \filter_var($sysResponse, FILTER_VALIDATE_BOOLEAN); 62 | } 63 | 64 | /** 65 | * @return string 66 | */ 67 | public function getAppDataDirectory(): string 68 | { 69 | return $this->getCurrentUserHome().'/.jupyter-php'; 70 | } 71 | 72 | /** 73 | * Returns true if the path is a "valid" path and is writable (even if the complete path does not yet exist). 74 | * @param string $path 75 | * @return boolean 76 | */ 77 | public function validatePath(string $path): bool 78 | { 79 | $absPath = $this->getAbsolutePath($path); 80 | $absPathParts = \preg_split('/\//', \preg_replace('/(^\/|\/$)/', '', $absPath)); 81 | $nSteps = \count($absPathParts); 82 | 83 | $tmpPath = ''; 84 | $prevReadable = false; 85 | $prevWritable = false; 86 | 87 | for ($i=0; $i<$nSteps; $i++) { 88 | $tmpPath .= '/' . $absPathParts[$i]; 89 | 90 | if (\file_exists($tmpPath)) { 91 | if (!\is_dir($tmpPath)) { 92 | if (\is_link($tmpPath)) { 93 | $linkPath = \readlink($tmpPath); 94 | if (false === $linkPath || !\is_dir($linkPath)) { 95 | return false; 96 | } 97 | $tmpPath = $linkPath; 98 | } else { 99 | return false; 100 | } 101 | } 102 | 103 | $prevReadable = \is_readable($tmpPath); 104 | $prevWritable = \is_writable($tmpPath); 105 | } else { 106 | return ($prevReadable && $prevWritable); 107 | } 108 | } 109 | 110 | return true; 111 | } 112 | 113 | /** 114 | * @param string $path 115 | * @return string The "absolute path" version of $path. 116 | */ 117 | public function ensurePath(string $path): string 118 | { 119 | $absPath = $this->getAbsolutePath($path); 120 | 121 | if (!\file_exists($absPath) && false === \mkdir($absPath, 0755, true)) { 122 | throw new \RuntimeException('Unable to create the specified directory ('.$absPath.').'); 123 | } 124 | 125 | return $absPath; 126 | } 127 | 128 | /** 129 | * @param string $path 130 | * @return bool 131 | */ 132 | protected function isAbsolutePath(string $path): bool 133 | { 134 | return (1 === \preg_match('#^/#', $path)); 135 | } 136 | 137 | /** 138 | * @param string $path 139 | * @return string 140 | */ 141 | protected function getAbsolutePath(string $path): string 142 | { 143 | return $this->isAbsolutePath($path) 144 | ? $path 145 | : (\getcwd() . DIRECTORY_SEPARATOR . $path); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /src/System/WindowsSystem.php: -------------------------------------------------------------------------------- 1 | nul 2>&1 && echo true"); 46 | 47 | return \filter_var($sysResponse, FILTER_VALIDATE_BOOLEAN); 48 | } 49 | 50 | public function getAppDataDirectory(): string 51 | { 52 | return $this->getCurrentUserHome() . '/.jupyter-php'; 53 | } 54 | 55 | /** 56 | * @inheritdoc 57 | */ 58 | public function validatePath(string $path): bool 59 | { 60 | $absPath = $this->getAbsolutePath($path); 61 | $absPathParts = \explode(DIRECTORY_SEPARATOR, $absPath); 62 | $nSteps = \count($absPathParts); 63 | 64 | $tmpPath = $absPathParts[0]; 65 | $prevReadable = false; 66 | $prevWritable = false; 67 | 68 | for ($i = 1; $i < $nSteps; $i++) { 69 | $tmpPath .= DIRECTORY_SEPARATOR . $absPathParts[$i]; 70 | 71 | if (\file_exists($tmpPath)) { 72 | if (!\is_dir($tmpPath)) { 73 | if (\is_link($tmpPath)) { 74 | $linkPath = \readlink($tmpPath); 75 | if (false === $linkPath || !\is_dir($linkPath)) { 76 | return false; 77 | } 78 | $tmpPath = $linkPath; 79 | } else { 80 | return false; 81 | } 82 | } 83 | 84 | $prevReadable = \is_readable($tmpPath); 85 | $prevWritable = \is_writable($tmpPath); 86 | } else { 87 | return ($prevReadable && $prevWritable); 88 | } 89 | } 90 | 91 | return true; 92 | } 93 | 94 | /** 95 | * @inheritdoc 96 | */ 97 | public function ensurePath(string $path): string 98 | { 99 | $absPath = $this->getAbsolutePath($path); 100 | 101 | if (!\file_exists($absPath) && false === \mkdir($absPath, 0755, true)) { 102 | throw new \RuntimeException('Unable to create the specified directory (' . $absPath . ').'); 103 | } 104 | 105 | return $absPath; 106 | } 107 | 108 | protected function isAbsolutePath(string $path): bool 109 | { 110 | return \preg_match('/^[a-z]\:/i', $path) === 1; 111 | } 112 | 113 | protected function getAbsolutePath(string $path): string 114 | { 115 | $path = $this->isAbsolutePath($path) 116 | ? $path 117 | : (\getcwd() . DIRECTORY_SEPARATOR . $path); 118 | 119 | // Normalise directory separators 120 | $path = \preg_replace('/[\/\\\\]/u', DIRECTORY_SEPARATOR, $path); 121 | 122 | return $path; 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/UserFunctions/functions.php: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Coder-Spirit/Jupyter-PHP/2ae08ebdad7f0656225d99e080ccb1cecc331eb5/src/UserFunctions/functions.php -------------------------------------------------------------------------------- /src/kernel.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | error('Unexpected error', ['error' => $e]); 51 | } catch (\Exception $e) { 52 | $logger->error('Unexpected exception', ['exception' => $e]); 53 | } 54 | 55 | function configureLogger(System $system): Logger 56 | { 57 | $logger = new Logger('kernel'); 58 | $loggerActivationStrategy = new ErrorLevelActivationStrategy(LoggerSettings::getCrossFingersLevel()); 59 | 60 | if ('root' === $system->getCurrentUser()) { 61 | if (System::OS_LINUX === $system->getOperativeSystem()) { 62 | $logger->pushHandler( 63 | new FingersCrossedHandler((Logger::DEBUG === $loggerActivationStrategy) 64 | ? (new GroupHandler([ 65 | new SyslogHandler('jupyter-php'), 66 | new StreamHandler('php://stderr') 67 | ])) 68 | : (new SyslogHandler('jupyter-php')), 69 | $loggerActivationStrategy, 70 | 128 71 | ) 72 | ); 73 | } 74 | } else { 75 | $system->ensurePath($system->getAppDataDirectory() . '/logs'); 76 | $logger->pushHandler( 77 | new FingersCrossedHandler((Logger::DEBUG === $loggerActivationStrategy) 78 | ? (new GroupHandler([ 79 | new RotatingFileHandler($system->getAppDataDirectory() . '/logs/error.log', 7), 80 | new StreamHandler('php://stderr') 81 | ])) 82 | : (new RotatingFileHandler($system->getAppDataDirectory() . '/logs/error.log', 7)), 83 | $loggerActivationStrategy, 84 | 128 85 | ) 86 | ); 87 | } 88 | 89 | return $logger; 90 | } 91 | 92 | function startCore(Logger $logger) 93 | { 94 | // Obtain settings 95 | $connectionSettings = ConnectionSettings::get(); 96 | $connUris = ConnectionSettings::getConnectionUris($connectionSettings); 97 | 98 | $logger->debug('Connection settings', [ 99 | 'processId' => \getmypid(), 100 | 'connSettings' => $connectionSettings, 101 | 'connUris' => $connUris 102 | ]); 103 | 104 | $kernelCore = new KernelCore( 105 | new JupyterBroker( 106 | $connectionSettings['key'], 107 | $connectionSettings['signature_scheme'], 108 | Uuid::uuid4(), 109 | $logger->withName('JupyterBroker') 110 | ), 111 | $connUris, 112 | $logger->withName('KernelCore') 113 | ); 114 | 115 | $kernelCore->run(); 116 | } 117 | --------------------------------------------------------------------------------