├── .gitignore ├── .travis.yml ├── CHANGELOG.md ├── LICENSE ├── README.md ├── bin └── wol.php ├── composer.json ├── phpunit.xml.dist ├── src ├── Factory.php └── Sender.php └── tests ├── FactoryTest.php ├── SenderTest.php └── bootstrap.php /.gitignore: -------------------------------------------------------------------------------- 1 | /vendor 2 | /composer.lock 3 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | php: 3 | - 5.3 4 | - 5.4 5 | - 5.5 6 | - 5.6 7 | - hhvm 8 | install: 9 | - composer install --prefer-source --no-interaction 10 | script: 11 | - phpunit --coverage-text 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # CHANGELOG 2 | 3 | This file is a manually maintained list of changes for each release. Feel free 4 | to add your changes here when sending pull requests. Also send corrections if 5 | you spot any mistakes. 6 | 7 | ## 0.1.0 (2014-05-10) 8 | 9 | * First tagged release 10 | * Refactor construction to support custom broadcast addresses 11 | ([#2](https://github.com/clue/reactphp-wake-on-lan/pull/2) @WyriHaximus) 12 | * Use stable/tagged dependencies 13 | ([#2](https://github.com/clue/reactphp-wake-on-lan/pull/2)) 14 | * Use `Clue\React\Wol` namespace and PSR-4 code layout 15 | * Extended test suite with 100% coverage and continuos integration by Travis CI 16 | 17 | ## 0.0.0 (2013-04-13) 18 | 19 | * Initial concept 20 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2013 Christian Lück 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 furnished 10 | 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 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # clue/wol-react [![Build Status](https://travis-ci.org/clue/php-wake-on-lan-react.svg?branch=master)](https://travis-ci.org/clue/php-wake-on-lan-react) 2 | 3 | Turn on your PC with [Wake-On-LAN](http://en.wikipedia.org/wiki/Wake-on-LAN) (WOL) requests 4 | 5 | > Note: This project is in beta stage! Feel free to report any issues you encounter. 6 | 7 | ## Usage 8 | 9 | Once [installed](#install), using this library is as simple as running: 10 | 11 | ```php 12 | $loop = React\EventLoop\Factory::create(); 13 | $wolFactory = new Clue\React\Wol\Factory($loop); 14 | 15 | $wolFactory->createSender()->then(function(Clue\React\Wol\Sender $wol) { 16 | $wol->send('11:22:33:44:55:66'); 17 | }); 18 | 19 | $loop->run(); 20 | ``` 21 | 22 | If your environment requires a non-default broadcast address (the default is: `255.255.255.255:7`), for example `1.2.3.4:9`. You can pass that to the `createSender` method on the `Factory`. For example: 23 | 24 | 25 | ```php 26 | $loop = React\EventLoop\Factory::create(); 27 | $wolFactory = new Clue\React\Wol\Factory($loop); 28 | 29 | $wolFactory->createSender('1.2.3.4:9')->then(function(Clue\React\Wol\Sender $wol) { 30 | $wol->send('11:22:33:44:55:66'); 31 | }); 32 | 33 | $loop->run(); 34 | ``` 35 | 36 | There's also a CLI script in `bin/wol.php` to send a WOL request from the 37 | command line simply by running: 38 | 39 | ```bash 40 | $ php bin/wol.php 11:22:33:44:55:66 41 | ``` 42 | 43 | ## Introduction 44 | 45 | The following short introduction is mostly taken from wikipedia's 46 | [article about WOL](http://en.wikipedia.org/wiki/Wake-on-LAN): 47 | 48 | Wake-on-LAN ("WOL") is implemented using a specially designed packet called a 49 | magic packet, which is sent to the computer to be woken up. The magic packet 50 | contains the MAC address of the destination computer. Powered-down computers 51 | capable of Wake-on-LAN will contain network devices able to "listen" to incoming 52 | packets in low-power mode while the system is powered down. If a magic packet is 53 | received that is directed to the device's MAC address, the NIC signals the 54 | computer's power supply to initiate system wake-up, much in the same way as 55 | pressing the power button would do. 56 | 57 | The magic packet is a broadcast frame containing anywhere within its payload 6 58 | bytes of all 255 (FF FF FF FF FF FF in hexadecimal), followed by sixteen 59 | repetitions of the target computer's 48-bit MAC address, for a total of 102 60 | bytes. 61 | 62 | Since the magic packet is only scanned for the string above, and not actually 63 | parsed by a full protocol stack, it may be sent as any network- and 64 | transport-layer protocol, although this library uses a typical UDP datagram. 65 | The magic packet is usually sent on the data link layer (layer 2 in the OSI 66 | model) and when sent, is broadcast to all attached devices on a given network, 67 | using the network broadcast address; the IP-address (layer 3 in the OSI model) 68 | is not used. 69 | 70 | ## Install 71 | 72 | The recommended way to install this library is [through composer](http://getcomposer.org). [New to composer?](http://getcomposer.org/doc/00-intro.md) 73 | 74 | ```JSON 75 | { 76 | "require": { 77 | "clue/wol-react": "0.1.*" 78 | } 79 | } 80 | ``` 81 | 82 | ## License 83 | 84 | MIT 85 | 86 | -------------------------------------------------------------------------------- /bin/wol.php: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env php 2 | createSender($address)->then(function(Clue\React\Wol\Sender $wol) use($mac) { 11 | $wol->send($mac); 12 | echo 'Sending magic wake on lan (WOL) packet to ' . $mac . PHP_EOL; 13 | })->then(null, function (Exception $error) { 14 | echo 'Error: ' . $error->getMessage() . PHP_EOL; 15 | exit(1); 16 | }); 17 | }; 18 | 19 | $prompt = '> '; 20 | 21 | if ($_SERVER['argc'] > 2) { 22 | $do($_SERVER['argv'][1], $_SERVER['argv'][2]); 23 | } else if ($_SERVER['argc'] > 1) { 24 | $do($_SERVER['argv'][1]); 25 | } else { 26 | echo 'No target MAC address given as argument, reading from STDIN: ' . PHP_EOL . $prompt; 27 | 28 | $loop->addReadStream(STDIN, function () use ($wol, $loop, $do, $prompt) { 29 | $line = fread(STDIN, 8192); 30 | if ($line === '') { 31 | // EOF: CRTL+D 32 | echo PHP_EOL; 33 | $loop->removeReadStream(STDIN); 34 | } else { 35 | $line = trim($line); 36 | if ($line === '') { 37 | // empty line 38 | echo $prompt; 39 | return; 40 | } 41 | 42 | $do($line); 43 | echo $prompt; 44 | } 45 | }); 46 | } 47 | 48 | $loop->run(); 49 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "clue/wol-react", 3 | "description": "Turn on your PC with Wake-On-LAN (WOL) requests", 4 | "keywords": ["WOL", "wake on LAN", "ReactPHP", "async"], 5 | "homepage": "https://github.com/clue/php-wake-on-lan-react", 6 | "license": "MIT", 7 | "authors": [ 8 | { 9 | "name": "Christian Lück", 10 | "email": "christian@lueck.tv" 11 | } 12 | ], 13 | "require": { 14 | "php": ">=5.3", 15 | "react/event-loop": "0.3.*", 16 | "clue/socket-react": "0.2.*" 17 | }, 18 | "autoload": { 19 | "psr-4": {"Clue\\React\\Wol\\": "src"} 20 | }, 21 | "bin": ["bin/wol.php"] 22 | } 23 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 9 | 10 | 11 | ./tests/ 12 | 13 | 14 | 15 | 16 | ./src/ 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/Factory.php: -------------------------------------------------------------------------------- 1 | loop = $loop; 21 | $this->datagramFactory = $datagramFactory; 22 | } 23 | 24 | public function createSender($address = self::DEFAULT_ADDRESS) 25 | { 26 | return $this->datagramFactory->createClient($address, array('broadcast' => true))->then(function (Socket $socket) { 27 | return new Sender($socket); 28 | }); 29 | } 30 | } -------------------------------------------------------------------------------- /src/Sender.php: -------------------------------------------------------------------------------- 1 | socket = $socket; 15 | $this->socket->pause(); 16 | } 17 | 18 | public function send($mac) 19 | { 20 | $mac = $this->coerceMac($mac); 21 | 22 | $message = "\xFF\xFF\xFF\xFF\xFF\xFF" . str_repeat($this->formatMac($mac), 16); 23 | 24 | $this->socket->send($message); 25 | } 26 | 27 | /** 28 | * 29 | * @param string $mac 30 | * mixed case mac address with colon, hyphen or no separators 31 | * @return string uppercase mac address with colon separators (e.g. 00:11:22:33:44:55) 32 | * @throws InvalidArgumentException 33 | */ 34 | public function coerceMac($mac) 35 | { 36 | if (strlen($mac) === 12) { 37 | // no separators => add colons in between 38 | $mac = implode(':', str_split($mac, 2)); 39 | } elseif (strpos($mac, '-') !== false) { 40 | // hyphen separators => replace with colons 41 | $mac = str_replace('-', ':', $mac); 42 | } 43 | $mac = strtoupper($mac); 44 | 45 | if (!preg_match('/^(?:[A-F0-9]{2}\:){5}[A-F0-9]{2}$/', $mac)) { 46 | throw new InvalidArgumentException('Invalid mac address given'); 47 | } 48 | 49 | return $mac; 50 | } 51 | 52 | private function formatMac($mac) 53 | { 54 | $address = ''; 55 | 56 | foreach (explode(':', $mac) as $part) { 57 | $address .= chr(hexdec($part)); 58 | } 59 | 60 | return $address; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /tests/FactoryTest.php: -------------------------------------------------------------------------------- 1 | loop = React\EventLoop\Factory::create(); 11 | $this->factory = new Factory($this->loop); 12 | } 13 | 14 | public function testSender() 15 | { 16 | $ret = $this->factory->createSender(); 17 | 18 | $this->assertInstanceOf('React\Promise\PromiseInterface', $ret); 19 | 20 | $ret->then($this->expectCallableOnce()); 21 | $ret->then(function (Sender $sender) { 22 | 23 | }); 24 | 25 | $this->loop->run(); 26 | } 27 | 28 | public function testInvalidSender() 29 | { 30 | $ret = $this->factory->createSender('some.host.invalid'); 31 | 32 | $this->assertInstanceOf('React\Promise\PromiseInterface', $ret); 33 | 34 | $ret->then(null, $this->expectCallableOnce()); 35 | $ret->then(function (Exception $exception) { 36 | 37 | }); 38 | 39 | $this->loop->run(); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /tests/SenderTest.php: -------------------------------------------------------------------------------- 1 | socket = $this->getMockBuilder('Socket\React\Datagram\Socket') 10 | ->disableOriginalConstructor() 11 | ->getMock(); 12 | 13 | $this->sender = new Sender($this->socket); 14 | } 15 | 16 | public function testSend() 17 | { 18 | $this->socket 19 | ->expects($this->once()) 20 | ->method('send'); 21 | 22 | $this->sender->send('aa:bb:cc:dd:ee:ff'); 23 | } 24 | 25 | /** 26 | * @dataProvider provideValidAddress 27 | * @param string $address 28 | * @param string $mac 29 | */ 30 | public function testValidAddress($address, $mac) 31 | { 32 | $this->assertEquals($mac, $this->sender->coerceMac($address)); 33 | } 34 | 35 | public function provideValidAddress() 36 | { 37 | return array( 38 | array( 39 | 'Aa:bB:12:EF:34:56', 40 | 'AA:BB:12:EF:34:56' 41 | ), 42 | array( 43 | 'Aa-bB-12-EF-34-56', 44 | 'AA:BB:12:EF:34:56' 45 | ), 46 | array( 47 | 'AabB12EF3456', 48 | 'AA:BB:12:EF:34:56' 49 | ), 50 | ); 51 | } 52 | 53 | /** 54 | * @dataProvider provideInvalidAddress 55 | * @expectedException InvalidArgumentException 56 | * @param unknown $address 57 | */ 58 | public function testInvalidAddress($address) 59 | { 60 | $this->sender->coerceMac($address); 61 | } 62 | 63 | public function provideInvalidAddress($address) 64 | { 65 | return array( 66 | array('aa:bb:cc'), 67 | array('aa:aa:aa:aa:aa:aa:aa'), 68 | array('gg:gg:gg:gg:gg:gg'), 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | createCallableMock(); 10 | $mock 11 | ->expects($this->once()) 12 | ->method('__invoke'); 13 | 14 | return $mock; 15 | } 16 | 17 | protected function expectCallableNever() 18 | { 19 | $mock = $this->createCallableMock(); 20 | $mock 21 | ->expects($this->never()) 22 | ->method('__invoke'); 23 | 24 | return $mock; 25 | } 26 | 27 | protected function expectCallableOnceParameter($type) 28 | { 29 | $mock = $this->createCallableMock(); 30 | $mock 31 | ->expects($this->once()) 32 | ->method('__invoke') 33 | ->with($this->isInstanceOf($type)); 34 | 35 | return $mock; 36 | } 37 | 38 | /** 39 | * @link https://github.com/reactphp/react/blob/master/tests/React/Tests/Socket/TestCase.php (taken from reactphp/react) 40 | */ 41 | protected function createCallableMock() 42 | { 43 | return $this->getMock('CallableStub'); 44 | } 45 | } 46 | 47 | class CallableStub 48 | { 49 | public function __invoke() 50 | { 51 | } 52 | } 53 | 54 | --------------------------------------------------------------------------------