summaryrefslogtreecommitdiffstats
path: root/powerline/lib/inotify.py
blob: 8b74a7f1f456c9eed91f0bd66c14ef9b1ce10bf0 (plain)
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
# vim:fileencoding=utf-8:noet
from __future__ import (unicode_literals, division, absolute_import, print_function)

import sys
import os
import errno
import ctypes
import struct

from ctypes.util import find_library

from powerline.lib.encoding import get_preferred_file_name_encoding


__copyright__ = '2013, Kovid Goyal <kovid at kovidgoyal.net>'
__docformat__ = 'restructuredtext en'


class INotifyError(Exception):
	pass


_inotify = None


def load_inotify():
	''' Initialize the inotify library '''
	global _inotify
	if _inotify is None:
		if hasattr(sys, 'getwindowsversion'):
			# On windows abort before loading the C library. Windows has
			# multiple, incompatible C runtimes, and we have no way of knowing
			# if the one chosen by ctypes is compatible with the currently
			# loaded one.
			raise INotifyError('INotify not available on windows')
		if sys.platform == 'darwin':
			raise INotifyError('INotify not available on OS X')
		if not hasattr(ctypes, 'c_ssize_t'):
			raise INotifyError('You need python >= 2.7 to use inotify')
		name = find_library('c')
		if not name:
			raise INotifyError('Cannot find C library')
		libc = ctypes.CDLL(name, use_errno=True)
		for function in ('inotify_add_watch', 'inotify_init1', 'inotify_rm_watch'):
			if not hasattr(libc, function):
				raise INotifyError('libc is too old')
		# inotify_init1()
		prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, use_errno=True)
		init1 = prototype(('inotify_init1', libc), ((1, 'flags', 0),))

		# inotify_add_watch()
		prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_char_p, ctypes.c_uint32, use_errno=True)
		add_watch = prototype(('inotify_add_watch', libc), (
			(1, 'fd'), (1, 'pathname'), (1, 'mask')))

		# inotify_rm_watch()
		prototype = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int, ctypes.c_int, use_errno=True)
		rm_watch = prototype(('inotify_rm_watch', libc), (
			(1, 'fd'), (1, 'wd')))

		# read()
		prototype = ctypes.CFUNCTYPE(ctypes.c_ssize_t, ctypes.c_int, ctypes.c_void_p, ctypes.c_size_t, use_errno=True)
		read = prototype(('read', libc), (
			(1, 'fd'), (1, 'buf'), (1, 'count')))
		_inotify = (init1, add_watch, rm_watch, read)
	return _inotify


class INotify(object):

	# See <sys/inotify.h> for the flags defined below

	# Supported events suitable for MASK parameter of INOTIFY_ADD_WATCH.
	ACCESS = 0x00000001         # File was accessed.
	MODIFY = 0x00000002         # File was modified.
	ATTRIB = 0x00000004         # Metadata changed.
	CLOSE_WRITE = 0x00000008    # Writtable file was closed.
	CLOSE_NOWRITE = 0x00000010  # Unwrittable file closed.
	OPEN = 0x00000020           # File was opened.
	MOVED_FROM = 0x00000040     # File was moved from X.
	MOVED_TO = 0x00000080       # File was moved to Y.
	CREATE = 0x00000100         # Subfile was created.
	DELETE = 0x00000200         # Subfile was deleted.
	DELETE_SELF = 0x00000400    # Self was deleted.
	MOVE_SELF = 0x00000800      # Self was moved.

	# Events sent by the kernel.
	UNMOUNT = 0x00002000     # Backing fs was unmounted.
	Q_OVERFLOW = 0x00004000  # Event queued overflowed.
	IGNORED = 0x00008000     # File was ignored.

	# Helper events.
	CLOSE = (CLOSE_WRITE | CLOSE_NOWRITE)  # Close.
	MOVE = (MOVED_FROM | MOVED_TO)         # Moves.

	# Special flags.
	ONLYDIR = 0x01000000      # Only watch the path if it is a directory.
	DONT_FOLLOW = 0x02000000  # Do not follow a sym link.
	EXCL_UNLINK = 0x04000000  # Exclude events on unlinked objects.
	MASK_ADD = 0x20000000     # Add to the mask of an already existing watch.
	ISDIR = 0x40000000        # Event occurred against dir.
	ONESHOT = 0x80000000      # Only send event once.

	# All events which a program can wait on.
	ALL_EVENTS = (
		ACCESS | MODIFY | ATTRIB | CLOSE_WRITE | CLOSE_NOWRITE | OPEN |
		MOVED_FROM | MOVED_TO | CREATE | DELETE | DELETE_SELF | MOVE_SELF
	)

	# See <bits/inotify.h>
	CLOEXEC = 0x80000
	NONBLOCK = 0x800

	def __init__(self, cloexec=True, nonblock=True):
		self._init1, self._add_watch, self._rm_watch, self._read = load_inotify()
		flags = 0
		if cloexec:
			flags |= self.CLOEXEC
		if nonblock:
			flags |= self.NONBLOCK
		self._inotify_fd = self._init1(flags)
		if self._inotify_fd == -1:
			raise INotifyError(os.strerror(ctypes.get_errno()))

		self._buf = ctypes.create_string_buffer(5000)
		self.fenc = get_preferred_file_name_encoding()
		self.hdr = struct.Struct(b'iIII')
		# We keep a reference to os to prevent it from being deleted
		# during interpreter shutdown, which would lead to errors in the
		# __del__ method
		self.os = os

	def handle_error(self):
		eno = ctypes.get_errno()
		extra = ''
		if eno == errno.ENOSPC:
			extra = 'You may need to increase the inotify limits on your system, via /proc/sys/fs/inotify/max_user_*'
		raise OSError(eno, self.os.strerror(eno) + str(extra))

	def __del__(self):
		# This method can be called during interpreter shutdown, which means we
		# must do the absolute minimum here. Note that there could be running
		# daemon threads that are trying to call other methods on this object.
		try:
			self.os.close(self._inotify_fd)
		except (AttributeError, TypeError):
			pass

	def close(self):
		if hasattr(self, '_inotify_fd'):
			self.os.close(self._inotify_fd)
			del self.os
			del self._add_watch
			del self._rm_watch
			del self._inotify_fd

	def read(self, get_name=True):
		buf = []
		while True:
			num = self._read(self._inotify_fd, self._buf, len(self._buf))
			if num == 0:
				break
			if num < 0:
				en = ctypes.get_errno()
				if en == errno.EAGAIN:
					break  # No more data
				if en == errno.EINTR:
					continue  # Interrupted, try again
				raise OSError(en, self.os.strerror(en))
			buf.append(self._buf.raw[:num])
		raw = b''.join(buf)
		pos = 0
		lraw = len(raw)
		while lraw - pos >= self.hdr.size:
			wd, mask, cookie, name_len = self.hdr.unpack_from(raw, pos)
			pos += self.hdr.size
			name = None
			if get_name:
				name = raw[pos:pos + name_len].rstrip(b'\0')
			pos += name_len
			self.process_event(wd, mask, cookie, name)

	def process_event(self, *args):
		raise NotImplementedError()