1<?php 2 3namespace Facebook\WebDriver\Remote\Service; 4 5use Exception; 6use Facebook\WebDriver\Net\URLChecker; 7use Symfony\Component\Process\Process; 8use Symfony\Component\Process\ProcessBuilder; 9 10/** 11 * Start local WebDriver service (when remote WebDriver server is not used). 12 * This will start new process of respective browser driver and take care of its lifecycle. 13 */ 14class DriverService 15{ 16 /** 17 * @var string 18 */ 19 private $executable; 20 21 /** 22 * @var string 23 */ 24 private $url; 25 26 /** 27 * @var array 28 */ 29 private $args; 30 31 /** 32 * @var array 33 */ 34 private $environment; 35 36 /** 37 * @var Process|null 38 */ 39 private $process; 40 41 /** 42 * @param string $executable 43 * @param int $port The given port the service should use. 44 * @param array $args 45 * @param array|null $environment Use the system environment if it is null 46 */ 47 public function __construct($executable, $port, $args = [], $environment = null) 48 { 49 $this->setExecutable($executable); 50 $this->url = sprintf('http://localhost:%d', $port); 51 $this->args = $args; 52 $this->environment = $environment ?: $_ENV; 53 } 54 55 /** 56 * @return string 57 */ 58 public function getURL() 59 { 60 return $this->url; 61 } 62 63 /** 64 * @return DriverService 65 */ 66 public function start() 67 { 68 if ($this->process !== null) { 69 return $this; 70 } 71 72 $this->process = $this->createProcess(); 73 $this->process->start(); 74 75 $this->checkWasStarted($this->process); 76 77 $checker = new URLChecker(); 78 $checker->waitUntilAvailable(20 * 1000, $this->url . '/status'); 79 80 return $this; 81 } 82 83 /** 84 * @return DriverService 85 */ 86 public function stop() 87 { 88 if ($this->process === null) { 89 return $this; 90 } 91 92 $this->process->stop(); 93 $this->process = null; 94 95 $checker = new URLChecker(); 96 $checker->waitUntilUnavailable(3 * 1000, $this->url . '/shutdown'); 97 98 return $this; 99 } 100 101 /** 102 * @return bool 103 */ 104 public function isRunning() 105 { 106 if ($this->process === null) { 107 return false; 108 } 109 110 return $this->process->isRunning(); 111 } 112 113 /** 114 * @deprecated Has no effect. Will be removed in next major version. Executable is now checked 115 * when calling setExecutable(). 116 * @param string $executable 117 * @return string 118 */ 119 protected static function checkExecutable($executable) 120 { 121 return $executable; 122 } 123 124 /** 125 * @param string $executable 126 * @throws Exception 127 */ 128 protected function setExecutable($executable) 129 { 130 if ($this->isExecutable($executable)) { 131 $this->executable = $executable; 132 133 return; 134 } 135 136 throw new Exception( 137 sprintf( 138 '"%s" is not executable. Make sure the path is correct or use environment variable to specify' 139 . ' location of the executable.', 140 $executable 141 ) 142 ); 143 } 144 145 /** 146 * @param Process $process 147 */ 148 protected function checkWasStarted($process) 149 { 150 usleep(10000); // wait 10ms, otherwise the asynchronous process failure may not yet be propagated 151 152 if (!$process->isRunning()) { 153 throw new Exception( 154 sprintf( 155 'Error starting driver executable "%s": %s', 156 $process->getCommandLine(), 157 $process->getErrorOutput() 158 ) 159 ); 160 } 161 } 162 163 /** 164 * @return Process 165 */ 166 private function createProcess() 167 { 168 // BC: ProcessBuilder deprecated since Symfony 3.4 and removed in Symfony 4.0. 169 if (class_exists(ProcessBuilder::class) 170 && mb_strpos('@deprecated', (new \ReflectionClass(ProcessBuilder::class))->getDocComment()) === false 171 ) { 172 $processBuilder = (new ProcessBuilder()) 173 ->setPrefix($this->executable) 174 ->setArguments($this->args) 175 ->addEnvironmentVariables($this->environment); 176 177 return $processBuilder->getProcess(); 178 } 179 // Safe to use since Symfony 3.3 180 $commandLine = array_merge([$this->executable], $this->args); 181 182 return new Process($commandLine, null, $this->environment); 183 } 184 185 /** 186 * Check whether given file is executable directly or using system PATH 187 * 188 * @param string $filename 189 * @return bool 190 */ 191 private function isExecutable($filename) 192 { 193 if (is_executable($filename)) { 194 return true; 195 } 196 if ($filename !== basename($filename)) { // $filename is an absolute path, do no try to search it in PATH 197 return false; 198 } 199 200 $paths = explode(PATH_SEPARATOR, getenv('PATH')); 201 foreach ($paths as $path) { 202 if (is_executable($path . DIRECTORY_SEPARATOR . $filename)) { 203 return true; 204 } 205 } 206 207 return false; 208 } 209} 210