-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy path__init__.py
More file actions
342 lines (272 loc) · 10.5 KB
/
Copy path__init__.py
File metadata and controls
342 lines (272 loc) · 10.5 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
"""
NLPL File System Watching Module.
Provides cross-platform file and directory watching via the watchdog library
(inotify on Linux, FSEvents on macOS, ReadDirectoryChangesW on Windows).
Uses a thread-safe event queue architecture: watchdog runs in a background
OS thread; events are staged into per-watcher queues; NexusLang programs drain
queues with fs_watch_poll() from the interpreter thread without risk of
re-entry.
Registered functions (callable from NexusLang programs):
fs_watch_start(path, recursive=False) -> String
Start watching *path* (must be a directory).
Returns a watcher_id string used to identify this watcher.
Raises ImportError if watchdog is not installed.
Raises OSError if path does not exist or is not a directory.
fs_watch_stop(watcher_id) -> Boolean
Stop and remove the watcher identified by *watcher_id*.
Returns True on success, False if watcher_id is unknown.
fs_watch_stop_all() -> Integer
Stop and remove all active watchers.
Returns the count of watchers that were stopped.
fs_watch_poll(watcher_id) -> List
Drain and return all pending events for *watcher_id*.
Returns an empty list if the watcher is unknown or has no events.
Each event is a Dictionary:
{
"type": "created" | "modified" | "deleted" | "moved",
"path": "/absolute/path/to/target",
"is_dir": True | False,
"dest_path": "/absolute/new/path" # only for "moved", else None
}
fs_watch_list() -> List
Return a List of Dictionaries describing all active watchers:
{
"id": "<watcher_id>",
"path": "/watched/path",
"recursive": True | False,
"active": True | False
}
fs_watch_is_active(watcher_id) -> Boolean
True when the watcher exists and its background observer is alive.
fs_watch_path(watcher_id) -> String
Return the path registered for *watcher_id*, or "" if unknown.
fs_watch_clear(watcher_id) -> Integer
Discard all pending events for *watcher_id* without returning them.
Returns the number of events discarded.
"""
from __future__ import annotations
import os
import queue
import threading
import uuid
import atexit
from typing import Any, Dict, List, Optional
try:
from watchdog.observers import Observer
from watchdog.events import (
FileSystemEventHandler,
FileCreatedEvent,
FileModifiedEvent,
FileDeletedEvent,
FileMovedEvent,
DirCreatedEvent,
DirModifiedEvent,
DirDeletedEvent,
DirMovedEvent,
)
HAS_WATCHDOG = True
except ImportError:
HAS_WATCHDOG = False
# Provide a minimal stub so that class definitions below don't
# raise NameError at module-import time on systems without watchdog.
class FileSystemEventHandler: # type: ignore[no-redef]
pass
class Observer: # type: ignore[no-redef]
pass
# ---------------------------------------------------------------------------
# Internal registry
# ---------------------------------------------------------------------------
# watcher_id -> _WatcherEntry
_WATCHERS: Dict[str, "_WatcherEntry"] = {}
_WATCHERS_LOCK = threading.Lock()
class _WatcherEntry:
"""Holds state for one active watcher."""
def __init__(self, watcher_id: str, path: str, recursive: bool) -> None:
self.watcher_id = watcher_id
self.path = path
self.recursive = recursive
self.event_queue: queue.Queue = queue.Queue()
self.observer: Optional[Any] = None # watchdog Observer
@property
def active(self) -> bool:
return self.observer is not None and self.observer.is_alive()
class _QueueHandler(FileSystemEventHandler):
"""Watchdog event handler that enqueues events as plain dicts."""
def __init__(self, event_queue: queue.Queue) -> None:
super().__init__()
self._queue = event_queue
def _enqueue(self, event_type: str, event: Any) -> None:
dest = getattr(event, "dest_path", None)
self._queue.put_nowait({
"type": event_type,
"path": os.path.abspath(event.src_path),
"is_dir": event.is_directory,
"dest_path": os.path.abspath(dest) if dest else None,
})
def on_created(self, event): # noqa: D401
self._enqueue("created", event)
def on_modified(self, event): # noqa: D401
self._enqueue("modified", event)
def on_deleted(self, event): # noqa: D401
self._enqueue("deleted", event)
def on_moved(self, event): # noqa: D401
self._enqueue("moved", event)
# ---------------------------------------------------------------------------
# Public functions
# ---------------------------------------------------------------------------
def fs_watch_start(path: str, recursive: bool = False) -> str:
"""Start watching *path* (must be a directory). Returns a watcher_id string."""
if not HAS_WATCHDOG:
raise ImportError("watchdog package required: pip install watchdog")
abs_path = os.path.abspath(path)
if not os.path.exists(abs_path):
raise OSError(f"fs_watch_start: path does not exist: {abs_path!r}")
if not os.path.isdir(abs_path):
raise OSError(f"fs_watch_start: path is not a directory: {abs_path!r}")
watcher_id = str(uuid.uuid4())
entry = _WatcherEntry(watcher_id, abs_path, bool(recursive))
handler = _QueueHandler(entry.event_queue)
observer = Observer()
try:
observer.schedule(handler, abs_path, recursive=bool(recursive))
observer.start()
except Exception:
# Ensure partially-started observers never leak kernel watch resources.
try:
observer.stop()
except Exception:
pass
try:
observer.join(timeout=1.0)
except Exception:
pass
raise
entry.observer = observer
with _WATCHERS_LOCK:
_WATCHERS[watcher_id] = entry
return watcher_id
def fs_watch_stop(watcher_id: str) -> bool:
"""Stop and remove the watcher with *watcher_id*. Returns True on success."""
with _WATCHERS_LOCK:
entry = _WATCHERS.pop(watcher_id, None)
if entry is None:
return False
try:
if entry.observer is not None:
# Unschedule first so recursive emitters release inotify watches promptly.
try:
entry.observer.unschedule_all()
except Exception:
pass
entry.observer.stop()
entry.observer.join(timeout=5.0)
except Exception:
pass
finally:
entry.observer = None
return True
def fs_watch_stop_all() -> int:
"""Stop all active watchers. Returns count stopped."""
with _WATCHERS_LOCK:
ids = list(_WATCHERS.keys())
count = 0
for watcher_id in ids:
if fs_watch_stop(watcher_id):
count += 1
return count
@atexit.register
def _cleanup_watchers_at_exit() -> None:
"""Best-effort watcher cleanup for interpreter shutdown paths."""
try:
fs_watch_stop_all()
except Exception:
pass
def fs_watch_poll(watcher_id: str) -> List[Dict[str, Any]]:
"""Drain and return all pending events for *watcher_id*.
Returns [] if the watcher is unknown.
"""
with _WATCHERS_LOCK:
entry = _WATCHERS.get(watcher_id)
if entry is None:
return []
events: List[Dict[str, Any]] = []
try:
while True:
events.append(entry.event_queue.get_nowait())
except queue.Empty:
pass
return events
def fs_watch_list() -> List[Dict[str, Any]]:
"""Return a list of dicts describing all active watchers."""
with _WATCHERS_LOCK:
snapshot = list(_WATCHERS.values())
return [
{
"id": e.watcher_id,
"path": e.path,
"recursive": e.recursive,
"active": e.active,
}
for e in snapshot
]
def fs_watch_is_active(watcher_id: str) -> bool:
"""True when the watcher exists and its observer thread is alive."""
with _WATCHERS_LOCK:
entry = _WATCHERS.get(watcher_id)
return entry is not None and entry.active
def fs_watch_path(watcher_id: str) -> str:
"""Return the watched path for *watcher_id*, or empty string if unknown."""
with _WATCHERS_LOCK:
entry = _WATCHERS.get(watcher_id)
return entry.path if entry is not None else ""
def fs_watch_clear(watcher_id: str) -> int:
"""Discard all pending events for *watcher_id*. Returns count discarded."""
with _WATCHERS_LOCK:
entry = _WATCHERS.get(watcher_id)
if entry is None:
return 0
count = 0
try:
while True:
entry.event_queue.get_nowait()
count += 1
except queue.Empty:
pass
return count
# ---------------------------------------------------------------------------
# Registration
# ---------------------------------------------------------------------------
def register_fs_watch_functions(runtime) -> None:
"""Register file system watching functions with the NexusLang runtime."""
if HAS_WATCHDOG:
runtime.register_function("fs_watch_start", fs_watch_start)
runtime.register_function("fs_watch_stop", fs_watch_stop)
runtime.register_function("fs_watch_stop_all", fs_watch_stop_all)
runtime.register_function("fs_watch_poll", fs_watch_poll)
runtime.register_function("fs_watch_list", fs_watch_list)
runtime.register_function("fs_watch_is_active", fs_watch_is_active)
runtime.register_function("fs_watch_path", fs_watch_path)
runtime.register_function("fs_watch_clear", fs_watch_clear)
else:
# Register stubs that raise a clear ImportError so NexusLang programs get
# a useful message rather than "function not found".
def _no_watchdog(*args, **kwargs):
raise ImportError("watchdog package required: pip install watchdog")
for name in (
"fs_watch_start", "fs_watch_stop", "fs_watch_stop_all",
"fs_watch_poll", "fs_watch_list", "fs_watch_is_active",
"fs_watch_path", "fs_watch_clear",
):
runtime.register_function(name, _no_watchdog)
__all__ = [
"fs_watch_start",
"fs_watch_stop",
"fs_watch_stop_all",
"fs_watch_poll",
"fs_watch_list",
"fs_watch_is_active",
"fs_watch_path",
"fs_watch_clear",
"register_fs_watch_functions",
"HAS_WATCHDOG",
]