├── .gitignore ├── dist └── reload.phar ├── .travis.yml ├── src ├── Response │ ├── ResponseWebSocketFrame.php │ ├── Response.php │ └── ResponseWebSocket.php ├── Protocol │ ├── WebSocket │ │ ├── MessageQueue.php │ │ ├── WebSocket.php │ │ └── Frame.php │ ├── WebSocketProtocol.php │ ├── LivereloadProtocol.php │ └── HttpProtocol.php ├── Command │ ├── InitCommand.php │ └── RunCommand.php └── Application │ └── ServerApplication.php ├── box.json ├── Tests ├── Response │ ├── ResponseWebSocketFrameTest.php │ └── ResponseWebSocketTest.php ├── Protocol │ ├── WebSocket │ │ ├── WebSocketTest.php │ │ ├── MessageQueueTest.php │ │ └── FrameTest.php │ └── HttpProtocolTest.php └── Application │ └── ServerApplicationTest.php ├── composer.json ├── phpunit.xml.dist ├── bin └── reload ├── LICENSE ├── README.md └── web └── js └── livereload.js /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor/ 2 | /phpunit.xml 3 | /build 4 | /composer.lock 5 | -------------------------------------------------------------------------------- /dist/reload.phar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/RickySu/php-livereload/HEAD/dist/reload.phar -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.4 4 | - 5.5 5 | - 5.6 6 | - hhvm 7 | before_script: 8 | - composer self-update 9 | - composer install 10 | - composer require satooshi/php-coveralls 11 | script: 12 | - phpunit 13 | after_script: 14 | - php vendor/bin/coveralls -v 15 | -------------------------------------------------------------------------------- /src/Response/ResponseWebSocketFrame.php: -------------------------------------------------------------------------------- 1 | frame = $frame; 18 | } 19 | 20 | public function __toString() 21 | { 22 | return $this->frame->encode(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /box.json: -------------------------------------------------------------------------------- 1 | { 2 | "chmod": "0755", 3 | "directories": ["src"], 4 | "extract": true, 5 | "files": [ 6 | "LICENSE", 7 | "web/js/livereload.js" 8 | ], 9 | "finder": [ 10 | { 11 | "name": "*.php", 12 | "exclude": [ 13 | "Tests" 14 | ], 15 | "in": "vendor" 16 | } 17 | ], 18 | "git-commit": "git-commit", 19 | "git-version": "git-version", 20 | "main": "bin/reload", 21 | "output": "dist/reload.phar", 22 | "web": false, 23 | "stub": true 24 | } 25 | -------------------------------------------------------------------------------- /src/Response/Response.php: -------------------------------------------------------------------------------- 1 | headers->set('Content-Type', "$type; charset=$charset"); 17 | } 18 | 19 | public function __toString() 20 | { 21 | $this->headers->set('Content-Length', strlen($this->getContent())); 22 | 23 | return parent::__toString(); 24 | } 25 | } -------------------------------------------------------------------------------- /Tests/Response/ResponseWebSocketFrameTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($frame->encode(), (string) $response); 19 | } 20 | 21 | } -------------------------------------------------------------------------------- /Tests/Response/ResponseWebSocketTest.php: -------------------------------------------------------------------------------- 1 | assertEquals(101, $response->getStatusCode()); 17 | $this->assertEquals('Upgrade', $response->headers->get('Connection')); 18 | $this->assertEquals('websocket', $response->headers->get('Upgrade')); 19 | } 20 | 21 | } 22 | -------------------------------------------------------------------------------- /src/Response/ResponseWebSocket.php: -------------------------------------------------------------------------------- 1 | headers->set('Connection', 'Upgrade'); 17 | $this->headers->set('Upgrade', 'websocket'); 18 | $this->headers->remove('Content-Length'); 19 | $this->headers->remove('Cache-Control'); 20 | $this->headers->remove('Date'); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rickysu/php-livereload", 3 | "description": "a livereload server implement by php", 4 | "license": "MIT", 5 | "authors": [ 6 | { 7 | "name": "Ricky Su", 8 | "email": "ricky@ez2.us" 9 | } 10 | ], 11 | "require": { 12 | "symfony/console": "^2.6||^3.0", 13 | "react/socket": "^0.4.0", 14 | "symfony/finder": "^2.6||^3.0", 15 | "symfony/http-foundation": "^2.6||^3.0" 16 | }, 17 | "require-dev": { 18 | "phpunit/phpunit": "^4.8" 19 | }, 20 | "autoload": { 21 | "psr-4": { 22 | "PHPLivereload\\": "src" 23 | } 24 | }, 25 | "autoload-dev": { 26 | "psr-4": { 27 | "PHPLivereload\\Tests\\": "Tests" 28 | } 29 | }, 30 | "bin": ["bin/reload"] 31 | } 32 | -------------------------------------------------------------------------------- /src/Protocol/WebSocket/MessageQueue.php: -------------------------------------------------------------------------------- 1 | data = $data; 16 | } 17 | 18 | public function clear() 19 | { 20 | $this->data = ''; 21 | } 22 | public function add($data) 23 | { 24 | $this->data.=$data; 25 | } 26 | 27 | public function __toString() 28 | { 29 | return $this->getAll(); 30 | } 31 | 32 | public function getAll() 33 | { 34 | return $this->data; 35 | } 36 | 37 | public function shift($size) 38 | { 39 | if ($size > strlen($this->data)) { 40 | return false; 41 | } 42 | 43 | $message = substr($this->data, 0, $size); 44 | 45 | $this->data = substr($this->data, $size); 46 | 47 | return $message; 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ./Tests/ 16 | 17 | 18 | 19 | 20 | 21 | ./ 22 | 23 | ./Tests 24 | ./vendor 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | -------------------------------------------------------------------------------- /bin/reload: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | files()->in(__DIR__.'/../src/Command/'); 25 | 26 | $application = new Application('php livereload', $appVersion); 27 | 28 | foreach ($finder as $file) { 29 | $class = 'PHPLivereload\\Command\\'.substr($file->getFileName(), 0, - (strlen($file->getExtension())+1)); 30 | $application->add(new $class); 31 | } 32 | 33 | $application->run(); 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Ricky Su 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 | 23 | -------------------------------------------------------------------------------- /Tests/Protocol/WebSocket/WebSocketTest.php: -------------------------------------------------------------------------------- 1 | headers->set('Upgrade', 'websocket'); 22 | $request->headers->set('Sec-WebSocket-Version', 13); 23 | $request->headers->set('Sec-WebSocket-Key', $key); 24 | $websocket = new WebSocket(); 25 | $response = $websocket->handshake($request); 26 | $this->assertTrue($response instanceof ResponseWebSocket); 27 | $this->assertEquals($acceptKey, $response->headers->get('Sec-WebSocket-Accept')); 28 | } 29 | 30 | public function provider_test_getHandshakeReponse() 31 | { 32 | return array( 33 | array('8MXSz0cH4mjNrI2d+w9Mbw==', 'JDvj7/uPnMEWPrAUvYR9SD+T2XI='), 34 | array('MAMGHM22VDXYF+s6CW2RUw==', 'TG3bsTWUBOalA6WXocR+xP4DSA0='), 35 | ); 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/Command/InitCommand.php: -------------------------------------------------------------------------------- 1 | setName('livereload:init') 16 | ->setAliases(array('init')) 17 | ->setDescription('Initialize livereload.json.') 18 | ->addOption('force', '-f', InputOption::VALUE_NONE, 'force rewrite livereload.json') 19 | ; 20 | } 21 | 22 | protected function execute(InputInterface $input, OutputInterface $output) 23 | { 24 | $forceRewrite = $input->getOption('force'); 25 | if(!$forceRewrite && file_exists('livereload.json')){ 26 | $output->writeln("livereload.json file exists.\nplease use --force to overwrite."); 27 | return; 28 | } 29 | $this->writeConfig($input, $output); 30 | } 31 | 32 | protected function writeConfig(InputInterface $input, OutputInterface $output) 33 | { 34 | $json = <<writeln("livereload.json is generated."); 47 | } 48 | } -------------------------------------------------------------------------------- /src/Protocol/WebSocket/WebSocket.php: -------------------------------------------------------------------------------- 1 | messageQueue = new MessageQueue(); 21 | } 22 | 23 | /** 24 | * @return Frame 25 | */ 26 | public function onMessage($data) 27 | { 28 | $this->messageQueue->add($data); 29 | 30 | return Frame::parse($this->messageQueue); 31 | } 32 | 33 | public function handshake($request) 34 | { 35 | $key = $request->headers->get('Sec-WebSocket-Key'); 36 | if( 37 | strtolower($request->headers->get('Upgrade')) != 'websocket' || 38 | !$this->checkProtocolVrsion($request) || 39 | !$this->checkSecKey($key) 40 | ){ 41 | return false; 42 | } 43 | $acceptKey = $this->generateAcceptKey($key); 44 | $response = new ResponseWebSocket(); 45 | $response->headers->set('Sec-WebSocket-Accept', $acceptKey); 46 | 47 | return $response; 48 | } 49 | 50 | protected function generateAcceptKey($key) 51 | { 52 | return base64_encode(sha1($key.static::MAGIC_GUID, true)); 53 | } 54 | 55 | protected function checkSecKey($key) 56 | { 57 | return strlen(base64_decode($key)) == 16; 58 | } 59 | 60 | protected function checkProtocolVrsion($request) 61 | { 62 | return $request->headers->get('Sec-WebSocket-Version', 0) >= static::MIN_WS_VERSION; 63 | } 64 | 65 | //put your code here 66 | } 67 | -------------------------------------------------------------------------------- /src/Protocol/WebSocketProtocol.php: -------------------------------------------------------------------------------- 1 | app = $app; 19 | $this->conn = $conn; 20 | $this->initEvent(); 21 | $this->handshake($request); 22 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser connected", OutputInterface::VERBOSITY_VERBOSE); 23 | new LivereloadProtocol($conn, $app); 24 | } 25 | 26 | protected function handshake(HttpFoundation\Request $request) 27 | { 28 | if (!($handshakeResponse = $this->websocket->handshake($request))) { 29 | $this->conn->write(new Response('bad protocol', 400), true); 30 | return; 31 | } 32 | $this->conn->write($handshakeResponse); 33 | } 34 | 35 | protected function initEvent() 36 | { 37 | $this->websocket = new WebSocket\WebSocket(); 38 | $this->conn->on('data', function($data){ 39 | $this->onData($data); 40 | }); 41 | } 42 | 43 | protected function onData($data) 44 | { 45 | $frame = $this->websocket->onMessage($data); 46 | if(!($frame instanceof WebSocket\Frame)) { 47 | return; 48 | } 49 | if(($command = json_decode($frame->getData(), true)) === null){ 50 | return; 51 | } 52 | $this->conn->emit('command', array($command)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/Command/RunCommand.php: -------------------------------------------------------------------------------- 1 | setName('server:run') 16 | ->setAliases(array('sr')) 17 | ->setDescription('Starts a live reload server.') 18 | ->addArgument('address', InputArgument::OPTIONAL, 'Address:port', '127.0.0.1:35729') 19 | ->addOption('config', '-c', InputOption::VALUE_OPTIONAL, 'Path to livereload.json') 20 | ->addOption('no-watch', '', InputOption::VALUE_NONE, 'Disable watching') 21 | ; 22 | } 23 | 24 | protected function execute(InputInterface $input, OutputInterface $output) 25 | { 26 | $noWatching = $input->getOption('no-watch'); 27 | $address = $input->getArgument('address', '127.0.0.1:35729'); 28 | list($host, $port) = explode(':', $address); 29 | $output->writeln(sprintf("Server running on http://%s\n", $input->getArgument('address'))); 30 | $output->writeln('Quit the server with CONTROL-C.'); 31 | $output->writeln(''); 32 | $app = new ServerApplication($host, $port); 33 | if(!$noWatching){ 34 | $config = $this->loadConfig($input, $output); 35 | $app->watching($config['period'], $config); 36 | } 37 | $app->setOutput($output); 38 | $app->run(); 39 | } 40 | 41 | protected function loadConfig(InputInterface $input, OutputInterface $output) 42 | { 43 | $configFile = $input->getOption('config'); 44 | if($configFile === null){ 45 | $configFile = 'livereload.json'; 46 | } 47 | if(!file_exists($configFile)){ 48 | throw new \Exception("$configFile not found."); 49 | } 50 | 51 | $config = json_decode(file_get_contents($configFile), true); 52 | return $config; 53 | } 54 | } -------------------------------------------------------------------------------- /Tests/Protocol/WebSocket/MessageQueueTest.php: -------------------------------------------------------------------------------- 1 | $property; 12 | } 13 | public function __set($name, $value) 14 | { 15 | $property = str_replace('_','', $name); 16 | $this->$property = $value; 17 | } 18 | } 19 | 20 | /** 21 | * Description of Message 22 | * 23 | * @author ricky 24 | */ 25 | class MessageQueueTest extends \PHPUnit_Framework_TestCase 26 | { 27 | 28 | protected $messageQueue; 29 | 30 | protected function setUp() 31 | { 32 | $this->messageQueue = new myMessageQueue(); 33 | } 34 | 35 | public function test_add() 36 | { 37 | $token = md5(microtime().rand()); 38 | $this->messageQueue->add($token); 39 | $this->messageQueue->add($token); 40 | $this->assertEquals($token.$token, $this->messageQueue->_data); 41 | } 42 | 43 | public function test_clear() 44 | { 45 | $token = md5(microtime().rand()); 46 | $this->messageQueue->add($token); 47 | $this->messageQueue->clear(); 48 | $this->assertEmpty($this->messageQueue->_data); 49 | } 50 | 51 | public function test_getAll() 52 | { 53 | $token = md5(microtime().rand()); 54 | $this->messageQueue->clear(); 55 | $this->messageQueue->add($token); 56 | $this->assertEquals($token, $this->messageQueue->getAll()); 57 | } 58 | 59 | public function test__toString() 60 | { 61 | $token = md5(microtime().rand()); 62 | $this->messageQueue->clear(); 63 | $this->messageQueue->add($token); 64 | $this->assertEquals($this->messageQueue, $this->messageQueue->getAll()); 65 | } 66 | 67 | public function test_shift() 68 | { 69 | $token = md5(microtime().rand()); 70 | $this->messageQueue->clear(); 71 | $this->messageQueue->add($token); 72 | $part = $this->messageQueue->shift(4); 73 | $this->assertEquals(substr($token, 0, 4), $part); 74 | $this->assertEquals(substr($token, 4), $this->messageQueue->getAll()); 75 | } 76 | 77 | } 78 | 79 | -------------------------------------------------------------------------------- /src/Protocol/LivereloadProtocol.php: -------------------------------------------------------------------------------- 1 | conn = $conn; 18 | $this->app = $app; 19 | $this->app->addClient($this); 20 | $this->initEvent(); 21 | } 22 | 23 | public function reload($file, $config) 24 | { 25 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser reload $file", OutputInterface::VERBOSITY_VERBOSE); 26 | $this->sendCommand(array( 27 | 'command' => 'reload', 28 | 'path' => $file, 29 | 'liveCSS' => $config['liveCSS'], 30 | )); 31 | } 32 | 33 | protected function shutdown() 34 | { 35 | $this->conn->removeAllListeners(); 36 | $this->app->getOutput()->writeln(strftime('%T')." - info - Browser disconnected", OutputInterface::VERBOSITY_VERBOSE); 37 | $this->app->removeClient($this); 38 | unset($this->app); 39 | unset($this->conn); 40 | } 41 | 42 | protected function initEvent() 43 | { 44 | $this->conn->on('command', function($command){ 45 | $this->dispatchCommand($command); 46 | }); 47 | $this->conn->on('close', function(){ 48 | $this->shutdown(); 49 | }); 50 | } 51 | 52 | protected function sendRaw($data) 53 | { 54 | $response = new ResponseWebSocketFrame(WebSocket\Frame::generate($data)); 55 | $this->conn->write($response); 56 | } 57 | 58 | protected function sendCommand($command) 59 | { 60 | $this->sendRaw(json_encode($command)); 61 | } 62 | 63 | protected function dispatchCommand($command) 64 | { 65 | switch($command['command']){ 66 | case 'hello': 67 | $this->processCommandHello($command); 68 | break; 69 | default: 70 | //$this->conn->end(); 71 | } 72 | } 73 | 74 | protected function processCommandHello($command) 75 | { 76 | if($this->connected){ 77 | return; 78 | } 79 | $this->app->getOutput()->writeln(strftime('%T')." - info - Livereload protocol initialized.", OutputInterface::VERBOSITY_VERBOSE); 80 | $this->connected = true; 81 | $this->sendCommand(array( 82 | 'command' => 'hello', 83 | 'protocols' => array( 84 | 'http://livereload.com/protocols/official-7', 85 | ), 86 | 'serverName' => 'php-livereload', 87 | )); 88 | } 89 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # PHP Livereload 2 | 3 | [![Build Status](https://travis-ci.org/RickySu/php-livereload.svg?branch=master)](https://travis-ci.org/RickySu/php-livereload) 4 | [![Coverage Status](https://coveralls.io/repos/RickySu/php-livereload/badge.svg?branch=master)](https://coveralls.io/r/RickySu/php-livereload?branch=master) 5 | 6 | php-livereload is a livereload server written in PHP. 7 | 8 | php-livereload uses [livereload.js](https://github.com/livereload/livereload-js) -- a JavaScript file implementing the client side of the LiveReload protocol. 9 | 10 | ## Install 11 | 12 | Install php-livereload from [composer](http://getcomposer.org). 13 | 14 | ```JSON 15 | { 16 | "require": { 17 | "rickysu/php-livereload": "dev-master" 18 | } 19 | } 20 | ``` 21 | 22 | Get the command-line php-livereload 23 | 24 | $ curl -O https://raw.github.com/RickySu/php-livereload/master/dist/reload.phar 25 | $ chmod +x reload.phar 26 | $ sudo mv reload.phar /usr/bin 27 | 28 | Install [LiveReload Safari/Chrome/Firefox extension](http://feedback.livereload.com/knowledgebase/articles/86242-how-do-i-install-and-use-the-browser-extensions-) 29 | 30 | ## Tests 31 | 32 | To run the test suite, you need install the dependencies via composer, then 33 | run PHPUnit. 34 | 35 | $ composer install 36 | $ phpunit 37 | 38 | ## Using php-livereload 39 | define a livereload.json in your project root. 40 | 41 | livereload.json 42 | 43 | ```JSON 44 | { 45 | "period": 1, 46 | "watch": { 47 | "web/css/": "*.css", 48 | "web/js/": "*.js", 49 | "web/img/": "\\.png|gif|jpg$" 50 | } 51 | } 52 | ``` 53 | 54 | * period: monitor file changes every 1 second. 55 | * watch: file and folder you want to watch 56 | 57 | #### Initialize a default livereload.json file. 58 | 59 | ``` 60 | $ php bin/reload livereload:init 61 | ``` 62 | 63 | #### Running Server. 64 | 65 | ``` 66 | $ php bin/reload server:run 67 | ``` 68 | 69 | #### Rolling Your Own Live Reload 70 | 71 | If you would like to trigger the live reload server yourself, simply POST files to the URL: `http://localhost:35729/changed`. 72 | Or if you rather roll your own live reload implementation use the following example: 73 | 74 | ``` 75 | # notify a single change 76 | curl http://localhost:35729/changed?files=style.css 77 | 78 | # notify using a longer path 79 | curl http://localhost:35729/changed?files=js/app.js 80 | 81 | # notify multiple changes, comma or space delimited 82 | curl http://localhost:35729/changed?files=index.html,style.css,docs/docco.css 83 | ``` 84 | 85 | Or you can bulk the information into a POST request, with body as a JSON array of files. 86 | 87 | ``` 88 | curl -X POST http://localhost:35729/changed -d '{ "files": ["style.css", "app.js"] }' 89 | 90 | # from a JSON file 91 | node -pe 'JSON.stringify({ files: ["some.css", "files.css"] })' > files.json 92 | curl -X POST -d @files.json http://localhost:35729 93 | ``` 94 | 95 | ## License 96 | 97 | MIT, see LICENSE. 98 | -------------------------------------------------------------------------------- /Tests/Protocol/WebSocket/FrameTest.php: -------------------------------------------------------------------------------- 1 | assertEquals($frameData, $closeFrame->encode()); 19 | } 20 | 21 | /** 22 | * @dataProvider provider_test_generate 23 | */ 24 | public function test_generate($expededEncodedData, $data) 25 | { 26 | $frame = Frame::generate($data); 27 | $this->assertEquals(md5($expededEncodedData), md5($frame->encode())); 28 | } 29 | 30 | public function provider_test_generate() 31 | { 32 | $tinyData = md5(microtime().rand()); 33 | $tinyDataEncoded = pack("CC", 0x81, strlen($tinyData)).$tinyData; 34 | $mediumData = str_pad(md5(microtime().rand()), rand(126, 65535), md5(microtime().rand())); //16 bit 35 | $mediumDataEncoded = pack("CCn", 0x81, 126, strlen($mediumData)).$mediumData; 36 | $hugeData = str_pad(md5(microtime().rand()), rand(65536, 100000), md5(microtime().rand())); //64 bit 37 | $hugeDataEncoded = pack("CCNN", 0x81, 127, strlen($hugeData)>>32, strlen($hugeData) & 0xffffffff).$hugeData; 38 | return array( 39 | array($tinyDataEncoded, $tinyData), 40 | array($mediumDataEncoded, $mediumData), 41 | array($hugeDataEncoded, $hugeData), 42 | ); 43 | } 44 | 45 | public function provider_test_parse() 46 | { 47 | $applyMask = function($maskingKey, $data) 48 | { 49 | $applied = ''; 50 | $maskingKeyLength = strlen($maskingKey); 51 | for ($i = 0, $len = strlen($data); $i < $len; $i++) { 52 | $applied .= $data[$i] ^ $maskingKey[$i % $maskingKeyLength]; 53 | } 54 | return $applied; 55 | }; 56 | 57 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535)); 58 | $tinyData = md5(microtime().rand()); 59 | $tinyDataEncoded = pack("CC", 0x81, strlen($tinyData)|0x80).$maskgingKey.$applyMask($maskgingKey, $tinyData); 60 | 61 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535)); 62 | $mediumData = str_pad(md5(microtime().rand()), rand(126, 65535), md5(microtime().rand())); //16 bit 63 | $mediumDataEncoded = pack("CCn", 0x81, 126|0x80, strlen($mediumData)).$maskgingKey.$applyMask($maskgingKey, $mediumData); 64 | 65 | $maskgingKey = pack("nn", rand(0, 65535), rand(0, 65535)); 66 | $hugeData = str_pad(md5(microtime().rand()), rand(65536, 100000), md5(microtime().rand())); //64 bit 67 | $hugeDataEncoded = pack("CCNN", 0x81, 127|0x80, strlen($hugeData)>>32, strlen($hugeData) & 0xffffffff).$maskgingKey.$applyMask($maskgingKey, $hugeData); 68 | return array( 69 | array($tinyDataEncoded, $tinyData), 70 | array($mediumDataEncoded, $mediumData), 71 | array($hugeDataEncoded, $hugeData), 72 | ); 73 | } 74 | 75 | /** 76 | * @dataProvider provider_test_parse 77 | */ 78 | public function test_parse($encodedData, $expededData) 79 | { 80 | $frame = Frame::parse($encodedData); 81 | $this->assertEquals(md5($expededData), md5($frame->getData())); 82 | } 83 | 84 | } 85 | 86 | -------------------------------------------------------------------------------- /src/Application/ServerApplication.php: -------------------------------------------------------------------------------- 1 | true, 17 | ); 18 | protected $watchConfig; 19 | protected $watchingFiles = array(); 20 | 21 | public function __construct($host = '127.0.0.1', $port = 35729) 22 | { 23 | $this->initLoop(); 24 | $this->initServer($host, $port); 25 | } 26 | 27 | public function setOutput(OutputInterface $output) 28 | { 29 | $this->output = $output; 30 | } 31 | 32 | /** 33 | * 34 | * @return OutputInterface 35 | */ 36 | public function getOutput() 37 | { 38 | return $this->output; 39 | } 40 | 41 | public function run() 42 | { 43 | $this->loop->run(); 44 | } 45 | 46 | public function getConfig() 47 | { 48 | return $this->config; 49 | } 50 | 51 | public function watching($time, $config) 52 | { 53 | $this->watchConfig = $config; 54 | $this->scanFiles(); 55 | $this->loop->addPeriodicTimer($time, function(){ 56 | $this->watchingFileChange(); 57 | }); 58 | } 59 | 60 | protected function scanFiles($scanNewFile = true) 61 | { 62 | foreach($this->watchConfig['watch'] as $path => $file){ 63 | $finder = new Finder(); 64 | try{ 65 | foreach($finder->in($path)->name($file)->followLinks() as $file){ 66 | if($file->getRealPath() && !isset($this->watchingFiles[$file->getRealpath()])){ 67 | $this->watchingFiles[$file->getRealpath()] = $scanNewFile?$file->getMTime():0; 68 | } 69 | } 70 | } 71 | catch(\InvalidArgumentException $e){ 72 | continue; 73 | } 74 | } 75 | } 76 | 77 | protected function watchingFileChange() 78 | { 79 | $this->scanFiles(false); 80 | foreach($this->watchingFiles as $file => $time){ 81 | $mtime = @filemtime($file); 82 | if($mtime && $mtime > $time){ 83 | $this->watchingFiles[$file] = $mtime; 84 | $this->reloadFile($file); 85 | } 86 | } 87 | } 88 | 89 | protected function initLoop() 90 | { 91 | $this->loop = LoopFactory::create(); 92 | } 93 | 94 | protected function initServer($host, $port) 95 | { 96 | $socket = new SocketServer($this->loop); 97 | $socket->listen($port, $host); 98 | return new Protocol\HttpProtocol($socket, $this); 99 | } 100 | 101 | public function addClient(Protocol\LivereloadProtocol $client) 102 | { 103 | if(!in_array($client, $this->clients)){ 104 | $this->clients[] = $client; 105 | } 106 | } 107 | 108 | public function removeClient(Protocol\LivereloadProtocol $client) 109 | { 110 | $index = array_search($client, $this->clients, true); 111 | if($index === false){ 112 | return; 113 | } 114 | unset($this->clients[$index]); 115 | } 116 | 117 | public function reloadFile($file) 118 | { 119 | foreach($this->clients as $client){ 120 | $client->reload($file, $this->config); 121 | } 122 | } 123 | } -------------------------------------------------------------------------------- /src/Protocol/HttpProtocol.php: -------------------------------------------------------------------------------- 1 | app = $app; 18 | $this->initEvent($socket); 19 | } 20 | 21 | protected function initEvent(SocketServer $socket) 22 | { 23 | $socket->on('connection', function(SocketConnection $conn){ 24 | $this->onConnect($conn); 25 | }); 26 | } 27 | 28 | protected function onConnect(SocketConnection $conn) 29 | { 30 | $conn->on('data', function($data) use($conn){ 31 | $this->onData($conn, $data); 32 | }); 33 | } 34 | 35 | protected function onData(SocketConnection $conn, $data) 36 | { 37 | $request = $this->doHttpHandshake($data); 38 | $this->handleRequest($conn, $request); 39 | } 40 | 41 | protected function handleRequest(SocketConnection $conn, Request $request) 42 | { 43 | switch($request->getPathInfo()){ 44 | case '/livereload': 45 | $this->initWebSocket($conn, $request); 46 | break; 47 | case '/livereload.js': 48 | $this->serveFile($conn, __DIR__.'/../../web/js/livereload.js'); 49 | break; 50 | case '/changed': 51 | $this->notifyChanged($conn, $request); 52 | break; 53 | default: 54 | $this->serve404Error($conn); 55 | } 56 | } 57 | 58 | protected function initWebSocket(SocketConnection $conn, Request $request) 59 | { 60 | $conn->removeAllListeners('data'); 61 | return new WebSocketProtocol($conn, $this->app, $request); 62 | } 63 | 64 | protected function getRequestChangedFiles(Request $request) 65 | { 66 | if(($files = $request->query->get('files')) != null){ 67 | return explode(',', $files); 68 | } 69 | $requestJson = json_decode($request->getContent(), true); 70 | return isset($requestJson['files'])?(is_array($requestJson['files'])?$requestJson['files']:[$requestJson['files']]):[]; 71 | } 72 | 73 | protected function notifyChanged(SocketConnection $conn, Request $request) 74 | { 75 | foreach($this->getRequestChangedFiles($request) as $file){ 76 | $this->app->getOutput()->writeln(strftime('%T')." - info - Receive request reload $file", OutputInterface::VERBOSITY_VERBOSE); 77 | $this->app->reloadFile($file); 78 | } 79 | $response = new Response(json_encode(array('status' => true))); 80 | $conn->write($response); 81 | } 82 | 83 | protected function serveFile(SocketConnection $conn, $file) 84 | { 85 | if(($path = realpath($file)) === null){ 86 | return ; 87 | } 88 | $content = file_get_contents($file); 89 | $response = new Response($content); 90 | $response->setContentType('text/plain', 'utf-8'); 91 | $conn->write($response); 92 | } 93 | 94 | protected function serve404Error(SocketConnection $conn) 95 | { 96 | $response = new Response('file not found.', Response::HTTP_NOT_FOUND); 97 | $conn->write($response); 98 | $conn->end(); 99 | } 100 | 101 | protected function doHttpHandshake($data) 102 | { 103 | $pos = strpos($data, "\r\n\r\n"); 104 | if($pos === false){ 105 | return false; 106 | } 107 | $body = substr($data, $pos + 4); 108 | $rawHeaders = explode("\r\n", substr($data, 0, $pos)); 109 | $requestLine = $this->parseRequest(array_shift($rawHeaders)); 110 | $headers = $this->parseHeaders($rawHeaders); 111 | return Request::create($requestLine['uri'], $requestLine['method'], array(), array(), array(), $headers, $body); 112 | } 113 | 114 | protected function parseRequest($rawRequest) 115 | { 116 | return array_combine(array('method', 'uri', 'protocol'), explode(' ', $rawRequest)); 117 | } 118 | 119 | protected function parseHeaders($rawHeaders) 120 | { 121 | $headers = array(); 122 | foreach($rawHeaders as $headerLine){ 123 | if(($pos = strpos($headerLine, ':')) === false){ 124 | continue; 125 | } 126 | $headers['HTTP_'.str_replace('-', '_', trim(strtoupper(substr($headerLine, 0, $pos))))] = trim(substr($headerLine, $pos + 1)); 127 | } 128 | return $headers; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/Protocol/WebSocket/Frame.php: -------------------------------------------------------------------------------- 1 | decode($data); 41 | if (!$frame->isCoalesced()) { 42 | return $frameSize; 43 | } 44 | $data->shift($frameSize); 45 | 46 | return $frame; 47 | } 48 | 49 | /** 50 | * 51 | * @param string $data 52 | * @return Frame 53 | */ 54 | public static function generate($data) 55 | { 56 | $frame = new static($data); 57 | 58 | return $frame; 59 | } 60 | 61 | /** 62 | * 63 | * @param string $data 64 | * @return Frame 65 | */ 66 | public static function close($data) 67 | { 68 | $frame = new static($data, true, static::OP_CLOSE); 69 | $frame->setClosed(); 70 | 71 | return $frame; 72 | } 73 | 74 | protected function __construct($data = null, $final = true, $opcode = self::OP_TEXT) 75 | { 76 | $this->firstByte = ($final ? 0x80 : 0) + $opcode; 77 | $this->appendData($data); 78 | } 79 | 80 | public function setClosed($isClosed = true) 81 | { 82 | $this->isClosed = $isClosed; 83 | } 84 | 85 | public function isClosed() 86 | { 87 | return $this->isClosed; 88 | } 89 | 90 | public function getOpcode() 91 | { 92 | return $this->firstByte & 0x7f; 93 | } 94 | 95 | public function setFirstByte($byte) 96 | { 97 | $this->firstByte = $byte; 98 | } 99 | 100 | public function setSecondByte($byte) 101 | { 102 | $this->secondByte = $byte; 103 | } 104 | 105 | public function setData($data) 106 | { 107 | $this->data = ''; 108 | $this->appendData($data); 109 | } 110 | 111 | public function appendData($data) 112 | { 113 | $this->data.=$data; 114 | $this->updatePayloadLength($this->data, $this->secondByte, $this->extendedPayload); 115 | } 116 | 117 | public function setMask($mask) 118 | { 119 | $mask &= 1; 120 | $this->secondByte |= ($mask << 7); 121 | } 122 | 123 | public function isMask() 124 | { 125 | return ($this->secondByte >> 7) == 1; 126 | } 127 | 128 | public function generateMaskingKey() 129 | { 130 | $maskingKey = ''; 131 | for ($i = 0; $i < static::MASK_LENGTH; $i++) { 132 | $maskingKey .= pack('C', rand(0, 255)); 133 | } 134 | 135 | return $maskingKey; 136 | } 137 | 138 | protected function applyMask($maskingKey, $payload = null) 139 | { 140 | $applied = ''; 141 | for ($i = 0, $len = strlen($payload); $i < $len; $i++) { 142 | $applied .= $payload[$i] ^ $maskingKey[$i % static::MASK_LENGTH]; 143 | } 144 | 145 | return $applied; 146 | } 147 | 148 | protected function maskPayload($payload) 149 | { 150 | if (!$this->isMask()) { 151 | return $payload; 152 | } 153 | $maskingKey = $this->generateMaskingKey(); 154 | 155 | return $maskingKey . $this->applyMask($maskingKey, $payload); 156 | } 157 | 158 | public function getPayloadLength($encodedData) 159 | { 160 | $length = $this->secondByte & 0x7f; 161 | if ($length < 126) { 162 | return [$length, 0]; 163 | } 164 | 165 | if ($length == 126) { // with 2 bytes extended payload length 166 | if (($packedPayloadLength = substr($encodedData, 2, 2)) === false) { 167 | return [0, 0]; 168 | } 169 | 170 | return [unpack("n", $packedPayloadLength)[1] + 2, 2]; 171 | } 172 | 173 | if ($length == 127) { //with 8 bytes extended payload length 174 | if (($packedPayloadLength = substr($encodedData, 2, 8)) === false) { 175 | return [0, 0]; 176 | } 177 | $payloadLength = unpack("N2", $packedPayloadLength); 178 | 179 | return [($packedPayloadLength[1] << 32) | $packedPayloadLength[2] + 8, 8]; 180 | } 181 | } 182 | 183 | public function decode($encodedData) 184 | { 185 | $this->isCoalesced = false; 186 | if (strlen($encodedData) <= 2) { 187 | return static::DECODE_STATUS_MORE_DATA; 188 | } 189 | $bytes = unpack("C2", $encodedData); 190 | $this->setFirstByte($bytes[1]); 191 | $this->setSecondByte($bytes[2]); 192 | 193 | if (!$this->verifyPayload()) { 194 | return static::DECODE_STATUS_ERROR; 195 | } 196 | list($payloadLength, $extendedPayloadBytes) = $this->getPayloadLength($encodedData); 197 | $totalFramLength = 2 + $payloadLength; 198 | if ($this->isMask()) { 199 | $totalFramLength += static::MASK_LENGTH; 200 | } 201 | if ($payloadLength == 0 || strlen($encodedData) < $totalFramLength) { 202 | return static::DECODE_STATUS_MORE_DATA; 203 | } 204 | $maskingKey = substr($encodedData, 2 + $extendedPayloadBytes, static::MASK_LENGTH); 205 | $data = $this->applyMask($maskingKey, substr($encodedData, 2 + $extendedPayloadBytes + static::MASK_LENGTH)); 206 | $this->setData($data); 207 | if (strlen($encodedData) >= $totalFramLength) { 208 | $this->isCoalesced = true; 209 | } 210 | 211 | return $totalFramLength; 212 | } 213 | 214 | public function isCoalesced() 215 | { 216 | return $this->isCoalesced; 217 | } 218 | 219 | public function getRSV1() 220 | { 221 | return ($this->firstByte & 4) > 0; 222 | } 223 | 224 | public function getRSV2() 225 | { 226 | return ($this->firstByte & 2) > 0; 227 | } 228 | 229 | public function getRSV3() 230 | { 231 | return ($this->firstByte & 1) > 0; 232 | } 233 | 234 | protected function verifyPayload() 235 | { 236 | if ($this->getRSV1() && $this->getRSV2() && $this->getRSV3()) { 237 | return false; 238 | } 239 | 240 | return true; 241 | } 242 | 243 | public function getData() 244 | { 245 | return $this->data; 246 | } 247 | 248 | public function encode() 249 | { 250 | $this->isCoalesced = true; 251 | 252 | return pack('CC', $this->firstByte, $this->secondByte) . $this->extendedPayload . $this->maskPayload($this->data); 253 | } 254 | 255 | protected function updatePayloadLength($data, &$secondByte, &$extendedPayload) 256 | { 257 | $secondByte &= 0x80; 258 | $size = strlen($data); 259 | if ($size < 126) { 260 | $secondByte |= $size; 261 | 262 | return; 263 | } 264 | 265 | if ($size <= 65535) { //use 2 bytes extended payload 266 | $secondByte |= 126; 267 | $extendedPayload = pack("n", $size); 268 | 269 | return; 270 | } 271 | 272 | //use 4 bytes extended payload 273 | $secondByte |= 127; 274 | $extendedPayload = pack("NN", $size >> 32, $size & 0xffffffff); 275 | } 276 | 277 | } 278 | -------------------------------------------------------------------------------- /Tests/Protocol/HttpProtocolTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\\PHPLivereload\\Application\\ServerApplication') 18 | ->disableOriginalConstructor() 19 | ->getMock(); 20 | $socket = $this->getMockBuilder('\\React\\Socket\\Server') 21 | ->disableOriginalConstructor() 22 | ->getMock(); 23 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 24 | ->setMethods(array('initEvent')) 25 | ->disableOriginalConstructor() 26 | ->getMock(); 27 | $httpProtocol->expects($this->any()) 28 | ->method('initEvent') 29 | ->will($this->returnCallback(function() use(&$calls){ 30 | $calls[] = 'initEvent'; 31 | })); 32 | 33 | $reflectedClass = new \ReflectionClass($httpProtocol); 34 | $constructor = $reflectedClass->getConstructor(); 35 | $constructor->invoke($httpProtocol, $socket, $app); 36 | $this->assertEquals($app, $this->getObjectAttribute($httpProtocol, 'app')); 37 | $this->assertEquals(array('initEvent'), $calls); 38 | } 39 | 40 | public function test_initEvent() 41 | { 42 | $calls = []; 43 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 44 | ->disableOriginalConstructor() 45 | ->getMock(); 46 | $socket = $this->getMockBuilder('\\React\\Socket\\Server') 47 | ->setMethods(array('on')) 48 | ->disableOriginalConstructor() 49 | ->getMock(); 50 | $socket->expects($this->any()) 51 | ->method('on') 52 | ->will($this->returnCallback(function($event, $callback) use(&$calls, $socketConn){ 53 | $calls[] = 'on'; 54 | $callback($socketConn); 55 | $this->assertEquals('connection', $event); 56 | })); 57 | 58 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 59 | ->setMethods(array('onConnect')) 60 | ->disableOriginalConstructor() 61 | ->getMock(); 62 | $httpProtocol->expects($this->any()) 63 | ->method('onConnect') 64 | ->will($this->returnCallback(function($conn) use(&$calls, $socketConn){ 65 | $calls[] = 'onConnect'; 66 | $this->assertEquals($socketConn, $conn); 67 | })); 68 | 69 | $reflectedClass = new \ReflectionClass($httpProtocol); 70 | $method = $reflectedClass->getMethod('initEvent'); 71 | $method->setAccessible(true); 72 | $method->invoke($httpProtocol, $socket); 73 | $this->assertEquals(array('on', 'onConnect'), $calls); 74 | } 75 | 76 | public function test_onConnect() 77 | { 78 | $calls = []; 79 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 80 | ->setMethods(array('on')) 81 | ->disableOriginalConstructor() 82 | ->getMock(); 83 | $socketConn->expects($this->any()) 84 | ->method('on') 85 | ->will($this->returnCallback(function($event, $callback) use(&$calls, $socketConn){ 86 | $calls[] = 'on'; 87 | $callback('dataForReceive'); 88 | $this->assertEquals('data', $event); 89 | })); 90 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 91 | ->setMethods(array('onData')) 92 | ->disableOriginalConstructor() 93 | ->getMock(); 94 | $httpProtocol->expects($this->any()) 95 | ->method('onData') 96 | ->will($this->returnCallback(function($conn, $data) use(&$calls, $socketConn){ 97 | $calls[] = 'onData'; 98 | $this->assertEquals('dataForReceive', $data); 99 | $this->assertEquals($socketConn, $conn); 100 | })); 101 | $reflectedClass = new \ReflectionClass($httpProtocol); 102 | $method = $reflectedClass->getMethod('onConnect'); 103 | $method->setAccessible(true); 104 | $method->invoke($httpProtocol, $socketConn); 105 | $this->assertEquals(array('on', 'onData'), $calls); 106 | } 107 | 108 | public function test_onData() 109 | { 110 | $calls = []; 111 | $request = new Request(); 112 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 113 | ->disableOriginalConstructor() 114 | ->getMock(); 115 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 116 | ->setMethods(array('doHttpHandshake', 'handleRequest')) 117 | ->disableOriginalConstructor() 118 | ->getMock(); 119 | $httpProtocol->expects($this->any()) 120 | ->method('doHttpHandshake') 121 | ->will($this->returnCallback(function($data) use(&$calls, $request){ 122 | $calls[] = 'doHttpHandshake'; 123 | $this->assertEquals('dataForReceive', $data); 124 | return $request; 125 | })); 126 | $httpProtocol->expects($this->any()) 127 | ->method('handleRequest') 128 | ->will($this->returnCallback(function($conn, $requestForTest) use(&$calls, $socketConn, $request){ 129 | $calls[] = 'handleRequest'; 130 | $this->assertEquals($socketConn, $conn); 131 | $this->assertEquals($request, $requestForTest); 132 | })); 133 | $reflectedClass = new \ReflectionClass($httpProtocol); 134 | $method = $reflectedClass->getMethod('onData'); 135 | $method->setAccessible(true); 136 | $method->invoke($httpProtocol, $socketConn, 'dataForReceive'); 137 | $this->assertEquals(array('doHttpHandshake', 'handleRequest'), $calls); 138 | } 139 | 140 | public function test_handleRequest() 141 | { 142 | $calls = []; 143 | $pathInfo = ''; 144 | 145 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 146 | ->disableOriginalConstructor() 147 | ->getMock(); 148 | 149 | $request = $this->getMockBuilder('\\Symfony\\Component\\HttpFoundation\\Request') 150 | ->setMethods(array('getPathInfo')) 151 | ->disableOriginalConstructor() 152 | ->getMock(); 153 | $request->expects($this->any()) 154 | ->method('getPathInfo') 155 | ->will($this->returnCallback(function() use(&$pathInfo){ 156 | return $pathInfo; 157 | })); 158 | 159 | $methods = array('initWebSocket', 'serveFile', 'notifyChanged', 'serve404Error'); 160 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 161 | ->setMethods($methods) 162 | ->disableOriginalConstructor() 163 | ->getMock(); 164 | foreach($methods as $method){ 165 | $httpProtocol->expects($this->any()) 166 | ->method($method) 167 | ->will($this->returnCallback(function() use(&$calls, $method){ 168 | $calls[] = $method; 169 | })); 170 | } 171 | 172 | $reflectedClass = new \ReflectionClass($httpProtocol); 173 | $method = $reflectedClass->getMethod('handleRequest'); 174 | $method->setAccessible(true); 175 | 176 | $pathInfo = '/livereload'; 177 | $calls = []; 178 | $method->invoke($httpProtocol, $socketConn, $request); 179 | $this->assertEquals(array('initWebSocket'), $calls); 180 | 181 | $pathInfo = '/livereload.js'; 182 | $calls = []; 183 | $method->invoke($httpProtocol, $socketConn, $request); 184 | $this->assertEquals(array('serveFile'), $calls); 185 | 186 | $pathInfo = '/changed'; 187 | $calls = []; 188 | $method->invoke($httpProtocol, $socketConn, $request); 189 | $this->assertEquals(array('notifyChanged'), $calls); 190 | 191 | $pathInfo = '/asjjahkhakjs'; //error 404 192 | $calls = []; 193 | $method->invoke($httpProtocol, $socketConn, $request); 194 | $this->assertEquals(array('serve404Error'), $calls); 195 | } 196 | 197 | public function test_serveFile() 198 | { 199 | $calls = []; 200 | $data = md5(microtime().rand()); 201 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 202 | ->disableOriginalConstructor() 203 | ->getMock(); 204 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 205 | ->disableOriginalConstructor() 206 | ->setMethods(array('write')) 207 | ->getMock(); 208 | $socketConn->expects($this->any()) 209 | ->method('write') 210 | ->will($this->returnCallback(function(Response $response) use($data, &$calls){ 211 | $calls[] = 'write'; 212 | $this->assertEquals($data, $response->getContent()); 213 | $this->assertEquals('text/plain; charset=utf-8', $response->headers->get('Content-Type')); 214 | })); 215 | $reflectedClass = new \ReflectionClass($httpProtocol); 216 | $method = $reflectedClass->getMethod('serveFile'); 217 | $method->setAccessible(true); 218 | 219 | $tempname = tempnam(sys_get_temp_dir(), 'livereloadtest-'); 220 | file_put_contents($tempname, $data); 221 | $method->invoke($httpProtocol, $socketConn, $tempname); 222 | unlink($tempname); 223 | $this->assertEquals(array('write'), $calls); 224 | } 225 | 226 | public function test_serve404Error() 227 | { 228 | $calls = []; 229 | $httpProtocol = $this->getMockBuilder('\\PHPLivereload\\Protocol\\HttpProtocol') 230 | ->disableOriginalConstructor() 231 | ->getMock(); 232 | $socketConn = $this->getMockBuilder('\\React\\Socket\\Connection') 233 | ->disableOriginalConstructor() 234 | ->setMethods(array('write', 'end')) 235 | ->getMock(); 236 | $socketConn->expects($this->any()) 237 | ->method('write') 238 | ->will($this->returnCallback(function(Response $response) use(&$calls){ 239 | $calls[] = 'write'; 240 | $this->assertEquals(Response::HTTP_NOT_FOUND ,$response->getStatusCode()); 241 | })); 242 | $socketConn->expects($this->any()) 243 | ->method('end') 244 | ->will($this->returnCallback(function() use(&$calls){ 245 | $calls[] = 'end'; 246 | })); 247 | $reflectedClass = new \ReflectionClass($httpProtocol); 248 | $method = $reflectedClass->getMethod('serve404Error'); 249 | $method->setAccessible(true); 250 | 251 | $method->invoke($httpProtocol, $socketConn); 252 | $this->assertEquals(array('write', 'end'), $calls); 253 | } 254 | } 255 | -------------------------------------------------------------------------------- /Tests/Application/ServerApplicationTest.php: -------------------------------------------------------------------------------- 1 | getMockBuilder('\\PHPLivereload\\Application\\ServerApplication') 23 | ->disableOriginalConstructor() 24 | ->setMethods(array('initLoop', 'initServer')) 25 | ->getMock(); 26 | $app->expects($this->any()) 27 | ->method('initLoop') 28 | ->will($this->returnCallback(function() use(&$calls){ 29 | $calls[] = 'initLoop'; 30 | })); 31 | $app->expects($this->any()) 32 | ->method('initServer') 33 | ->will($this->returnCallback(function($hostForTest, $portForTest) use(&$calls, $host, $port){ 34 | $calls[] = 'initServer'; 35 | $this->assertEquals($host, $hostForTest); 36 | $this->assertEquals($port, $portForTest); 37 | })); 38 | 39 | $reflectedClass = new \ReflectionClass($app); 40 | $constructor = $reflectedClass->getConstructor(); 41 | $constructor->invoke($app, $host, $port); 42 | $this->assertEquals(array('initLoop', 'initServer'), $calls); 43 | } 44 | 45 | public function test_run() 46 | { 47 | $calls = array(); 48 | $app = new MockServerApplication(); 49 | $loop = $this->getMock('\\React\\EventLoop\\StreamSelectLoop', array('run')); 50 | $loop->expects($this->any()) 51 | ->method('run') 52 | ->will($this->returnCallback(function() use(&$calls){ 53 | $calls[] = 'run'; 54 | })); 55 | $reflectedClass = new \ReflectionClass($app); 56 | $loopProperty = $reflectedClass->getProperty('loop'); 57 | $loopProperty->setAccessible(true); 58 | $loopProperty->setValue($app, $loop); 59 | $app->run(); 60 | $this->assertEquals(array('run'), $calls); 61 | } 62 | 63 | public function test_getConfig() 64 | { 65 | $config = md5(microtime().rand()); 66 | $app = new MockServerApplication(); 67 | $reflectedClass = new \ReflectionClass($app); 68 | $configProperty = $reflectedClass->getProperty('config'); 69 | $configProperty->setAccessible(true); 70 | $configProperty->setValue($app, $config); 71 | $this->assertEquals($config, $app->getConfig()); 72 | } 73 | 74 | public function test_initLoop() 75 | { 76 | $app = new MockServerApplication(); 77 | $reflectedClass = new \ReflectionClass($app); 78 | $initLoop = $reflectedClass->getMethod('initLoop'); 79 | $initLoop->setAccessible(true); 80 | $initLoop->invoke($app); 81 | $loopProperty = $reflectedClass->getProperty('loop'); 82 | $loopProperty->setAccessible(true); 83 | $loop = $loopProperty->getValue($app); 84 | $this->assertTrue($loop instanceof \React\EventLoop\LoopInterface); 85 | } 86 | 87 | public function test_addClient() 88 | { 89 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 90 | ->disableOriginalConstructor() 91 | ->getMock(); 92 | $client2 = clone $client1; 93 | $app = new MockServerApplication(); 94 | $app->addClient($client1); 95 | $reflectedClass = new \ReflectionClass($app); 96 | $clientsProperty = $reflectedClass->getProperty('clients'); 97 | $clientsProperty->setAccessible(true); 98 | $clients = $clientsProperty->getValue($app); 99 | $this->assertEquals(array($client1), $clients); 100 | $app->addClient($client2); 101 | $clients = $clientsProperty->getValue($app); 102 | $this->assertEquals(array($client1, $client2), $clients); 103 | $app->addClient($client1); 104 | $clients = $clientsProperty->getValue($app); 105 | $this->assertEquals(array($client1, $client2), $clients); 106 | } 107 | 108 | public function test_removeClient() 109 | { 110 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 111 | ->disableOriginalConstructor() 112 | ->setMockClassName('client1') 113 | ->getMock(); 114 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 115 | ->disableOriginalConstructor() 116 | ->setMockClassName('client2') 117 | ->getMock(); 118 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 119 | ->disableOriginalConstructor() 120 | ->setMockClassName('client3') 121 | ->getMock(); 122 | $app = new MockServerApplication(); 123 | $app->addClient($client1); 124 | $app->addClient($client2); 125 | $app->addClient($client3); 126 | $app->removeClient($client2); 127 | $reflectedClass = new \ReflectionClass($app); 128 | $clientsProperty = $reflectedClass->getProperty('clients'); 129 | $clientsProperty->setAccessible(true); 130 | $clients = $clientsProperty->getValue($app); 131 | $this->assertEquals(array($client1, $client3), array_values($clients)); 132 | } 133 | 134 | public function test_removeClient_client_is_the_first_element() 135 | { 136 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 137 | ->disableOriginalConstructor() 138 | ->setMockClassName('client1') 139 | ->getMock(); 140 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 141 | ->disableOriginalConstructor() 142 | ->setMockClassName('client2') 143 | ->getMock(); 144 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 145 | ->disableOriginalConstructor() 146 | ->setMockClassName('client3') 147 | ->getMock(); 148 | $app = new MockServerApplication(); 149 | $app->addClient($client1); 150 | $app->addClient($client2); 151 | $app->addClient($client3); 152 | $app->removeClient($client1); 153 | $reflectedClass = new \ReflectionClass($app); 154 | $clientsProperty = $reflectedClass->getProperty('clients'); 155 | $clientsProperty->setAccessible(true); 156 | $clients = $clientsProperty->getValue($app); 157 | $this->assertEquals(array($client2, $client3), array_values($clients)); 158 | } 159 | 160 | public function test_reloadFile() 161 | { 162 | $calls = array(); 163 | $file = md5(microtime().rand()); 164 | $client1 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 165 | ->disableOriginalConstructor() 166 | ->setMockClassName('client1') 167 | ->setMethods(array('reload')) 168 | ->getMock(); 169 | $client1->expects($this->any()) 170 | ->method('reload') 171 | ->will($this->returnCallback(function() use(&$calls){ 172 | $calls[] = 'client1'; 173 | })); 174 | $client2 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 175 | ->disableOriginalConstructor() 176 | ->setMockClassName('client2') 177 | ->setMethods(array('reload')) 178 | ->getMock(); 179 | $client2->expects($this->any()) 180 | ->method('reload') 181 | ->will($this->returnCallback(function() use(&$calls){ 182 | $calls[] = 'client2'; 183 | })); 184 | $client3 = $this->getMockBuilder('\\PHPLivereload\\Protocol\\LivereloadProtocol') 185 | ->disableOriginalConstructor() 186 | ->setMockClassName('client3') 187 | ->setMethods(array('reload')) 188 | ->getMock(); 189 | $client3->expects($this->any()) 190 | ->method('reload') 191 | ->will($this->returnCallback(function() use(&$calls){ 192 | $calls[] = 'client3'; 193 | })); 194 | $app = new MockServerApplication(); 195 | $app->addClient($client1); 196 | $app->addClient($client2); 197 | $app->addClient($client3); 198 | $app->reloadFile($file); 199 | $this->assertEquals(array('client1', 'client2', 'client3'), $calls); 200 | } 201 | 202 | public function test_setOutput() 203 | { 204 | $serverApp = new MockServerApplication(); 205 | $output = $this->getMock('\\Symfony\\Component\\Console\\Output\\OutputInterface'); 206 | $serverApp->setOutput($output); 207 | $this->assertEquals($output, $this->getObjectAttribute($serverApp, 'output')); 208 | } 209 | 210 | public function test_getOutput() 211 | { 212 | $serverApp = new MockServerApplication(); 213 | $output = $this->getMock('\\Symfony\\Component\\Console\\Output\\OutputInterface'); 214 | $reflectedClass = new \ReflectionClass($serverApp); 215 | $properity = $reflectedClass->getProperty('output'); 216 | $properity->setAccessible(true); 217 | $properity->setValue($serverApp, $output); 218 | $this->assertEquals($output, $serverApp->getOutput()); 219 | } 220 | 221 | public function test_watching() 222 | { 223 | $calls = []; 224 | $testConfig = array(1, 2, 3, 4); 225 | $serverApp = $this->getMock('\\PHPLivereload\\Application\\ServerApplication', array('scanFiles', 'watchingFileChange'), array(), '', false); 226 | $serverApp->expects($this->any()) 227 | ->method('scanFiles') 228 | ->will($this->returnCallback(function() use(&$calls){ 229 | $calls[] = 'scanFiles'; 230 | })); 231 | $serverApp->expects($this->any()) 232 | ->method('watchingFileChange') 233 | ->will($this->returnCallback(function() use(&$calls){ 234 | $calls[] = 'watchingFileChange'; 235 | })); 236 | $loop = $this->getMock('\\React\\EventLoop\\StreamSelectLoop', array('addPeriodicTimer')); 237 | $loop->expects($this->any()) 238 | ->method('addPeriodicTimer') 239 | ->will($this->returnCallback(function($time, $callback) use(&$calls){ 240 | $calls[] = 'addPeriodicTimer'; 241 | $this->assertEquals(123, $time); 242 | $callback(); 243 | })); 244 | $reflectedClass = new \ReflectionClass($serverApp); 245 | $properity = $reflectedClass->getProperty('loop'); 246 | $properity->setAccessible(true); 247 | $properity->setValue($serverApp, $loop); 248 | $serverApp->watching(123, $testConfig); 249 | $this->assertEquals(array('scanFiles', 'addPeriodicTimer', 'watchingFileChange'), $calls); 250 | } 251 | 252 | public function test_initServer() 253 | { 254 | $loop = LoopFactory::create(); 255 | $serverApp = new MockServerApplication(); 256 | $reflectedClass = new \ReflectionClass($serverApp); 257 | $properity = $reflectedClass->getProperty('loop'); 258 | $properity->setAccessible(true); 259 | $properity->setValue($serverApp, $loop); 260 | $method = $reflectedClass->getMethod('initServer'); 261 | $method->setAccessible(true); 262 | $result = $method->invoke($serverApp, '127.0.0.1', 8888); 263 | $this->assertTrue($result instanceof Protocol\HttpProtocol); 264 | } 265 | } 266 | -------------------------------------------------------------------------------- /web/js/livereload.js: -------------------------------------------------------------------------------- 1 | (function e(t,n,r){function s(o,u){if(!n[o]){if(!t[o]){var a=typeof require=="function"&&require;if(!u&&a)return a(o,!0);if(i)return i(o,!0);var f=new Error("Cannot find module '"+o+"'");throw f.code="MODULE_NOT_FOUND",f}var l=n[o]={exports:{}};t[o][0].call(l.exports,function(e){var n=t[o][1][e];return s(n?n:e)},l,l.exports,e,t,n,r)}return n[o].exports}var i=typeof require=="function"&&require;for(var o=0;o tag"); 307 | return; 308 | } 309 | this.reloader = new Reloader(this.window, this.console, Timer); 310 | this.connector = new Connector(this.options, this.WebSocket, Timer, { 311 | connecting: (function(_this) { 312 | return function() {}; 313 | })(this), 314 | socketConnected: (function(_this) { 315 | return function() {}; 316 | })(this), 317 | connected: (function(_this) { 318 | return function(protocol) { 319 | var _base; 320 | if (typeof (_base = _this.listeners).connect === "function") { 321 | _base.connect(); 322 | } 323 | _this.log("LiveReload is connected to " + _this.options.host + ":" + _this.options.port + " (protocol v" + protocol + ")."); 324 | return _this.analyze(); 325 | }; 326 | })(this), 327 | error: (function(_this) { 328 | return function(e) { 329 | if (e instanceof ProtocolError) { 330 | if (typeof console !== "undefined" && console !== null) { 331 | return console.log("" + e.message + "."); 332 | } 333 | } else { 334 | if (typeof console !== "undefined" && console !== null) { 335 | return console.log("LiveReload internal error: " + e.message); 336 | } 337 | } 338 | }; 339 | })(this), 340 | disconnected: (function(_this) { 341 | return function(reason, nextDelay) { 342 | var _base; 343 | if (typeof (_base = _this.listeners).disconnect === "function") { 344 | _base.disconnect(); 345 | } 346 | switch (reason) { 347 | case 'cannot-connect': 348 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + ", will retry in " + nextDelay + " sec."); 349 | case 'broken': 350 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + ", reconnecting in " + nextDelay + " sec."); 351 | case 'handshake-timeout': 352 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake timeout), will retry in " + nextDelay + " sec."); 353 | case 'handshake-failed': 354 | return _this.log("LiveReload cannot connect to " + _this.options.host + ":" + _this.options.port + " (handshake failed), will retry in " + nextDelay + " sec."); 355 | case 'manual': 356 | break; 357 | case 'error': 358 | break; 359 | default: 360 | return _this.log("LiveReload disconnected from " + _this.options.host + ":" + _this.options.port + " (" + reason + "), reconnecting in " + nextDelay + " sec."); 361 | } 362 | }; 363 | })(this), 364 | message: (function(_this) { 365 | return function(message) { 366 | switch (message.command) { 367 | case 'reload': 368 | return _this.performReload(message); 369 | case 'alert': 370 | return _this.performAlert(message); 371 | } 372 | }; 373 | })(this) 374 | }); 375 | } 376 | 377 | LiveReload.prototype.on = function(eventName, handler) { 378 | return this.listeners[eventName] = handler; 379 | }; 380 | 381 | LiveReload.prototype.log = function(message) { 382 | return this.console.log("" + message); 383 | }; 384 | 385 | LiveReload.prototype.performReload = function(message) { 386 | var _ref, _ref1; 387 | this.log("LiveReload received reload request: " + (JSON.stringify(message, null, 2))); 388 | return this.reloader.reload(message.path, { 389 | liveCSS: (_ref = message.liveCSS) != null ? _ref : true, 390 | liveImg: (_ref1 = message.liveImg) != null ? _ref1 : true, 391 | originalPath: message.originalPath || '', 392 | overrideURL: message.overrideURL || '', 393 | serverURL: "http://" + this.options.host + ":" + this.options.port 394 | }); 395 | }; 396 | 397 | LiveReload.prototype.performAlert = function(message) { 398 | return alert(message.message); 399 | }; 400 | 401 | LiveReload.prototype.shutDown = function() { 402 | var _base; 403 | this.connector.disconnect(); 404 | this.log("LiveReload disconnected."); 405 | return typeof (_base = this.listeners).shutdown === "function" ? _base.shutdown() : void 0; 406 | }; 407 | 408 | LiveReload.prototype.hasPlugin = function(identifier) { 409 | return !!this.pluginIdentifiers[identifier]; 410 | }; 411 | 412 | LiveReload.prototype.addPlugin = function(pluginClass) { 413 | var plugin; 414 | if (this.hasPlugin(pluginClass.identifier)) { 415 | return; 416 | } 417 | this.pluginIdentifiers[pluginClass.identifier] = true; 418 | plugin = new pluginClass(this.window, { 419 | _livereload: this, 420 | _reloader: this.reloader, 421 | _connector: this.connector, 422 | console: this.console, 423 | Timer: Timer, 424 | generateCacheBustUrl: (function(_this) { 425 | return function(url) { 426 | return _this.reloader.generateCacheBustUrl(url); 427 | }; 428 | })(this) 429 | }); 430 | this.plugins.push(plugin); 431 | this.reloader.addPlugin(plugin); 432 | }; 433 | 434 | LiveReload.prototype.analyze = function() { 435 | var plugin, pluginData, pluginsData, _i, _len, _ref; 436 | if (!(this.connector.protocol >= 7)) { 437 | return; 438 | } 439 | pluginsData = {}; 440 | _ref = this.plugins; 441 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 442 | plugin = _ref[_i]; 443 | pluginsData[plugin.constructor.identifier] = pluginData = (typeof plugin.analyze === "function" ? plugin.analyze() : void 0) || {}; 444 | pluginData.version = plugin.constructor.version; 445 | } 446 | this.connector.sendCommand({ 447 | command: 'info', 448 | plugins: pluginsData, 449 | url: this.window.location.href 450 | }); 451 | }; 452 | 453 | return LiveReload; 454 | 455 | })(); 456 | 457 | }).call(this); 458 | 459 | },{"./connector":1,"./options":5,"./reloader":7,"./timer":9}],5:[function(require,module,exports){ 460 | (function() { 461 | var Options; 462 | 463 | exports.Options = Options = (function() { 464 | function Options() { 465 | this.host = null; 466 | this.port = 35729; 467 | this.snipver = null; 468 | this.ext = null; 469 | this.extver = null; 470 | this.mindelay = 1000; 471 | this.maxdelay = 60000; 472 | this.handshake_timeout = 5000; 473 | } 474 | 475 | Options.prototype.set = function(name, value) { 476 | if (typeof value === 'undefined') { 477 | return; 478 | } 479 | if (!isNaN(+value)) { 480 | value = +value; 481 | } 482 | return this[name] = value; 483 | }; 484 | 485 | return Options; 486 | 487 | })(); 488 | 489 | Options.extract = function(document) { 490 | var element, keyAndValue, m, mm, options, pair, src, _i, _j, _len, _len1, _ref, _ref1; 491 | _ref = document.getElementsByTagName('script'); 492 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 493 | element = _ref[_i]; 494 | if ((src = element.src) && (m = src.match(/^[^:]+:\/\/(.*)\/z?livereload\.js(?:\?(.*))?$/))) { 495 | options = new Options(); 496 | if (mm = m[1].match(/^([^\/:]+)(?::(\d+))?$/)) { 497 | options.host = mm[1]; 498 | if (mm[2]) { 499 | options.port = parseInt(mm[2], 10); 500 | } 501 | } 502 | if (m[2]) { 503 | _ref1 = m[2].split('&'); 504 | for (_j = 0, _len1 = _ref1.length; _j < _len1; _j++) { 505 | pair = _ref1[_j]; 506 | if ((keyAndValue = pair.split('=')).length > 1) { 507 | options.set(keyAndValue[0].replace(/-/g, '_'), keyAndValue.slice(1).join('=')); 508 | } 509 | } 510 | } 511 | return options; 512 | } 513 | } 514 | return null; 515 | }; 516 | 517 | }).call(this); 518 | 519 | },{}],6:[function(require,module,exports){ 520 | (function() { 521 | var PROTOCOL_6, PROTOCOL_7, Parser, ProtocolError, 522 | __indexOf = [].indexOf || function(item) { for (var i = 0, l = this.length; i < l; i++) { if (i in this && this[i] === item) return i; } return -1; }; 523 | 524 | exports.PROTOCOL_6 = PROTOCOL_6 = 'http://livereload.com/protocols/official-6'; 525 | 526 | exports.PROTOCOL_7 = PROTOCOL_7 = 'http://livereload.com/protocols/official-7'; 527 | 528 | exports.ProtocolError = ProtocolError = (function() { 529 | function ProtocolError(reason, data) { 530 | this.message = "LiveReload protocol error (" + reason + ") after receiving data: \"" + data + "\"."; 531 | } 532 | 533 | return ProtocolError; 534 | 535 | })(); 536 | 537 | exports.Parser = Parser = (function() { 538 | function Parser(handlers) { 539 | this.handlers = handlers; 540 | this.reset(); 541 | } 542 | 543 | Parser.prototype.reset = function() { 544 | return this.protocol = null; 545 | }; 546 | 547 | Parser.prototype.process = function(data) { 548 | var command, e, message, options, _ref; 549 | try { 550 | if (this.protocol == null) { 551 | if (data.match(/^!!ver:([\d.]+)$/)) { 552 | this.protocol = 6; 553 | } else if (message = this._parseMessage(data, ['hello'])) { 554 | if (!message.protocols.length) { 555 | throw new ProtocolError("no protocols specified in handshake message"); 556 | } else if (__indexOf.call(message.protocols, PROTOCOL_7) >= 0) { 557 | this.protocol = 7; 558 | } else if (__indexOf.call(message.protocols, PROTOCOL_6) >= 0) { 559 | this.protocol = 6; 560 | } else { 561 | throw new ProtocolError("no supported protocols found"); 562 | } 563 | } 564 | return this.handlers.connected(this.protocol); 565 | } else if (this.protocol === 6) { 566 | message = JSON.parse(data); 567 | if (!message.length) { 568 | throw new ProtocolError("protocol 6 messages must be arrays"); 569 | } 570 | command = message[0], options = message[1]; 571 | if (command !== 'refresh') { 572 | throw new ProtocolError("unknown protocol 6 command"); 573 | } 574 | return this.handlers.message({ 575 | command: 'reload', 576 | path: options.path, 577 | liveCSS: (_ref = options.apply_css_live) != null ? _ref : true 578 | }); 579 | } else { 580 | message = this._parseMessage(data, ['reload', 'alert']); 581 | return this.handlers.message(message); 582 | } 583 | } catch (_error) { 584 | e = _error; 585 | if (e instanceof ProtocolError) { 586 | return this.handlers.error(e); 587 | } else { 588 | throw e; 589 | } 590 | } 591 | }; 592 | 593 | Parser.prototype._parseMessage = function(data, validCommands) { 594 | var e, message, _ref; 595 | try { 596 | message = JSON.parse(data); 597 | } catch (_error) { 598 | e = _error; 599 | throw new ProtocolError('unparsable JSON', data); 600 | } 601 | if (!message.command) { 602 | throw new ProtocolError('missing "command" key', data); 603 | } 604 | if (_ref = message.command, __indexOf.call(validCommands, _ref) < 0) { 605 | throw new ProtocolError("invalid command '" + message.command + "', only valid commands are: " + (validCommands.join(', ')) + ")", data); 606 | } 607 | return message; 608 | }; 609 | 610 | return Parser; 611 | 612 | })(); 613 | 614 | }).call(this); 615 | 616 | },{}],7:[function(require,module,exports){ 617 | (function() { 618 | var IMAGE_STYLES, Reloader, numberOfMatchingSegments, pathFromUrl, pathsMatch, pickBestMatch, splitUrl; 619 | 620 | splitUrl = function(url) { 621 | var hash, index, params; 622 | if ((index = url.indexOf('#')) >= 0) { 623 | hash = url.slice(index); 624 | url = url.slice(0, index); 625 | } else { 626 | hash = ''; 627 | } 628 | if ((index = url.indexOf('?')) >= 0) { 629 | params = url.slice(index); 630 | url = url.slice(0, index); 631 | } else { 632 | params = ''; 633 | } 634 | return { 635 | url: url, 636 | params: params, 637 | hash: hash 638 | }; 639 | }; 640 | 641 | pathFromUrl = function(url) { 642 | var path; 643 | url = splitUrl(url).url; 644 | if (url.indexOf('file://') === 0) { 645 | path = url.replace(/^file:\/\/(localhost)?/, ''); 646 | } else { 647 | path = url.replace(/^([^:]+:)?\/\/([^:\/]+)(:\d*)?\//, '/'); 648 | } 649 | return decodeURIComponent(path); 650 | }; 651 | 652 | pickBestMatch = function(path, objects, pathFunc) { 653 | var bestMatch, object, score, _i, _len; 654 | bestMatch = { 655 | score: 0 656 | }; 657 | for (_i = 0, _len = objects.length; _i < _len; _i++) { 658 | object = objects[_i]; 659 | score = numberOfMatchingSegments(path, pathFunc(object)); 660 | if (score > bestMatch.score) { 661 | bestMatch = { 662 | object: object, 663 | score: score 664 | }; 665 | } 666 | } 667 | if (bestMatch.score > 0) { 668 | return bestMatch; 669 | } else { 670 | return null; 671 | } 672 | }; 673 | 674 | numberOfMatchingSegments = function(path1, path2) { 675 | var comps1, comps2, eqCount, len; 676 | path1 = path1.replace(/^\/+/, '').toLowerCase(); 677 | path2 = path2.replace(/^\/+/, '').toLowerCase(); 678 | if (path1 === path2) { 679 | return 10000; 680 | } 681 | comps1 = path1.split('/').reverse(); 682 | comps2 = path2.split('/').reverse(); 683 | len = Math.min(comps1.length, comps2.length); 684 | eqCount = 0; 685 | while (eqCount < len && comps1[eqCount] === comps2[eqCount]) { 686 | ++eqCount; 687 | } 688 | return eqCount; 689 | }; 690 | 691 | pathsMatch = function(path1, path2) { 692 | return numberOfMatchingSegments(path1, path2) > 0; 693 | }; 694 | 695 | IMAGE_STYLES = [ 696 | { 697 | selector: 'background', 698 | styleNames: ['backgroundImage'] 699 | }, { 700 | selector: 'border', 701 | styleNames: ['borderImage', 'webkitBorderImage', 'MozBorderImage'] 702 | } 703 | ]; 704 | 705 | exports.Reloader = Reloader = (function() { 706 | function Reloader(window, console, Timer) { 707 | this.window = window; 708 | this.console = console; 709 | this.Timer = Timer; 710 | this.document = this.window.document; 711 | this.importCacheWaitPeriod = 200; 712 | this.plugins = []; 713 | } 714 | 715 | Reloader.prototype.addPlugin = function(plugin) { 716 | return this.plugins.push(plugin); 717 | }; 718 | 719 | Reloader.prototype.analyze = function(callback) { 720 | return results; 721 | }; 722 | 723 | Reloader.prototype.reload = function(path, options) { 724 | var plugin, _base, _i, _len, _ref; 725 | this.options = options; 726 | if ((_base = this.options).stylesheetReloadTimeout == null) { 727 | _base.stylesheetReloadTimeout = 15000; 728 | } 729 | _ref = this.plugins; 730 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 731 | plugin = _ref[_i]; 732 | if (plugin.reload && plugin.reload(path, options)) { 733 | return; 734 | } 735 | } 736 | if (options.liveCSS) { 737 | if (path.match(/\.css$/i)) { 738 | if (this.reloadStylesheet(path)) { 739 | return; 740 | } 741 | } 742 | } 743 | if (options.liveImg) { 744 | if (path.match(/\.(jpe?g|png|gif)$/i)) { 745 | this.reloadImages(path); 746 | return; 747 | } 748 | } 749 | return this.reloadPage(); 750 | }; 751 | 752 | Reloader.prototype.reloadPage = function() { 753 | return this.window.document.location.reload(); 754 | }; 755 | 756 | Reloader.prototype.reloadImages = function(path) { 757 | var expando, img, selector, styleNames, styleSheet, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1, _ref2, _ref3, _results; 758 | expando = this.generateUniqueString(); 759 | _ref = this.document.images; 760 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 761 | img = _ref[_i]; 762 | if (pathsMatch(path, pathFromUrl(img.src))) { 763 | img.src = this.generateCacheBustUrl(img.src, expando); 764 | } 765 | } 766 | if (this.document.querySelectorAll) { 767 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 768 | _ref1 = IMAGE_STYLES[_j], selector = _ref1.selector, styleNames = _ref1.styleNames; 769 | _ref2 = this.document.querySelectorAll("[style*=" + selector + "]"); 770 | for (_k = 0, _len2 = _ref2.length; _k < _len2; _k++) { 771 | img = _ref2[_k]; 772 | this.reloadStyleImages(img.style, styleNames, path, expando); 773 | } 774 | } 775 | } 776 | if (this.document.styleSheets) { 777 | _ref3 = this.document.styleSheets; 778 | _results = []; 779 | for (_l = 0, _len3 = _ref3.length; _l < _len3; _l++) { 780 | styleSheet = _ref3[_l]; 781 | _results.push(this.reloadStylesheetImages(styleSheet, path, expando)); 782 | } 783 | return _results; 784 | } 785 | }; 786 | 787 | Reloader.prototype.reloadStylesheetImages = function(styleSheet, path, expando) { 788 | var e, rule, rules, styleNames, _i, _j, _len, _len1; 789 | try { 790 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 791 | } catch (_error) { 792 | e = _error; 793 | } 794 | if (!rules) { 795 | return; 796 | } 797 | for (_i = 0, _len = rules.length; _i < _len; _i++) { 798 | rule = rules[_i]; 799 | switch (rule.type) { 800 | case CSSRule.IMPORT_RULE: 801 | this.reloadStylesheetImages(rule.styleSheet, path, expando); 802 | break; 803 | case CSSRule.STYLE_RULE: 804 | for (_j = 0, _len1 = IMAGE_STYLES.length; _j < _len1; _j++) { 805 | styleNames = IMAGE_STYLES[_j].styleNames; 806 | this.reloadStyleImages(rule.style, styleNames, path, expando); 807 | } 808 | break; 809 | case CSSRule.MEDIA_RULE: 810 | this.reloadStylesheetImages(rule, path, expando); 811 | } 812 | } 813 | }; 814 | 815 | Reloader.prototype.reloadStyleImages = function(style, styleNames, path, expando) { 816 | var newValue, styleName, value, _i, _len; 817 | for (_i = 0, _len = styleNames.length; _i < _len; _i++) { 818 | styleName = styleNames[_i]; 819 | value = style[styleName]; 820 | if (typeof value === 'string') { 821 | newValue = value.replace(/\burl\s*\(([^)]*)\)/, (function(_this) { 822 | return function(match, src) { 823 | if (pathsMatch(path, pathFromUrl(src))) { 824 | return "url(" + (_this.generateCacheBustUrl(src, expando)) + ")"; 825 | } else { 826 | return match; 827 | } 828 | }; 829 | })(this)); 830 | if (newValue !== value) { 831 | style[styleName] = newValue; 832 | } 833 | } 834 | } 835 | }; 836 | 837 | Reloader.prototype.reloadStylesheet = function(path) { 838 | var imported, link, links, match, style, _i, _j, _k, _l, _len, _len1, _len2, _len3, _ref, _ref1; 839 | links = (function() { 840 | var _i, _len, _ref, _results; 841 | _ref = this.document.getElementsByTagName('link'); 842 | _results = []; 843 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 844 | link = _ref[_i]; 845 | if (link.rel.match(/^stylesheet$/i) && !link.__LiveReload_pendingRemoval) { 846 | _results.push(link); 847 | } 848 | } 849 | return _results; 850 | }).call(this); 851 | imported = []; 852 | _ref = this.document.getElementsByTagName('style'); 853 | for (_i = 0, _len = _ref.length; _i < _len; _i++) { 854 | style = _ref[_i]; 855 | if (style.sheet) { 856 | this.collectImportedStylesheets(style, style.sheet, imported); 857 | } 858 | } 859 | for (_j = 0, _len1 = links.length; _j < _len1; _j++) { 860 | link = links[_j]; 861 | this.collectImportedStylesheets(link, link.sheet, imported); 862 | } 863 | if (this.window.StyleFix && this.document.querySelectorAll) { 864 | _ref1 = this.document.querySelectorAll('style[data-href]'); 865 | for (_k = 0, _len2 = _ref1.length; _k < _len2; _k++) { 866 | style = _ref1[_k]; 867 | links.push(style); 868 | } 869 | } 870 | this.console.log("LiveReload found " + links.length + " LINKed stylesheets, " + imported.length + " @imported stylesheets"); 871 | match = pickBestMatch(path, links.concat(imported), (function(_this) { 872 | return function(l) { 873 | return pathFromUrl(_this.linkHref(l)); 874 | }; 875 | })(this)); 876 | if (match) { 877 | if (match.object.rule) { 878 | this.console.log("LiveReload is reloading imported stylesheet: " + match.object.href); 879 | this.reattachImportedRule(match.object); 880 | } else { 881 | this.console.log("LiveReload is reloading stylesheet: " + (this.linkHref(match.object))); 882 | this.reattachStylesheetLink(match.object); 883 | } 884 | } else { 885 | this.console.log("LiveReload will reload all stylesheets because path '" + path + "' did not match any specific one"); 886 | for (_l = 0, _len3 = links.length; _l < _len3; _l++) { 887 | link = links[_l]; 888 | this.reattachStylesheetLink(link); 889 | } 890 | } 891 | return true; 892 | }; 893 | 894 | Reloader.prototype.collectImportedStylesheets = function(link, styleSheet, result) { 895 | var e, index, rule, rules, _i, _len; 896 | try { 897 | rules = styleSheet != null ? styleSheet.cssRules : void 0; 898 | } catch (_error) { 899 | e = _error; 900 | } 901 | if (rules && rules.length) { 902 | for (index = _i = 0, _len = rules.length; _i < _len; index = ++_i) { 903 | rule = rules[index]; 904 | switch (rule.type) { 905 | case CSSRule.CHARSET_RULE: 906 | continue; 907 | case CSSRule.IMPORT_RULE: 908 | result.push({ 909 | link: link, 910 | rule: rule, 911 | index: index, 912 | href: rule.href 913 | }); 914 | this.collectImportedStylesheets(link, rule.styleSheet, result); 915 | break; 916 | default: 917 | break; 918 | } 919 | } 920 | } 921 | }; 922 | 923 | Reloader.prototype.waitUntilCssLoads = function(clone, func) { 924 | var callbackExecuted, executeCallback, poll; 925 | callbackExecuted = false; 926 | executeCallback = (function(_this) { 927 | return function() { 928 | if (callbackExecuted) { 929 | return; 930 | } 931 | callbackExecuted = true; 932 | return func(); 933 | }; 934 | })(this); 935 | clone.onload = (function(_this) { 936 | return function() { 937 | _this.console.log("LiveReload: the new stylesheet has finished loading"); 938 | _this.knownToSupportCssOnLoad = true; 939 | return executeCallback(); 940 | }; 941 | })(this); 942 | if (!this.knownToSupportCssOnLoad) { 943 | (poll = (function(_this) { 944 | return function() { 945 | if (clone.sheet) { 946 | _this.console.log("LiveReload is polling until the new CSS finishes loading..."); 947 | return executeCallback(); 948 | } else { 949 | return _this.Timer.start(50, poll); 950 | } 951 | }; 952 | })(this))(); 953 | } 954 | return this.Timer.start(this.options.stylesheetReloadTimeout, executeCallback); 955 | }; 956 | 957 | Reloader.prototype.linkHref = function(link) { 958 | return link.href || link.getAttribute('data-href'); 959 | }; 960 | 961 | Reloader.prototype.reattachStylesheetLink = function(link) { 962 | var clone, parent; 963 | if (link.__LiveReload_pendingRemoval) { 964 | return; 965 | } 966 | link.__LiveReload_pendingRemoval = true; 967 | if (link.tagName === 'STYLE') { 968 | clone = this.document.createElement('link'); 969 | clone.rel = 'stylesheet'; 970 | clone.media = link.media; 971 | clone.disabled = link.disabled; 972 | } else { 973 | clone = link.cloneNode(false); 974 | } 975 | clone.href = this.generateCacheBustUrl(this.linkHref(link)); 976 | parent = link.parentNode; 977 | if (parent.lastChild === link) { 978 | parent.appendChild(clone); 979 | } else { 980 | parent.insertBefore(clone, link.nextSibling); 981 | } 982 | return this.waitUntilCssLoads(clone, (function(_this) { 983 | return function() { 984 | var additionalWaitingTime; 985 | if (/AppleWebKit/.test(navigator.userAgent)) { 986 | additionalWaitingTime = 5; 987 | } else { 988 | additionalWaitingTime = 200; 989 | } 990 | return _this.Timer.start(additionalWaitingTime, function() { 991 | var _ref; 992 | if (!link.parentNode) { 993 | return; 994 | } 995 | link.parentNode.removeChild(link); 996 | clone.onreadystatechange = null; 997 | return (_ref = _this.window.StyleFix) != null ? _ref.link(clone) : void 0; 998 | }); 999 | }; 1000 | })(this)); 1001 | }; 1002 | 1003 | Reloader.prototype.reattachImportedRule = function(_arg) { 1004 | var href, index, link, media, newRule, parent, rule, tempLink; 1005 | rule = _arg.rule, index = _arg.index, link = _arg.link; 1006 | parent = rule.parentStyleSheet; 1007 | href = this.generateCacheBustUrl(rule.href); 1008 | media = rule.media.length ? [].join.call(rule.media, ', ') : ''; 1009 | newRule = "@import url(\"" + href + "\") " + media + ";"; 1010 | rule.__LiveReload_newHref = href; 1011 | tempLink = this.document.createElement("link"); 1012 | tempLink.rel = 'stylesheet'; 1013 | tempLink.href = href; 1014 | tempLink.__LiveReload_pendingRemoval = true; 1015 | if (link.parentNode) { 1016 | link.parentNode.insertBefore(tempLink, link); 1017 | } 1018 | return this.Timer.start(this.importCacheWaitPeriod, (function(_this) { 1019 | return function() { 1020 | if (tempLink.parentNode) { 1021 | tempLink.parentNode.removeChild(tempLink); 1022 | } 1023 | if (rule.__LiveReload_newHref !== href) { 1024 | return; 1025 | } 1026 | parent.insertRule(newRule, index); 1027 | parent.deleteRule(index + 1); 1028 | rule = parent.cssRules[index]; 1029 | rule.__LiveReload_newHref = href; 1030 | return _this.Timer.start(_this.importCacheWaitPeriod, function() { 1031 | if (rule.__LiveReload_newHref !== href) { 1032 | return; 1033 | } 1034 | parent.insertRule(newRule, index); 1035 | return parent.deleteRule(index + 1); 1036 | }); 1037 | }; 1038 | })(this)); 1039 | }; 1040 | 1041 | Reloader.prototype.generateUniqueString = function() { 1042 | return 'livereload=' + Date.now(); 1043 | }; 1044 | 1045 | Reloader.prototype.generateCacheBustUrl = function(url, expando) { 1046 | var hash, oldParams, originalUrl, params, _ref; 1047 | if (expando == null) { 1048 | expando = this.generateUniqueString(); 1049 | } 1050 | _ref = splitUrl(url), url = _ref.url, hash = _ref.hash, oldParams = _ref.params; 1051 | if (this.options.overrideURL) { 1052 | if (url.indexOf(this.options.serverURL) < 0) { 1053 | originalUrl = url; 1054 | url = this.options.serverURL + this.options.overrideURL + "?url=" + encodeURIComponent(url); 1055 | this.console.log("LiveReload is overriding source URL " + originalUrl + " with " + url); 1056 | } 1057 | } 1058 | params = oldParams.replace(/(\?|&)livereload=(\d+)/, function(match, sep) { 1059 | return "" + sep + expando; 1060 | }); 1061 | if (params === oldParams) { 1062 | if (oldParams.length === 0) { 1063 | params = "?" + expando; 1064 | } else { 1065 | params = "" + oldParams + "&" + expando; 1066 | } 1067 | } 1068 | return url + params + hash; 1069 | }; 1070 | 1071 | return Reloader; 1072 | 1073 | })(); 1074 | 1075 | }).call(this); 1076 | 1077 | },{}],8:[function(require,module,exports){ 1078 | (function() { 1079 | var CustomEvents, LiveReload, k; 1080 | 1081 | CustomEvents = require('./customevents'); 1082 | 1083 | LiveReload = window.LiveReload = new (require('./livereload').LiveReload)(window); 1084 | 1085 | for (k in window) { 1086 | if (k.match(/^LiveReloadPlugin/)) { 1087 | LiveReload.addPlugin(window[k]); 1088 | } 1089 | } 1090 | 1091 | LiveReload.addPlugin(require('./less')); 1092 | 1093 | LiveReload.on('shutdown', function() { 1094 | return delete window.LiveReload; 1095 | }); 1096 | 1097 | LiveReload.on('connect', function() { 1098 | return CustomEvents.fire(document, 'LiveReloadConnect'); 1099 | }); 1100 | 1101 | LiveReload.on('disconnect', function() { 1102 | return CustomEvents.fire(document, 'LiveReloadDisconnect'); 1103 | }); 1104 | 1105 | CustomEvents.bind(document, 'LiveReloadShutDown', function() { 1106 | return LiveReload.shutDown(); 1107 | }); 1108 | 1109 | }).call(this); 1110 | 1111 | },{"./customevents":2,"./less":3,"./livereload":4}],9:[function(require,module,exports){ 1112 | (function() { 1113 | var Timer; 1114 | 1115 | exports.Timer = Timer = (function() { 1116 | function Timer(func) { 1117 | this.func = func; 1118 | this.running = false; 1119 | this.id = null; 1120 | this._handler = (function(_this) { 1121 | return function() { 1122 | _this.running = false; 1123 | _this.id = null; 1124 | return _this.func(); 1125 | }; 1126 | })(this); 1127 | } 1128 | 1129 | Timer.prototype.start = function(timeout) { 1130 | if (this.running) { 1131 | clearTimeout(this.id); 1132 | } 1133 | this.id = setTimeout(this._handler, timeout); 1134 | return this.running = true; 1135 | }; 1136 | 1137 | Timer.prototype.stop = function() { 1138 | if (this.running) { 1139 | clearTimeout(this.id); 1140 | this.running = false; 1141 | return this.id = null; 1142 | } 1143 | }; 1144 | 1145 | return Timer; 1146 | 1147 | })(); 1148 | 1149 | Timer.start = function(timeout, func) { 1150 | return setTimeout(func, timeout); 1151 | }; 1152 | 1153 | }).call(this); 1154 | 1155 | },{}]},{},[8]); 1156 | --------------------------------------------------------------------------------