Skip to content

Commit 7ac4cb1

Browse files
committed
[Filesystem] Added a lock handler
1 parent 75f5f5c commit 7ac4cb1

File tree

3 files changed

+194
-1
lines changed

3 files changed

+194
-1
lines changed

CHANGELOG.md

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
CHANGELOG
22
=========
33

4+
2.6.0
5+
-----
6+
7+
* added LockHandler
8+
49
2.3.12
510
------
611

@@ -10,7 +15,7 @@ CHANGELOG
1015
-----
1116

1217
* added the dumpFile() method to atomically write files
13-
18+
1419
2.2.0
1520
-----
1621

LockHandler.php

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
3+
/*
4+
* This file is part of the Symfony package.
5+
*
6+
* (c) Fabien Potencier <[email protected]>
7+
*
8+
* For the full copyright and license information, please view the LICENSE
9+
* file that was distributed with this source code.
10+
*/
11+
12+
namespace Symfony\Component\Filesystem;
13+
14+
use Symfony\Component\Filesystem\Exception\IOException;
15+
16+
/**
17+
* LockHandler class provides a simple abstraction to lock anything by means of
18+
* a file lock.
19+
*
20+
* A locked file is created based on the lock name when calling lock(). Other
21+
* lock handlers will not be able to lock the same name until it is released
22+
* (explicitly by calling release() or implicitly when the instance holding the
23+
* lock is destroyed).
24+
*
25+
* @author Grégoire Pineau <[email protected]>
26+
* @author Romain Neutron <[email protected]>
27+
* @author Nicolas Grekas <[email protected]>
28+
*/
29+
class LockHandler
30+
{
31+
private $file;
32+
private $handle;
33+
34+
/**
35+
* @param string $name The lock name
36+
* @param string|null $lockPath The directory to store the lock. Default values will use temporary directory
37+
* @throws IOException If the lock directory could not be created or is not writable
38+
*/
39+
public function __construct($name, $lockPath = null)
40+
{
41+
$lockPath = $lockPath ?: sys_get_temp_dir();
42+
43+
if (!is_dir($lockPath)) {
44+
$fs = new Filesystem();
45+
$fs->mkdir($lockPath);
46+
}
47+
48+
if (!is_writable($lockPath)) {
49+
throw new IOException(sprintf('The directory "%s" is not writable.', $lockPath), 0, null, $lockPath);
50+
}
51+
52+
$name = sprintf('%s-%s', preg_replace('/[^a-z0-9\._-]+/i', '-', $name), hash('sha256', $name));
53+
54+
$this->file = sprintf('%s/%s', $lockPath, $name);
55+
}
56+
57+
/**
58+
* Lock the resource
59+
*
60+
* @param bool $blocking wait until the lock is released
61+
* @return bool Returns true if the lock was acquired, false otherwise
62+
* @throws IOException If the lock file could not be created or opened
63+
*/
64+
public function lock($blocking = false)
65+
{
66+
if ($this->handle) {
67+
return true;
68+
}
69+
70+
// Set an error handler to not trigger the registered error handler if
71+
// the file can not be opened.
72+
set_error_handler('var_dump', 0);
73+
$this->handle = @fopen($this->file, 'c');
74+
restore_error_handler();
75+
76+
if (!$this->handle) {
77+
throw new IOException(sprintf('Unable to fopen "%s".', $this->file), 0, null, $this->file);
78+
}
79+
80+
// On Windows, even if PHP doc says the contrary, LOCK_NB works, see
81+
// https://bugs.php.net/54129
82+
if (!flock($this->handle, LOCK_EX | ($blocking ? 0 : LOCK_NB))) {
83+
fclose($this->handle);
84+
$this->handle = null;
85+
86+
return false;
87+
}
88+
89+
return true;
90+
}
91+
92+
/**
93+
* Release the resource
94+
*/
95+
public function release()
96+
{
97+
if ($this->handle) {
98+
flock($this->handle, LOCK_UN | LOCK_NB);
99+
fclose($this->handle);
100+
$this->handle = null;
101+
}
102+
}
103+
}

Tests/LockHandlerTest.php

Lines changed: 85 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,85 @@
1+
<?php
2+
3+
namespace Symfony\Component\Filesystem\Tests;
4+
5+
use Symfony\Component\Filesystem\LockHandler;
6+
7+
class LockHandlerTest extends \PHPUnit_Framework_TestCase
8+
{
9+
/**
10+
* @expectedException Symfony\Component\Filesystem\Exception\IOException
11+
* @expectedExceptionMessage Failed to create "/a/b/c/d/e": mkdir(): Permission denied.
12+
*/
13+
public function testConstructWhenRepositoryDoesNotExist()
14+
{
15+
new LockHandler('lock', '/a/b/c/d/e');
16+
}
17+
18+
/**
19+
* @expectedException Symfony\Component\Filesystem\Exception\IOException
20+
* @expectedExceptionMessage The directory "/" is not writable.
21+
*/
22+
public function testConstructWhenRepositoryIsNotWriteable()
23+
{
24+
new LockHandler('lock', '/');
25+
}
26+
27+
public function testConstructSanitizeName()
28+
{
29+
$lock = new LockHandler('<?php echo "% hello word ! %" ?>');
30+
31+
$file = sprintf('%s/-php-echo-hello-word--4b3d9d0d27ddef3a78a64685dda3a963e478659a9e5240feaf7b4173a8f28d5f', sys_get_temp_dir());
32+
// ensure the file does not exist before the lock
33+
@unlink($file);
34+
35+
$lock->lock();
36+
37+
$this->assertFileExists($file);
38+
39+
$lock->release();
40+
}
41+
42+
public function testLockRelease()
43+
{
44+
$name = 'symfony-test-filesystem.lock';
45+
46+
$l1 = new LockHandler($name);
47+
$l2 = new LockHandler($name);
48+
49+
$this->assertTrue($l1->lock());
50+
$this->assertFalse($l2->lock());
51+
52+
$l1->release();
53+
54+
$this->assertTrue($l2->lock());
55+
$l2->release();
56+
}
57+
58+
public function testLockTwice()
59+
{
60+
$name = 'symfony-test-filesystem.lock';
61+
62+
$lockHandler = new LockHandler($name);
63+
64+
$this->assertTrue($lockHandler->lock());
65+
$this->assertTrue($lockHandler->lock());
66+
67+
$lockHandler->release();
68+
}
69+
70+
public function testLockIsReleased()
71+
{
72+
$name = 'symfony-test-filesystem.lock';
73+
74+
$l1 = new LockHandler($name);
75+
$l2 = new LockHandler($name);
76+
77+
$this->assertTrue($l1->lock());
78+
$this->assertFalse($l2->lock());
79+
80+
$l1 = null;
81+
82+
$this->assertTrue($l2->lock());
83+
$l2->release();
84+
}
85+
}

0 commit comments

Comments
 (0)