Skip to content

Commit 2a86806

Browse files
authored
Merge pull request #1 from phprtc/recursive-dir-watch
Recursive Directory Watching
2 parents ae8c54d + 6f6a64d commit 2a86806

File tree

3 files changed

+144
-27
lines changed

3 files changed

+144
-27
lines changed

src/Watcher.php

Lines changed: 130 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -4,31 +4,45 @@
44
namespace RTC\Watcher;
55

66

7-
use JetBrains\PhpStorm\Pure;
7+
use RecursiveDirectoryIterator;
8+
use RecursiveIteratorIterator;
89
use RTC\Watcher\Watching\EventInfo;
910
use RTC\Watcher\Watching\EventTrait;
1011
use RTC\Watcher\Watching\WatchedItem;
11-
use Swoole\Event as SWEvent;
12+
use Swoole\Event as SwooleEvent;
1213

1314
class Watcher
1415
{
15-
use EventTrait;
16+
use EventTrait {
17+
EventTrait::__construct as private __eventTraitConstructor;
18+
}
1619

1720
protected mixed $inotifyFD;
1821
protected array $paths = [];
1922
protected array $extensions = [];
2023
protected array $watchedItems = [];
2124

25+
protected readonly int $maskItemCreated;
26+
protected readonly int $maskItemDeleted;
27+
2228
protected array $fileShouldNotEndWith = [
2329
'~'
2430
];
2531

2632

27-
#[Pure] public static function create(): Watcher
33+
public static function create(): Watcher
2834
{
2935
return new Watcher();
3036
}
3137

38+
public function __construct()
39+
{
40+
$this->__eventTraitConstructor();
41+
42+
$this->maskItemCreated = Event::ON_CREATE_HIGH->value;
43+
$this->maskItemDeleted = Event::ON_DELETE_HIGH->value;
44+
}
45+
3246
/**
3347
* Get inotify file descriptor
3448
*
@@ -44,6 +58,8 @@ public function getInotifyFD(): mixed
4458
}
4559

4660
/**
61+
* Returns list of files currently being watched
62+
*
4763
* @return WatchedItem[]
4864
*/
4965
public function getWatchedItems(): array
@@ -58,49 +74,143 @@ public function getWatchedItems(): array
5874
*/
5975
public function watch(): void
6076
{
61-
static $index = 1;
62-
6377
// Register paths
6478
foreach ($this->paths as $path) {
65-
$this->watchedItems[$index] = $path;
66-
inotify_add_watch($this->getInotifyFD(), $path, $this->event->value);
67-
$index += 1;
79+
$this->recursivelyRegisterInotifyEvent($path);
6880
}
6981

7082
// Set up a new event listener for inotify read events
71-
SWEvent::add($this->getInotifyFD(), function () {
72-
$events = inotify_read($this->getInotifyFD());
83+
SwooleEvent::add($this->getInotifyFD(), function () {
84+
$inotifyEvents = inotify_read($this->getInotifyFD());
7385

7486
// IF WE ARE LISTENING TO 'ON_ALL_EVENTS'
7587
if ($this->willWatchAny) {
76-
foreach ($events as $event) {
77-
if (!empty($event['name'])) { // Filter out invalid events
78-
$this->fireEvent($event);
79-
}
88+
foreach ($inotifyEvents as $inotifyEvent) {
89+
$this->inotifyPerformAdditionalOperations($inotifyEvent);
90+
91+
$this->fireEvent($inotifyEvent);
8092
}
8193

8294
return;
8395
}
8496

8597
// INDIVIDUAL LISTENERS
86-
foreach ($events as $event) {
98+
foreach ($inotifyEvents as $inotifyEvent) {
99+
$this->inotifyPerformAdditionalOperations($inotifyEvent);
100+
87101
// Make sure that we support this event
88-
if (array_key_exists($event['mask'], self::$constants)) {
89-
$this->fireEvent($event);
102+
if ($inotifyEvent['mask'] == $this->event->value) {
103+
$this->fireEvent($inotifyEvent);
90104
}
91105
}
92106

93107
});
94108

95109
// Set to monitor and listen for read events for the given $fd
96-
SWEvent::set(
110+
SwooleEvent::set(
97111
$this->getInotifyFD(),
98112
null,
99113
null,
100114
SWOOLE_EVENT_READ
101115
);
102116
}
103117

118+
/**
119+
* Handles directory creation/deletion on the fly
120+
*
121+
* @param array $inotifyEvent
122+
* @return void
123+
*/
124+
private function inotifyPerformAdditionalOperations(array $inotifyEvent): void
125+
{
126+
// Handle directory creation
127+
if ($inotifyEvent['mask'] == $this->maskItemCreated) {
128+
$eventInfo = new EventInfo($inotifyEvent, $this->watchedItems[$inotifyEvent['wd']]);
129+
// Register this path also if it's directory
130+
if ($eventInfo->getWatchedItem()->isDir()) {
131+
$this->recursivelyRegisterInotifyEvent($eventInfo->getWatchedItem()->getFullPath());
132+
}
133+
134+
return;
135+
}
136+
137+
// Handle directory deletion
138+
if ($inotifyEvent['mask'] == $this->maskItemDeleted) {
139+
$eventInfo = new EventInfo($inotifyEvent, $this->watchedItems[$inotifyEvent['wd']]);
140+
// Remove this path also if it's directory
141+
if ($eventInfo->getWatchedItem()->isDir()) {
142+
$this->removeInotifyEvent($eventInfo);
143+
}
144+
}
145+
}
146+
147+
/**
148+
* Register directory/file to inotify watcher
149+
* Loops through directory recursively and register all it's subdirectories as well
150+
*
151+
* @param string $path
152+
* @return void
153+
*/
154+
private function recursivelyRegisterInotifyEvent(string $path): void
155+
{
156+
if (is_dir($path)) {
157+
$iterator = new RecursiveDirectoryIterator($path);
158+
159+
// Loop through files
160+
foreach (new RecursiveIteratorIterator($iterator) as $file) {
161+
if ($file->isDir()/**&& !in_array($file->getRealPath(), $this->watchedItems)**/) {
162+
$this->registerInotifyEvent($file->getRealPath());
163+
}
164+
}
165+
166+
return;
167+
}
168+
169+
// Register file watch
170+
$this->registerInotifyEvent($path);
171+
}
172+
173+
/**
174+
* Register directory/file to inotify watcher
175+
*
176+
* @param string $path
177+
* @return void
178+
*/
179+
private function registerInotifyEvent(string $path): void
180+
{
181+
$descriptor = inotify_add_watch(
182+
$this->getInotifyFD(),
183+
$path,
184+
Event::ON_ALL_EVENTS->value
185+
);
186+
187+
$this->watchedItems[$descriptor] = [
188+
'path' => $path,
189+
'mask' => $this->event->value,
190+
];
191+
}
192+
193+
/**
194+
* Stop watching file/directory
195+
*
196+
* @param EventInfo $eventInfo
197+
* @return void
198+
*/
199+
public function removeInotifyEvent(EventInfo $eventInfo)
200+
{
201+
// Stop watching event
202+
inotify_rm_watch($this->getInotifyFD(), $eventInfo->getWatchDescriptor());
203+
204+
// Stop tracking descriptor
205+
unset($this->watchedItems[$eventInfo->getWatchDescriptor()]);
206+
}
207+
208+
/**
209+
* Trigger an event
210+
*
211+
* @param array $inotifyEvent
212+
* @return void
213+
*/
104214
private function fireEvent(array $inotifyEvent): void
105215
{
106216
$shouldFireEvent = array_key_exists($inotifyEvent['mask'], self::$constants);
@@ -131,7 +241,7 @@ private function fireEvent(array $inotifyEvent): void
131241

132242
$eventMask = $this->willWatchAny
133243
? Event::ON_ALL_EVENTS->value
134-
: $eventInfo->getMask();
244+
: $eventInfo->getMask()->value;
135245

136246
$this->eventEmitter->emit($eventMask, [$eventInfo]);
137247
}

src/Watching/EventInfo.php

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
namespace RTC\Watcher\Watching;
44

5+
use JetBrains\PhpStorm\Pure;
6+
use RTC\Watcher\Event;
57
use RTC\Watcher\Watcher;
68

79
class EventInfo
@@ -13,17 +15,17 @@ class EventInfo
1315

1416
public function __construct(
1517
protected array $event,
16-
string $path
18+
array $pathData
1719
)
1820
{
19-
$this->watchedItem = new WatchedItem($path, $this);
21+
$this->watchedItem = new WatchedItem($pathData['path'], $this);
2022
$this->eventMask = $this->event['mask'];
2123
$this->eventInfo = Watcher::$constants[$event['mask']];
2224
}
2325

24-
public function getMask(): int
26+
public function getMask(): Event
2527
{
26-
return $this->eventMask;
28+
return Event::from($this->eventMask);
2729
}
2830

2931
public function getName(): string
@@ -44,6 +46,11 @@ public function getEvent(): array
4446
return $this->event;
4547
}
4648

49+
#[Pure] public function getWatchDescriptor(): int
50+
{
51+
return $this->getEvent()['wd'];
52+
}
53+
4754
/**
4855
* @return WatchedItem
4956
*/

src/Watching/EventTrait.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,9 +35,9 @@ trait EventTrait
3535
32768 => ['ON_IGNORED', 'Watch was removed (explicitly by inotify_rm_watch() or because file was removed or filesystem unmounted'],
3636
1073741824 => ['ON_ISDIR', 'Subject of this event is a directory'],
3737
1073741840 => ['ON_CLOSE_NOWRITE', 'High-bit: File not opened for writing was closed'],
38-
1073741856 => ['ON_OPEN', 'High-bit: File was opened'],
39-
1073742080 => ['ON_CREATE', 'High-bit: File or directory created in watched directory'],
40-
1073742336 => ['ON_DELETE', 'High-bit: File or directory deleted in watched directory'],
38+
1073741856 => ['ON_OPEN_HIGH', 'High-bit: File was opened'],
39+
1073742080 => ['ON_CREATE_HIGH', 'High-bit: File or directory created in watched directory'],
40+
1073742336 => ['ON_DELETE_HIGH', 'High-bit: File or directory deleted in watched directory'],
4141
16777216 => ['ON_ONLYDIR', 'Only watch pathname if it is a directory (Since Linux 2.6.15)'],
4242
33554432 => ['ON_DONT_FOLLOW', 'Do not dereference pathname if it is a symlink (Since Linux 2.6.15)'],
4343
536870912 => ['ON_MASK_ADD', 'Add events to watch mask for this pathname if it already exists (instead of replacing mask).'],

0 commit comments

Comments
 (0)