├── gevent_inotifyx ├── vendor │ ├── __init__.py │ └── inotifyx │ │ ├── distinfo.py │ │ ├── __init__.py │ │ └── binding.c └── __init__.py ├── requirements.txt ├── .gitignore ├── MANIFEST.in ├── Makefile ├── .travis.yml ├── CHANGELOG.md ├── example.py ├── LICENSE ├── setup.py ├── test_gevent_inotifyx.py ├── README.md └── README.rst /gevent_inotifyx/vendor/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | -e . 2 | 3 | pytest 4 | -------------------------------------------------------------------------------- /gevent_inotifyx/vendor/inotifyx/distinfo.py: -------------------------------------------------------------------------------- 1 | version = 'unknown' 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.egg-info 2 | *.pyc 3 | *.so 4 | .pytest_cache 5 | /build 6 | /dist 7 | __pycache__ 8 | MANIFEST 9 | -------------------------------------------------------------------------------- /MANIFEST.in: -------------------------------------------------------------------------------- 1 | include README.md 2 | include README.rst 3 | include CHANGELOG.md 4 | include LICENSE 5 | include example.py 6 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: test 2 | 3 | test: 4 | pytest 5 | 6 | README.rst: README.md 7 | pandoc --from=markdown --to=rst $< > $@ 8 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | - "2.7" 4 | - "3.6" 5 | - "3.7" 6 | - "3.8" 7 | - "3.9" 8 | install: pip install -r requirements.txt 9 | script: make test 10 | notifications: 11 | email: false 12 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.2 (2018-03-01) 4 | 5 | - Vendor a copy of inotifyx, with patches for Python 3 compatibility from the 6 | upstream bug tracker applied. 7 | - Support for Python 3 8 | 9 | ## 0.1.1 (2013-04-12) 10 | 11 | - Packaging fixes 12 | 13 | ## 0.1.0 (2012-12-07) 14 | 15 | - Initial release 16 | -------------------------------------------------------------------------------- /example.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | from __future__ import print_function 3 | 4 | import os 5 | import gevent 6 | import gevent_inotifyx as inotify 7 | 8 | def create_file_events(): 9 | """Open and close a file to generate inotify events.""" 10 | while True: 11 | with open('/tmp/test.txt', 'a'): 12 | pass 13 | gevent.sleep(1) 14 | 15 | 16 | def watch_for_events(): 17 | """Wait for events and print them to stdout.""" 18 | fd = inotify.init() 19 | try: 20 | wd = inotify.add_watch(fd, '/tmp', inotify.IN_CLOSE_WRITE) 21 | while True: 22 | for event in inotify.get_events(fd): 23 | print("event:", event.name, event.get_mask_description()) 24 | finally: 25 | os.close(fd) 26 | 27 | if __name__ == '__main__': 28 | tasks = [ 29 | gevent.spawn(watch_for_events), 30 | gevent.spawn(create_file_events), 31 | ] 32 | gevent.joinall(tasks) 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 Stanis Trendelenburg 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a 4 | copy of this software and associated documentation files (the "Software"), 5 | to deal in the Software without restriction, including without limitation 6 | the rights to use, copy, modify, merge, publish, distribute, sublicense, 7 | and/or sell copies of the Software, and to permit persons to whom the 8 | Software is furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included 11 | in all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 16 | THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR 17 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 18 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 19 | OTHER DEALINGS IN THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | import re 3 | from setuptools import setup, find_packages, Extension 4 | 5 | with open('gevent_inotifyx/__init__.py') as f: 6 | version = re.search(r"^__version__ = '(.*)'", f.read(), re.M).group(1) 7 | 8 | with open('README.rst') as f: 9 | README = f.read() 10 | 11 | setup( 12 | name='gevent_inotifyx', 13 | version=version, 14 | author='Stanis Trendelenburg', 15 | author_email='stanis.trendelenburg@gmail.com', 16 | url='https://github.com/trendels/gevent_inotifyx', 17 | license='MIT', 18 | packages=find_packages(), 19 | ext_modules=[ 20 | Extension( 21 | 'gevent_inotifyx.vendor.inotifyx.binding', 22 | sources=['gevent_inotifyx/vendor/inotifyx/binding.c'], 23 | ) 24 | ], 25 | install_requires=['gevent'], 26 | description='gevent compatibility for inotifyx', 27 | long_description=README, 28 | classifiers=[ 29 | 'License :: OSI Approved :: MIT License', 30 | 'Programming Language :: Python', 31 | 'Programming Language :: Python :: 2', 32 | 'Programming Language :: Python :: 3', 33 | ], 34 | ) 35 | -------------------------------------------------------------------------------- /test_gevent_inotifyx.py: -------------------------------------------------------------------------------- 1 | import os 2 | import unittest 3 | import gevent 4 | 5 | #import inotifyx as inotify 6 | import gevent_inotifyx as inotify 7 | 8 | class BasicTests(unittest.TestCase): 9 | def setUp(self): 10 | self.workdir = os.path.abspath(os.getcwd()) 11 | self.testdir = os.path.join(self.workdir, 'test') 12 | os.mkdir(self.testdir) 13 | os.chdir(self.testdir) 14 | self.fd = inotify.init() 15 | 16 | def tearDown(self): 17 | os.close(self.fd) 18 | os.rmdir(self.testdir) 19 | os.chdir(self.workdir) 20 | 21 | def _create_file(self, path, content = ''): 22 | f = open(path, 'w') 23 | try: 24 | f.write(content) 25 | finally: 26 | f.close() 27 | 28 | def test_get_events(self): 29 | wd = inotify.add_watch(self.fd, self.testdir, inotify.IN_CREATE) 30 | self._create_file('foo') 31 | try: 32 | events = inotify.get_events(self.fd) 33 | self.assertEqual(len(events), 1) 34 | self.assertEqual(events[0].mask, inotify.IN_CREATE) 35 | self.assertEqual(events[0].name, 'foo') 36 | finally: 37 | os.unlink('foo') 38 | 39 | def test_get_events_async(self): 40 | wd = inotify.add_watch(self.fd, self.testdir, inotify.IN_CREATE) 41 | task1 = gevent.spawn(inotify.get_events, self.fd, 10) 42 | task2 = gevent.spawn(self._create_file, 'bar') 43 | try: 44 | # in the synchronous case we would block here for 10s and the 45 | # return value in task1.value would be empty 46 | task1.join() 47 | task2.join() 48 | events = task1.value 49 | self.assertEqual(len(events), 1) 50 | self.assertEqual(events[0].mask, inotify.IN_CREATE) 51 | self.assertEqual(events[0].name, 'bar') 52 | finally: 53 | os.unlink('bar') 54 | 55 | if __name__ == "__main__": 56 | unittest.main() 57 | 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # gevent_inotifyx 2 | 3 | [![build-status-img]][build-status-url] 4 | [![PyPI](https://img.shields.io/pypi/v/gevent_inotifyx)](https://pypi.python.org/pypi/gevent_inotifyx) 5 | 6 | Gevent-compatible low-level inotify bindings based on [inotifyx]. 7 | 8 | - Python 2 and 3 compatible 9 | - Exposes a low-level [inotify(7)][inotify] API 10 | - Allows to wait for events in a non-blocking way when using [gevent]. 11 | 12 | [inotify]: http://man7.org/linux/man-pages/man7/inotify.7.html 13 | [inotifyx]: https://launchpad.net/inotifyx/ 14 | [gevent]: http://www.gevent.org/ 15 | [build-status-url]: https://travis-ci.org/trendels/gevent_inotifyx 16 | [build-status-img]: https://travis-ci.org/trendels/gevent_inotifyx.svg 17 | 18 | ## Installation 19 | 20 | $ pip install gevent_inotifyx 21 | 22 | From source: 23 | 24 | $ python setup.py install 25 | 26 | To run the tests: 27 | 28 | $ make test 29 | 30 | ## Examples 31 | 32 | Watch a directory while creating new files. This prints 33 | 34 | event: test.txt IN_CLOSE|IN_CLOSE_WRITE|IN_ALL_EVENTS 35 | 36 | every second: 37 | 38 | ```python 39 | #!/usr/bin/env python 40 | from __future__ import print_function 41 | import os 42 | import gevent 43 | import gevent_inotifyx as inotify 44 | 45 | def create_file_events(): 46 | """Open and close a file to generate inotify events.""" 47 | while True: 48 | with open('/tmp/test.txt', 'a'): 49 | pass 50 | gevent.sleep(1) 51 | 52 | def watch_for_events(): 53 | """Wait for events and print them to stdout.""" 54 | fd = inotify.init() 55 | try: 56 | wd = inotify.add_watch(fd, '/tmp', inotify.IN_CLOSE_WRITE) 57 | while True: 58 | for event in inotify.get_events(fd): 59 | print("event:", event.name, event.get_mask_description()) 60 | finally: 61 | os.close(fd) 62 | 63 | if __name__ == '__main__': 64 | tasks = [ 65 | gevent.spawn(watch_for_events), 66 | gevent.spawn(create_file_events), 67 | ] 68 | gevent.joinall(tasks) 69 | ``` 70 | 71 | ## License 72 | 73 | gevent_inotifyx is licensed under the MIT License. See the included file `LICENSE` for details. 74 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | gevent_inotifyx 2 | =============== 3 | 4 | |build-status-img| 5 | 6 | Gevent-compatible low-level inotify bindings based on 7 | `inotifyx `__. 8 | 9 | - Python 2 and 3 compatible 10 | - Exposes a low-level 11 | `inotify(7) `__ 12 | API 13 | - Allows to wait for events in a non-blocking way when using 14 | `gevent `__. 15 | 16 | Installation 17 | ------------ 18 | 19 | :: 20 | 21 | $ pip install gevent_inotifyx 22 | 23 | From source: 24 | 25 | :: 26 | 27 | $ python setup.py install 28 | 29 | To run the tests: 30 | 31 | :: 32 | 33 | $ make test 34 | 35 | Examples 36 | -------- 37 | 38 | Watch a directory while creating new files. This prints 39 | 40 | :: 41 | 42 | event: test.txt IN_CLOSE|IN_CLOSE_WRITE|IN_ALL_EVENTS 43 | 44 | every second: 45 | 46 | .. code:: python 47 | 48 | #!/usr/bin/env python 49 | from __future__ import print_function 50 | import os 51 | import gevent 52 | import gevent_inotifyx as inotify 53 | 54 | def create_file_events(): 55 | """Open and close a file to generate inotify events.""" 56 | while True: 57 | with open('/tmp/test.txt', 'a'): 58 | pass 59 | gevent.sleep(1) 60 | 61 | def watch_for_events(): 62 | """Wait for events and print them to stdout.""" 63 | fd = inotify.init() 64 | try: 65 | wd = inotify.add_watch(fd, '/tmp', inotify.IN_CLOSE_WRITE) 66 | while True: 67 | for event in inotify.get_events(fd): 68 | print("event:", event.name, event.get_mask_description()) 69 | finally: 70 | os.close(fd) 71 | 72 | if __name__ == '__main__': 73 | tasks = [ 74 | gevent.spawn(watch_for_events), 75 | gevent.spawn(create_file_events), 76 | ] 77 | gevent.joinall(tasks) 78 | 79 | License 80 | ------- 81 | 82 | gevent_inotifyx is licensed under the MIT License. See the included file 83 | ``LICENSE`` for details. 84 | 85 | .. |build-status-img| image:: https://travis-ci.org/trendels/gevent_inotifyx.svg 86 | :target: https://travis-ci.org/trendels/gevent_inotifyx 87 | -------------------------------------------------------------------------------- /gevent_inotifyx/__init__.py: -------------------------------------------------------------------------------- 1 | """ 2 | gevent compatibility for inotifyx 3 | 4 | General usage: 5 | >>> import os 6 | >>> import gevent_inotifyx as inotify 7 | >>> fd = inotify.init() 8 | >>> try: 9 | ... wd = inotify.add_watch(fd, '/path', inotify.IN_CREATE) 10 | ... events = inotify.get_events(fd) 11 | ... for event in events: 12 | ... print("File created: ", os.path.join('/path', event.name)) 13 | ... inotify.rm_watch(fd, wd) 14 | ... finally: 15 | ... os.close(fd) 16 | 17 | 18 | This module is designed as a drop-in replacement for inotifyx. 19 | Calling gevent_inotifyx.get_events() will only block the current greenlet 20 | instead of the current thread. 21 | """ 22 | 23 | __version__ = '0.2' 24 | 25 | import os 26 | import struct 27 | import sys 28 | 29 | from .vendor import inotifyx 30 | from .vendor.inotifyx import * 31 | from gevent.select import select 32 | 33 | _EVENT_FMT = 'iIII' 34 | _EVENT_SIZE = struct.calcsize(_EVENT_FMT) 35 | _BUF_LEN = inotifyx.BUF_LEN 36 | 37 | __all__ = ['InotifyEvent', 'add_watch', 'get_events', 'init', 'rm_watch'] 38 | __all__.extend([name for name in dir(inotifyx) if name.startswith('IN_')]) 39 | 40 | ENCODING = sys.getfilesystemencoding() 41 | 42 | # Replacement for the inotifyx_get_events() C function using gevent.select 43 | def get_events(fd, timeout=None): 44 | """get_events(fd[, timeout]) 45 | 46 | Return a list of InotifyEvent instances representing events read from 47 | inotify. If timeout is None, this will block forever until at least one 48 | event can be read. Otherwise, timeout should be an integer or float 49 | specifying a timeout in seconds. If get_events times out waiting for 50 | events, an empty list will be returned. If timeout is zero, get_events 51 | will not block. 52 | This version of get_events() will only block the current greenlet. 53 | """ 54 | (rlist, _, _) = select([fd], [], [], timeout) 55 | if not rlist: 56 | return [] 57 | events = [] 58 | 59 | while True: 60 | buf = os.read(fd, _BUF_LEN) 61 | i = 0 62 | while i < len(buf): 63 | (wd, mask, cookie, len_) = struct.unpack_from(_EVENT_FMT, buf, i) 64 | name = None 65 | if len_ > 0: 66 | start = i + _EVENT_SIZE 67 | end = start + len_ 68 | # remove \0 terminator and padding 69 | name = buf[start:end].rstrip(b'\0').decode(ENCODING) 70 | 71 | events.append(InotifyEvent(wd, mask, cookie, name)) 72 | i += _EVENT_SIZE + len_ 73 | 74 | (rlist, _, _) = select([fd], [], [], 0) 75 | if not rlist: 76 | break 77 | 78 | return events 79 | 80 | # Reproduced here from inotifyx/__init__.py to enable command line use: 81 | # $ python -m gevent_inofityx /path 82 | if __name__ == "__main__": 83 | import sys 84 | 85 | if len(sys.argv) == 1: 86 | sys.stderr.write('usage: gevent_inotifyx path [path ...]\n') 87 | sys.exit(1) 88 | 89 | paths = sys.argv[1:] 90 | fd = init() 91 | wd_to_path = {} 92 | try: 93 | for path in paths: 94 | wd = add_watch(fd, path) 95 | wd_to_path[wd] = path 96 | try: 97 | while True: 98 | events = get_events(fd) 99 | for event in events: 100 | path = wd_to_path[event.wd] 101 | parts = [event.get_mask_description()] 102 | if event.name: 103 | parts.append(event.name) 104 | print('%s: %s' % (path, ' '.join(parts))) 105 | except KeyboardInterrupt: 106 | pass 107 | 108 | finally: 109 | os.close(fd) 110 | 111 | -------------------------------------------------------------------------------- /gevent_inotifyx/vendor/inotifyx/__init__.py: -------------------------------------------------------------------------------- 1 | # Copyright (c) 2005 Manuel Amador 2 | # Copyright (c) 2009-2011 Forest Bond 3 | # 4 | # Permission is hereby granted, free of charge, to any person obtaining a 5 | # copy of this software and associated documentation files (the "Software"), 6 | # to deal in the Software without restriction, including without limitation 7 | # the rights to use, copy, modify, merge, publish, distribute, sublicense, 8 | # and/or sell copies of the Software, and to permit persons to whom the 9 | # Software is furnished to do so, subject to the following conditions: 10 | # 11 | # The above copyright notice and this permission notice shall be included in 12 | # all copies or substantial portions of the Software. 13 | # 14 | # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 15 | # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 16 | # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 17 | # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 18 | # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 19 | # FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 20 | # DEALINGS IN THE SOFTWARE. 21 | 22 | ''' 23 | inotifyx is a simple Python binding to the Linux inotify file system event 24 | monitoring API. 25 | 26 | Generally, usage is as follows: 27 | 28 | >>> fd = init() 29 | >>> try: 30 | ... wd = add_watch(fd, '/path', IN_ALL_EVENTS) 31 | ... events = get_events(fd) 32 | ... rm_watch(fd, wd) 33 | ... finally: 34 | ... os.close(fd) 35 | ''' 36 | from __future__ import print_function 37 | 38 | import os, select 39 | 40 | from . import binding 41 | from .distinfo import version as __version__ 42 | 43 | 44 | constants = {} 45 | 46 | for name in dir(binding): 47 | if name.startswith('IN_') or name == 'BUF_LEN': 48 | globals()[name] = constants[name] = getattr(binding, name) 49 | 50 | 51 | init = binding.init 52 | rm_watch = binding.rm_watch 53 | add_watch = binding.add_watch 54 | 55 | 56 | class InotifyEvent(object): 57 | ''' 58 | InotifyEvent(wd, mask, cookie, name) 59 | 60 | A representation of the inotify_event structure. See the inotify 61 | documentation for a description of these fields. 62 | ''' 63 | 64 | wd = None 65 | mask = None 66 | cookie = None 67 | name = None 68 | 69 | def __init__(self, wd, mask, cookie, name): 70 | self.wd = wd 71 | self.mask = mask 72 | self.cookie = cookie 73 | self.name = name 74 | 75 | def __str__(self): 76 | return '%s: %s' % (self.wd, self.get_mask_description()) 77 | 78 | def __repr__(self): 79 | return '%s(%s, %s, %s, %s)' % ( 80 | self.__class__.__name__, 81 | repr(self.wd), 82 | repr(self.mask), 83 | repr(self.cookie), 84 | repr(self.name), 85 | ) 86 | 87 | def get_mask_description(self): 88 | ''' 89 | Return an ASCII string describing the mask field in terms of 90 | bitwise-or'd IN_* constants, or 0. The result is valid Python code 91 | that could be eval'd to get the value of the mask field. In other 92 | words, for a given event: 93 | 94 | >>> from inotifyx import * 95 | >>> assert (event.mask == eval(event.get_mask_description())) 96 | ''' 97 | 98 | parts = [] 99 | for name, value in list(constants.items()): 100 | if self.mask & value: 101 | parts.append(name) 102 | if parts: 103 | return '|'.join(parts) 104 | return '0' 105 | 106 | 107 | def get_events(fd, *args): 108 | ''' 109 | get_events(fd[, timeout]) 110 | 111 | Return a list of InotifyEvent instances representing events read from 112 | inotify. If timeout is None, this will block forever until at least one 113 | event can be read. Otherwise, timeout should be an integer or float 114 | specifying a timeout in seconds. If get_events times out waiting for 115 | events, an empty list will be returned. If timeout is zero, get_events 116 | will not block. 117 | ''' 118 | return [ 119 | InotifyEvent(wd, mask, cookie, name) 120 | for wd, mask, cookie, name in binding.get_events(fd, *args) 121 | ] 122 | 123 | 124 | if __name__ == '__main__': 125 | import sys 126 | 127 | if len(sys.argv) == 1: 128 | sys.stderr.write('usage: inotify path [path ...]') 129 | sys.exit(1) 130 | 131 | paths = sys.argv[1:] 132 | 133 | fd = init() 134 | 135 | wd_to_path = {} 136 | 137 | try: 138 | for path in paths: 139 | wd = add_watch(fd, path) 140 | wd_to_path[wd] = path 141 | 142 | try: 143 | while True: 144 | events = get_events(fd) 145 | for event in events: 146 | path = wd_to_path[event.wd] 147 | parts = [event.get_mask_description()] 148 | if event.name: 149 | parts.append(event.name) 150 | print('%s: %s' % (path, ' '.join(parts))) 151 | except KeyboardInterrupt: 152 | pass 153 | 154 | finally: 155 | os.close(fd) 156 | -------------------------------------------------------------------------------- /gevent_inotifyx/vendor/inotifyx/binding.c: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (c) 2004 Novell, Inc. 3 | * Copyright (c) 2005 Manuel Amador 4 | * Copyright (c) 2009-2014 Forest Bond 5 | * Copyright (c) 2014 Henry Stern 6 | * 7 | * Permission is hereby granted, free of charge, to any person obtaining a 8 | * copy of this software and associated documentation files (the "Software"), 9 | * to deal in the Software without restriction, including without limitation 10 | * the rights to use, copy, modify, merge, publish, distribute, sublicense, 11 | * and/or sell copies of the Software, and to permit persons to whom the 12 | * Software is furnished to do so, subject to the following conditions: 13 | * 14 | * The above copyright notice and this permission notice shall be included in 15 | * all copies or substantial portions of the Software. 16 | * 17 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 20 | * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 21 | * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 22 | * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | * DEALINGS IN THE SOFTWARE. 24 | */ 25 | 26 | #include 27 | 28 | #include 29 | #include 30 | #include 31 | #include 32 | #include 33 | #include 34 | #include 35 | #include 36 | #include 37 | #include 38 | #include 39 | 40 | 41 | #define MAX_PENDING_PAUSE_COUNT 5 42 | #define PENDING_PAUSE_MICROSECONDS 2000 43 | #define PENDING_THRESHOLD(qsize) ((qsize) >> 1) 44 | 45 | #define EVENT_SIZE (sizeof (struct inotify_event)) 46 | /* From inotify(7): Specifying a buffer of size 47 | * 48 | * sizeof(struct inotify_event) + NAME_MAX + 1 49 | * 50 | * will be sufficient to read at least one event. 51 | */ 52 | #define BUF_LEN_MIN (EVENT_SIZE + NAME_MAX + 1) 53 | #define BUF_LEN_MAX 32768 54 | #define BUF_LEN ((BUF_LEN_MIN) > (BUF_LEN_MAX) ? (BUF_LEN_MIN) : (BUF_LEN_MAX)) 55 | 56 | #if PY_MAJOR_VERSION >= 3 57 | #define IS_PY3 58 | #endif 59 | 60 | static PyObject * inotifyx_init(PyObject *self, PyObject *args) { 61 | int fd; 62 | 63 | Py_BEGIN_ALLOW_THREADS; 64 | fd = inotify_init(); 65 | Py_END_ALLOW_THREADS; 66 | 67 | if(fd < 0) { 68 | PyErr_SetFromErrno(PyExc_IOError); 69 | return NULL; 70 | } 71 | return Py_BuildValue("i", fd); 72 | } 73 | 74 | 75 | static PyObject * inotifyx_add_watch(PyObject *self, PyObject *args) { 76 | int fd; 77 | char *path; 78 | int watch_descriptor; 79 | uint32_t mask; 80 | 81 | mask = IN_ALL_EVENTS; 82 | 83 | if(! PyArg_ParseTuple(args, "is|i", &fd, &path, &mask)) { 84 | return NULL; 85 | } 86 | 87 | Py_BEGIN_ALLOW_THREADS; 88 | watch_descriptor = inotify_add_watch(fd, (const char *)path, mask); 89 | Py_END_ALLOW_THREADS; 90 | 91 | if(watch_descriptor < 0) { 92 | PyErr_SetFromErrno(PyExc_IOError); 93 | return NULL; 94 | } 95 | return Py_BuildValue("i", watch_descriptor); 96 | } 97 | 98 | 99 | static PyObject * inotifyx_rm_watch(PyObject *self, PyObject *args) { 100 | int fd; 101 | int watch_descriptor; 102 | int retvalue; 103 | 104 | if(! PyArg_ParseTuple(args, "ii", &fd, &watch_descriptor)) { 105 | return NULL; 106 | } 107 | 108 | Py_BEGIN_ALLOW_THREADS; 109 | retvalue = inotify_rm_watch(fd, watch_descriptor); 110 | Py_END_ALLOW_THREADS; 111 | 112 | if(retvalue < 0) { 113 | PyErr_SetFromErrno(PyExc_IOError); 114 | return NULL; 115 | } 116 | return Py_BuildValue("i", retvalue); 117 | } 118 | 119 | 120 | static PyObject * inotifyx_get_events(PyObject *self, PyObject *args) { 121 | int fd; 122 | 123 | static char buf[BUF_LEN]; 124 | int i; 125 | int len; 126 | 127 | float timeout_arg; 128 | 129 | struct timeval timeout; 130 | void *ptimeout; 131 | 132 | struct inotify_event *event; 133 | 134 | fd_set read_fds; 135 | int select_retval; 136 | 137 | timeout_arg = -1.0; 138 | if(! PyArg_ParseTuple(args, "i|f", &fd, &timeout_arg)) { 139 | return NULL; 140 | } 141 | 142 | if(timeout_arg < 0.0) { 143 | ptimeout = NULL; 144 | } else { 145 | timeout.tv_sec = (int)timeout_arg; 146 | timeout.tv_usec = (int)(1000000.0 * (timeout_arg - (float)((int) timeout_arg))); 147 | ptimeout = &timeout; 148 | } 149 | 150 | FD_ZERO(&read_fds); 151 | FD_SET(fd, &read_fds); 152 | 153 | Py_BEGIN_ALLOW_THREADS; 154 | select_retval = select(fd + 1, &read_fds, NULL, NULL, ptimeout); 155 | Py_END_ALLOW_THREADS; 156 | 157 | if(select_retval == 0) { 158 | // Timed out. 159 | return PyList_New(0); 160 | } else if(select_retval < 0) { 161 | PyErr_SetFromErrno(PyExc_IOError); 162 | return NULL; 163 | } 164 | 165 | PyObject* retvalue = PyList_New(0); 166 | if (retvalue == NULL) 167 | return NULL; 168 | 169 | timeout.tv_sec = 0; 170 | timeout.tv_usec = 0; 171 | ptimeout = &timeout; 172 | 173 | while (1) { 174 | Py_BEGIN_ALLOW_THREADS; 175 | len = read(fd, buf, BUF_LEN); 176 | Py_END_ALLOW_THREADS; 177 | 178 | if(len < 0) { 179 | PyErr_SetFromErrno(PyExc_IOError); 180 | Py_DECREF(retvalue); 181 | return NULL; 182 | } else if(len == 0) { 183 | PyErr_SetString(PyExc_IOError, "event buffer too small"); 184 | Py_DECREF(retvalue); 185 | return NULL; 186 | } 187 | 188 | i = 0; 189 | while(i < len) { 190 | event = (struct inotify_event *)(& buf[i]); 191 | 192 | PyObject* value = NULL; 193 | if(event->len > 0 && event->name[0] != '\0') { 194 | value = Py_BuildValue( 195 | "iiis", 196 | event->wd, 197 | event->mask, 198 | event->cookie, 199 | event->name 200 | ); 201 | } else { 202 | value = Py_BuildValue( 203 | "iiiO", 204 | event->wd, 205 | event->mask, 206 | event->cookie, 207 | Py_None 208 | ); 209 | } 210 | if(PyList_Append(retvalue, value) == -1) { 211 | Py_DECREF(retvalue); 212 | Py_DECREF(value); 213 | return NULL; 214 | } 215 | Py_DECREF(value); 216 | 217 | i += EVENT_SIZE + event->len; 218 | } 219 | 220 | FD_ZERO(&read_fds); 221 | FD_SET(fd, &read_fds); 222 | 223 | Py_BEGIN_ALLOW_THREADS; 224 | select_retval = select(fd + 1, &read_fds, NULL, NULL, ptimeout); 225 | Py_END_ALLOW_THREADS; 226 | 227 | if (select_retval <= 0) 228 | break; 229 | } 230 | 231 | return retvalue; 232 | } 233 | 234 | 235 | static PyMethodDef InotifyMethods[] = { 236 | { 237 | "init", 238 | inotifyx_init, 239 | METH_VARARGS, 240 | ( 241 | "init()\n\n" 242 | "Initialize an inotify instance and return the associated file\n" 243 | "descriptor. The file descriptor should be closed via os.close\n" 244 | "after it is no longer needed." 245 | ) 246 | }, 247 | { 248 | "add_watch", 249 | inotifyx_add_watch, 250 | METH_VARARGS, 251 | ( 252 | "add_watch(fd, path[, mask])\n\n" 253 | "Add a watch for path and return the watch descriptor.\n" 254 | "fd should be the file descriptor returned by init.\n" 255 | "If left unspecified, mask defaults to IN_ALL_EVENTS.\n" 256 | "See the inotify documentation for details." 257 | ) 258 | }, 259 | { 260 | "rm_watch", 261 | inotifyx_rm_watch, 262 | METH_VARARGS, 263 | ( 264 | "rm_watch(fd, wd)\n\n" 265 | "Remove the watch associated with watch descriptor wd.\n" 266 | "fd should be the file descriptor returned by init.\n" 267 | ) 268 | }, 269 | { 270 | "get_events", 271 | inotifyx_get_events, 272 | METH_VARARGS, 273 | "get_events(fd[, timeout])\n\n" 274 | "Read events from inotify and return a list of tuples " 275 | "(wd, mask, cookie, name).\n" 276 | "The name field is None if no name is associated with the inotify event.\n" 277 | "Timeout specifies a timeout in seconds (as an integer or float).\n" 278 | "If left unspecified, there is no timeout and get_events will block\n" 279 | "indefinitely. If timeout is zero, get_events will not block." 280 | }, 281 | {NULL, NULL, 0, NULL} 282 | }; 283 | 284 | #ifdef IS_PY3 285 | static struct PyModuleDef inotifybinding = 286 | { 287 | PyModuleDef_HEAD_INIT, 288 | "gevent_inotifyx.vendor.inotifyx.binding", 289 | ( 290 | "Low-level interface to inotify. Do not use this module directly.\n" 291 | "Instead, use the inotifyx module." 292 | ), 293 | -1, 294 | InotifyMethods 295 | }; 296 | 297 | PyMODINIT_FUNC PyInit_binding(void) { 298 | PyObject *module = PyModule_Create(&inotifybinding); 299 | 300 | if (module == NULL) 301 | return NULL; 302 | #else 303 | PyMODINIT_FUNC initbinding(void) { 304 | PyObject* module = Py_InitModule3( 305 | "gevent_inotifyx.vendor.inotifyx.binding", 306 | InotifyMethods, 307 | ( 308 | "Low-level interface to inotify. Do not use this module directly.\n" 309 | "Instead, use the inotifyx module." 310 | ) 311 | ); 312 | 313 | if (module == NULL) 314 | return; 315 | #endif 316 | 317 | PyModule_AddIntConstant(module, "IN_ACCESS", IN_ACCESS); 318 | PyModule_AddIntConstant(module, "IN_MODIFY", IN_MODIFY); 319 | PyModule_AddIntConstant(module, "IN_ATTRIB", IN_ATTRIB); 320 | PyModule_AddIntConstant(module, "IN_CLOSE_WRITE", IN_CLOSE_WRITE); 321 | PyModule_AddIntConstant(module, "IN_CLOSE_NOWRITE", IN_CLOSE_NOWRITE); 322 | PyModule_AddIntConstant(module, "IN_CLOSE", IN_CLOSE); 323 | PyModule_AddIntConstant(module, "IN_OPEN", IN_OPEN); 324 | PyModule_AddIntConstant(module, "IN_MOVED_FROM", IN_MOVED_FROM); 325 | PyModule_AddIntConstant(module, "IN_MOVED_TO", IN_MOVED_TO); 326 | PyModule_AddIntConstant(module, "IN_MOVE", IN_MOVE); 327 | PyModule_AddIntConstant(module, "IN_CREATE", IN_CREATE); 328 | PyModule_AddIntConstant(module, "IN_DELETE", IN_DELETE); 329 | PyModule_AddIntConstant(module, "IN_DELETE_SELF", IN_DELETE_SELF); 330 | PyModule_AddIntConstant(module, "IN_MOVE_SELF", IN_MOVE_SELF); 331 | PyModule_AddIntConstant(module, "IN_UNMOUNT", IN_UNMOUNT); 332 | PyModule_AddIntConstant(module, "IN_Q_OVERFLOW", IN_Q_OVERFLOW); 333 | PyModule_AddIntConstant(module, "IN_IGNORED", IN_IGNORED); 334 | PyModule_AddIntConstant(module, "IN_ONLYDIR", IN_ONLYDIR); 335 | PyModule_AddIntConstant(module, "IN_DONT_FOLLOW", IN_DONT_FOLLOW); 336 | PyModule_AddIntConstant(module, "IN_MASK_ADD", IN_MASK_ADD); 337 | PyModule_AddIntConstant(module, "IN_ISDIR", IN_ISDIR); 338 | PyModule_AddIntConstant(module, "IN_ONESHOT", IN_ONESHOT); 339 | PyModule_AddIntConstant(module, "IN_ALL_EVENTS", IN_ALL_EVENTS); 340 | PyModule_AddIntConstant(module, "BUF_LEN", BUF_LEN); 341 | #ifdef IS_PY3 342 | return module; 343 | #endif 344 | } 345 | --------------------------------------------------------------------------------