├── .travis.yml ├── LockException.php ├── PidFile.php ├── ProcessManager.php ├── README.md ├── Tests ├── PidFileTest.php ├── ProcessManagerTest.php └── bootstrap.php ├── composer.json └── phpunit.xml.dist /.travis.yml: -------------------------------------------------------------------------------- 1 | language: php 2 | 3 | php: 4 | - 5.3 5 | - 5.4 6 | - 5.5 7 | 8 | script: phpunit --coverage-text 9 | 10 | notifications: 11 | slack: liip:3QOs1QKt3aCFxpJvRzpJCbVZ 12 | -------------------------------------------------------------------------------- /LockException.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Daniel Barsotti 10 | */ 11 | class PidFile 12 | { 13 | /** 14 | * @var string 15 | */ 16 | protected $filename; 17 | 18 | /** 19 | * @var ProcessManager 20 | */ 21 | protected $processManager; 22 | 23 | /** 24 | * @var resource 25 | */ 26 | protected $file = null; 27 | 28 | /** 29 | * Prepares new lock on the file $filename 30 | * 31 | * @param ProcessManager $processManager Process manager instance 32 | * @param string $filename The name of the lock file 33 | */ 34 | public function __construct(ProcessManager $processManager, $filename) 35 | { 36 | $this->processManager = $processManager; 37 | $this->filename = $filename; 38 | } 39 | 40 | /** 41 | * Acquire a lock on the lock file. 42 | * 43 | * @throws LockException 44 | * @return void 45 | */ 46 | public function acquireLock() 47 | { 48 | $dir = dirname($this->filename); 49 | if (!file_exists($dir)) { 50 | mkdir($dir, 0775, true); 51 | } 52 | 53 | $this->file = fopen($this->filename, 'a+'); 54 | if (! flock($this->file, LOCK_EX | LOCK_NB)) { 55 | throw new LockException('Could not lock the pidfile'); 56 | } 57 | } 58 | 59 | /** 60 | * Write the given PID to the lock file. The file must be locked before! 61 | * 62 | * @param string $pid The PID to write to the file 63 | * 64 | * @return int 65 | */ 66 | public function setPid($pid) 67 | { 68 | if (null === $this->file) { 69 | throw new LockException('The pidfile is not locked'); 70 | } 71 | 72 | ftruncate($this->file, 0); 73 | return fwrite($this->file, $pid); 74 | } 75 | 76 | /** 77 | * Read the PID written in the lock file. The file must be locked before! 78 | * 79 | * @return string 80 | */ 81 | public function getPid() 82 | { 83 | if (null === $this->file) { 84 | throw new LockException('The pidfile is not locked'); 85 | } 86 | 87 | return file_get_contents($this->filename); 88 | } 89 | 90 | /** 91 | * Exec a command in the background and return the PID 92 | * 93 | * @param string $command The command to execute 94 | * 95 | * @return string 96 | */ 97 | public function execProcess($command) 98 | { 99 | return $this->processManager->execProcess($command); 100 | } 101 | 102 | /** 103 | * Check if the PID written in the lock file corresponds to a running process. 104 | * The file must be locked before! 105 | * 106 | * @return boolean 107 | */ 108 | public function isProcessRunning() 109 | { 110 | if (null === $this->file) { 111 | throw new LockException('The pidfile is not locked'); 112 | } 113 | 114 | $pid = $this->getPid(); 115 | 116 | return $this->processManager->isProcessRunning($pid); 117 | } 118 | 119 | /** 120 | * Kill the currently running process 121 | * 122 | * @return boolean 123 | */ 124 | public function killProcess() 125 | { 126 | return $this->processManager->killProcess($this->getPid()); 127 | } 128 | 129 | /** 130 | * Release the lock on the lock file 131 | * 132 | * @return boolean 133 | */ 134 | public function releaseLock() 135 | { 136 | if (! is_resource($this->file)) { 137 | return false; 138 | } 139 | 140 | flock($this->file, LOCK_UN); 141 | fclose($this->file); 142 | @unlink($this->filename); 143 | $this->file = null; 144 | return true; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /ProcessManager.php: -------------------------------------------------------------------------------- 1 | 9 | * @author Daniel Barsotti 10 | */ 11 | class ProcessManager 12 | { 13 | 14 | /** 15 | * Output log location 16 | * 17 | * @var string 18 | */ 19 | protected $logFile; 20 | 21 | /** 22 | * Sets up a new ProcessManager 23 | * 24 | * @param string $log Location to output log file 25 | */ 26 | public function __construct($logFile = '/dev/null') 27 | { 28 | $this->logFile = $logFile; 29 | } 30 | 31 | /** 32 | * Exec a command in the background and return the PID 33 | * 34 | * @param string $command 35 | * 36 | * @return int PID 37 | */ 38 | public function execProcess($command) 39 | { 40 | // The double & in the command "(.... &)&" will have the same effect as nohup 41 | // but the pid returned is actually the correct pid (which is not the case when 42 | // using nohup). 43 | $command = '(' . $command.' > ' . $this->logFile . ' 2>&1 & echo $!)&'; 44 | exec($command, $op); 45 | return (int)$op[0]; 46 | } 47 | 48 | /** 49 | * Check if the PID is a running process. 50 | * 51 | * @param string $pid The PID to write to the file 52 | * 53 | * @return boolean 54 | */ 55 | public function isProcessRunning($pid) 56 | { 57 | // Warning: this will only work on Unix 58 | return ($pid !== '') && file_exists("/proc/$pid"); 59 | } 60 | 61 | /** 62 | * Kill the currently running process 63 | * 64 | * @param string $pid The PID to write to the file 65 | * 66 | * @return boolean 67 | */ 68 | public function killProcess($pid) 69 | { 70 | return posix_kill($pid, SIGKILL); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ProcessManager 2 | ============== 3 | 4 | Provides a simple locking mechanism based on UNIX process id's written to a "PID file". 5 | 6 | [![Build Status](https://secure.travis-ci.org/liip/LiipProcessManager.png)](http://travis-ci.org/liip/LiipProcessManager) 7 | 8 | http://github.com/liip/LiipProcessManager.git 9 | 10 | Here is a simple example 11 | 12 | ```php 13 | execProcess('sleep 10m'); 20 | $processManager->isProcessRunning($pid) 21 | $processManager->killProcess($pid); 22 | 23 | // to set log location instead of routing it to /dev/null by default 24 | $processManager = new ProcessManager('/path/to/logfile'); 25 | $pid = $processManager->execProcess('sleep 10m'); 26 | 27 | // acquire a lock via a pid file 28 | $lock = new PidFile(new ProcessManager(), '/tmp/foobar'); 29 | $lock->acquireLock(); 30 | $pid = $lock->execProcess('sleep 10m'); 31 | // set the PID which should be locked on 32 | $lock->setPid(getmypid()); 33 | $lock->releaseLock(); 34 | ``` 35 | -------------------------------------------------------------------------------- /Tests/PidFileTest.php: -------------------------------------------------------------------------------- 1 | 11 | * @author Daniel Barsotti 12 | */ 13 | class PidFileTest extends \PHPUnit_Framework_TestCase 14 | { 15 | protected $processManager; 16 | protected $pidfile = '/tmp/nzzApiTestPidFile'; 17 | 18 | public function setUp() 19 | { 20 | $this->processManager = $this->getMockBuilder('Liip\ProcessManager\ProcessManager') 21 | ->disableOriginalConstructor()->getMock(); 22 | } 23 | 24 | public function tearDown() 25 | { 26 | if (file_exists($this->pidfile)) { 27 | unlink($this->pidfile); 28 | } 29 | } 30 | 31 | /** 32 | * @covers Liip\ProcessManager\PidFile::__construct 33 | */ 34 | public function testConstructor() 35 | { 36 | $helper = new PidFile($this->processManager, $this->pidfile); 37 | $this->assertAttributeSame($this->pidfile, 'filename', $helper); 38 | $this->assertAttributeSame(null, 'file', $helper); 39 | } 40 | 41 | /** 42 | * @covers Liip\ProcessManager\PidFile::acquireLock 43 | * @covers Liip\ProcessManager\PidFile::releaseLock 44 | */ 45 | public function testLockingUnlocking() 46 | { 47 | $helper = new PidFile($this->processManager, $this->pidfile); 48 | $helper->acquireLock(); 49 | 50 | $this->assertAttributeNotSame(null, 'file', $helper); 51 | $this->assertTrue(file_exists($this->pidfile)); 52 | 53 | $helper->releaseLock(); 54 | 55 | $this->assertAttributeSame(null, 'file', $helper); 56 | } 57 | 58 | /** 59 | * @covers Liip\ProcessManager\PidFile::acquireLock 60 | * @covers Liip\ProcessManager\PidFile::releaseLock 61 | */ 62 | public function testLockingLockedFileDoesNotWork() 63 | { 64 | $helper1 = new PidFile($this->processManager, $this->pidfile); 65 | $helper2 = new PidFile($this->processManager, $this->pidfile); 66 | 67 | $helper1->acquireLock(); 68 | 69 | try { 70 | $helper2->acquireLock(); 71 | } catch (LockException $ex) { 72 | $this->assertEquals('Could not lock the pidfile', $ex->getMessage()); 73 | } 74 | 75 | $helper1->releaseLock(); 76 | } 77 | 78 | /** 79 | * @covers Liip\ProcessManager\PidFile::acquireLock 80 | * @covers Liip\ProcessManager\PidFile::releaseLock 81 | */ 82 | public function testLockingUnlockedFileWorks() 83 | { 84 | $helper1 = new PidFile($this->processManager, $this->pidfile); 85 | $helper2 = new PidFile($this->processManager, $this->pidfile); 86 | 87 | $helper1->acquireLock(); 88 | $helper1->releaseLock(); 89 | 90 | $helper2->acquireLock(); 91 | $helper2->releaseLock(); 92 | } 93 | 94 | /** 95 | * @covers Liip\ProcessManager\PidFile::setPid 96 | * @covers Liip\ProcessManager\PidFile::getPid 97 | */ 98 | public function testGetSetPid() 99 | { 100 | $helper = new PidFile($this->processManager, $this->pidfile); 101 | $helper->acquireLock(); 102 | $helper->setPid('my_first_pid'); 103 | $helper->setPid('my_pid'); 104 | 105 | $this->assertEquals('my_pid', file_get_contents($this->pidfile)); 106 | 107 | $pid = $helper->getPid(); 108 | $this->assertEquals('my_pid', $pid); 109 | 110 | $helper->releaseLock(); 111 | } 112 | 113 | /** 114 | * @covers Liip\ProcessManager\PidFile::isProcessRunning 115 | */ 116 | public function testIsProcessRunning() 117 | { 118 | $processManager = $this->getMockBuilder('Liip\ProcessManager\ProcessManager')->disableOriginalConstructor()->getMock(); 119 | $processManager->expects($this->any()) 120 | ->method('isProcessRunning') 121 | ->will($this->onConsecutiveCalls(true, false, false)) 122 | ; 123 | 124 | $helper = new PidFile($processManager, $this->pidfile); 125 | $helper->acquireLock(); 126 | 127 | // Test with my real pid 128 | $helper->setPid('foo'); 129 | $this->assertTrue($helper->isProcessRunning()); 130 | 131 | // Test with a fake pid 132 | $helper->setPid('1234a');// letter, to make sure, process does really not exist 133 | $this->assertFalse($helper->isProcessRunning()); 134 | 135 | // Test with empty pid 136 | $helper->setPid(''); 137 | $this->assertFalse($helper->isProcessRunning()); 138 | 139 | $helper->releaseLock(); 140 | } 141 | 142 | /** 143 | * @covers Liip\ProcessManager\PidFile::getPid 144 | * @covers Liip\ProcessManager\PidFile::setPid 145 | * @covers Liip\ProcessManager\PidFile::isProcessRunning 146 | */ 147 | public function testCannotDoAnythingIfNotLocked() 148 | { 149 | $helper = new PidFile($this->processManager, $this->pidfile); 150 | 151 | try { 152 | $helper->getPid(); 153 | } catch (LockException $ex) { 154 | $this->assertEquals('The pidfile is not locked', $ex->getMessage()); 155 | } 156 | 157 | try { 158 | $helper->setPid('1234'); 159 | } catch (LockException $ex) { 160 | $this->assertEquals('The pidfile is not locked', $ex->getMessage()); 161 | } 162 | 163 | try { 164 | $helper->isProcessRunning(); 165 | } catch (LockException $ex) { 166 | $this->assertEquals('The pidfile is not locked', $ex->getMessage()); 167 | } 168 | } 169 | 170 | /** 171 | * @covers Liip\ProcessManager\PidFile::execProcess 172 | */ 173 | public function testExecProcess() 174 | { 175 | $processManager = $this->getMockBuilder('Liip\ProcessManager\ProcessManager')->disableOriginalConstructor()->getMock(); 176 | $processManager->expects($this->once()) 177 | ->method('execProcess') 178 | ->with('foo') 179 | ->will($this->returnValue(null)) 180 | ; 181 | 182 | $helper = new PidFile($processManager, $this->pidfile); 183 | $helper->execProcess('foo'); 184 | } 185 | 186 | /** 187 | * @covers Liip\ProcessManager\PidFile::killProcess 188 | */ 189 | public function testKillProcess() 190 | { 191 | // Do not mock the ProcessManager for this test ! 192 | $pm = new ProcessManager(); 193 | 194 | // Spawn a new process 195 | $child_pid = $pm->execProcess('sleep 10m'); 196 | 197 | // Write the new process pid in the lock file and try to kill it 198 | $helper = new PidFile($pm, $this->pidfile); 199 | $helper->acquireLock(); 200 | $helper->setPid($child_pid); 201 | $process_killed = $helper->killProcess(); 202 | $helper->releaseLock(); 203 | 204 | // Assert the process was killed 205 | $this->assertTrue($process_killed); 206 | 207 | // Assert the 'sleep 10m' command is not running anymore 208 | $out = null; 209 | exec('ps aux', $out); 210 | foreach ($out as $row) { 211 | if (strpos($row, 'sleep 10m') !== false) { 212 | exec("kill -9 $child_pid"); 213 | $this->fail("The process $child_pid is supposed to have been killed, yet it was still running. Had to kill it in cleanup !"); 214 | } 215 | } 216 | } 217 | } 218 | 219 | -------------------------------------------------------------------------------- /Tests/ProcessManagerTest.php: -------------------------------------------------------------------------------- 1 | 10 | * @author Daniel Barsotti 11 | */ 12 | class ProcessManagerTest extends \PHPUnit_Framework_TestCase 13 | { 14 | public function testExecProcess() 15 | { 16 | $processManager = new ProcessManager(); 17 | 18 | $child_pid = $processManager->execProcess('sleep 10m'); 19 | 20 | // Assert the 'sleep 10m' command is running 21 | $out = null; 22 | $process_running = false; 23 | exec('ps aux', $out); 24 | foreach ($out as $row) { 25 | if (strpos($row, 'sleep 10m') !== false) { 26 | $process_running = true; 27 | break; 28 | } 29 | } 30 | 31 | $this->assertTrue($process_running, "The process $child_pid is not running"); 32 | 33 | // Kill the sleeping process 34 | exec("kill -9 $child_pid"); 35 | } 36 | 37 | public function testIsProcessRunning() 38 | { 39 | $processManager = new ProcessManager(); 40 | 41 | if (!is_dir('/proc')) { 42 | $this->markTestSkipped('Mac does not have the proc dir.'); 43 | } 44 | $this->assertTrue($processManager->isProcessRunning(getmypid()), "ProcessManager reported that my process is not running"); 45 | 46 | $this->assertFalse($processManager->isProcessRunning('Unexisting_pid'), "ProcessManager reported that an unexisting pid process is running"); 47 | 48 | $this->assertFalse($processManager->isProcessRunning(''), "ProcessManager reported that an empty pid process is running"); 49 | } 50 | 51 | public function testKillProcess() 52 | { 53 | $processManager = new ProcessManager(); 54 | 55 | $child_pid = $processManager->execProcess('sleep 10m'); 56 | 57 | // Let the time to the process to be spawned, otherwise we can have 58 | // cases where the ProcessManager tries to kill a process which is 59 | // actually not yet started --> test failing 60 | sleep(1); 61 | 62 | $process_killed = $processManager->killProcess($child_pid); 63 | 64 | // Assert the process was killed 65 | $this->assertTrue($process_killed); 66 | 67 | // Assert the 'sleep 10m' command is not running anymore 68 | $out = null; 69 | exec('ps aux', $out); 70 | foreach ($out as $row) { 71 | if (strpos($row, 'sleep 10m') !== false) { 72 | exec("kill -9 $child_pid"); 73 | $this->fail("The process $child_pid is supposed to have been killed, yet it was still running. Had to kill it in cleanup !"); 74 | } 75 | } 76 | } 77 | 78 | } 79 | 80 | -------------------------------------------------------------------------------- /Tests/bootstrap.php: -------------------------------------------------------------------------------- 1 | =5.3.0" 19 | }, 20 | "autoload": { 21 | "psr-0": { "Liip\\ProcessManager": "" } 22 | }, 23 | "target-dir": "Liip/ProcessManager" 24 | } 25 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 16 | ./Tests/ 17 | 18 | 19 | 20 | 21 | 22 | ./ 23 | 24 | ./Tests 25 | 26 | 27 | 28 | 29 | --------------------------------------------------------------------------------