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

import errno
import os
import ctypes

from threading import RLock

from powerline.lib.inotify import INotify
from powerline.lib.monotonic import monotonic
from powerline.lib.path import realpath


class INotifyFileWatcher(INotify):
	def __init__(self, expire_time=10):
		super(INotifyFileWatcher, self).__init__()
		self.watches = {}
		self.modified = {}
		self.last_query = {}
		self.lock = RLock()
		self.expire_time = expire_time * 60

	def expire_watches(self):
		now = monotonic()
		for path, last_query in tuple(self.last_query.items()):
			if last_query - now > self.expire_time:
				self.unwatch(path)

	def process_event(self, wd, mask, cookie, name):
		if wd == -1 and (mask & self.Q_OVERFLOW):
			# We missed some INOTIFY events, so we don't
			# know the state of any tracked files.
			for path in tuple(self.modified):
				if os.path.exists(path):
					self.modified[path] = True
				else:
					self.watches.pop(path, None)
					self.modified.pop(path, None)
					self.last_query.pop(path, None)
			return

		for path, num in tuple(self.watches.items()):
			if num == wd:
				if mask & self.IGNORED:
					self.watches.pop(path, None)
					self.modified.pop(path, None)
					self.last_query.pop(path, None)
				else:
					if mask & self.ATTRIB:
						# The watched file could have had its inode changed, in
						# which case we will not get any more events for this
						# file, so re-register the watch. For example by some
						# other file being renamed as this file.
						try:
							self.unwatch(path)
						except OSError:
							pass
						try:
							self.watch(path)
						except OSError as e:
							if getattr(e, 'errno', None) != errno.ENOENT:
								raise
						else:
							self.modified[path] = True
					else:
						self.modified[path] = True

	def unwatch(self, path):
		''' Remove the watch for path. Raises an OSError if removing the watch
		fails for some reason. '''
		path = realpath(path)
		with self.lock:
			self.modified.pop(path, None)
			self.last_query.pop(path, None)
			wd = self.watches.pop(path, None)
			if wd is not None:
				if self._rm_watch(self._inotify_fd, wd) != 0:
					self.handle_error()

	def watch(self, path):
		''' Register a watch for the file/directory named path. Raises an OSError if path
		does not exist. '''
		path = realpath(path)
		with self.lock:
			if path not in self.watches:
				bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
				flags = self.MOVE_SELF | self.DELETE_SELF
				buf = ctypes.c_char_p(bpath)
				# Try watching path as a directory
				wd = self._add_watch(self._inotify_fd, buf, flags | self.ONLYDIR)
				if wd == -1:
					eno = ctypes.get_errno()
					if eno != errno.ENOTDIR:
						self.handle_error()
					# Try watching path as a file
					flags |= (self.MODIFY | self.ATTRIB)
					wd = self._add_watch(self._inotify_fd, buf, flags)
					if wd == -1:
						self.handle_error()
				self.watches[path] = wd
				self.modified[path] = False

	def is_watching(self, path):
		with self.lock:
			return realpath(path) in self.watches

	def __call__(self, path):
		''' Return True if path has been modified since the last call. Can
		raise OSError if the path does not exist. '''
		path = realpath(path)
		with self.lock:
			self.last_query[path] = monotonic()
			self.expire_watches()
			if path not in self.watches:
				# Try to re-add the watch, it will fail if the file does not
				# exist/you don't have permission
				self.watch(path)
				return True
			self.read(get_name=False)
			if path not in self.modified:
				# An ignored event was received which means the path has been
				# automatically unwatched
				return True
			ans = self.modified[path]
			if ans:
				self.modified[path] = False
			return ans

	def close(self):
		with self.lock:
			for path in tuple(self.watches):
				try:
					self.unwatch(path)
				except OSError:
					pass
			super(INotifyFileWatcher, self).close()


class NoSuchDir(ValueError):
	pass


class BaseDirChanged(ValueError):
	pass


class DirTooLarge(ValueError):
	def __init__(self, bdir):
		ValueError.__init__(self, 'The directory {0} is too large to monitor. Try increasing the value in /proc/sys/fs/inotify/max_user_watches'.format(bdir))


class INotifyTreeWatcher(INotify):
	is_dummy = False

	def __init__(self, basedir, ignore_event=None):
		super(INotifyTreeWatcher, self).__init__()
		self.basedir = realpath(basedir)
		self.watch_tree()
		self.modified = True
		self.ignore_event = (lambda path, name: False) if ignore_event is None else ignore_event

	def watch_tree(self):
		self.watched_dirs = {}
		self.watched_rmap = {}
		try:
			self.add_watches(self.basedir)
		except OSError as e:
			if e.errno == errno.ENOSPC:
				raise DirTooLarge(self.basedir)

	def add_watches(self, base, top_level=True):
		''' Add watches for this directory and all its descendant directories,
		recursively. '''
		base = realpath(base)
		# There may exist a link which leads to an endless
		# add_watches loop or to maximum recursion depth exceeded
		if not top_level and base in self.watched_dirs:
			return
		try:
			is_dir = self.add_watch(base)
		except OSError as e:
			if e.errno == errno.ENOENT:
				# The entry could have been deleted between listdir() and
				# add_watch().
				if top_level:
					raise NoSuchDir('The dir {0} does not exist'.format(base))
				return
			if e.errno == errno.EACCES:
				# We silently ignore entries for which we don't have permission,
				# unless they are the top level dir
				if top_level:
					raise NoSuchDir('You do not have permission to monitor {0}'.format(base))
				return
			raise
		else:
			if is_dir:
				try:
					files = os.listdir(base)
				except OSError as e:
					if e.errno in (errno.ENOTDIR, errno.ENOENT):
						# The dir was deleted/replaced between the add_watch()
						# and listdir()
						if top_level:
							raise NoSuchDir('The dir {0} does not exist'.format(base))
						return
					raise
				for x in files:
					self.add_watches(os.path.join(base, x), top_level=False)
			elif top_level:
				# The top level dir is a file, not good.
				raise NoSuchDir('The dir {0} does not exist'.format(base))

	def add_watch(self, path):
		bpath = path if isinstance(path, bytes) else path.encode(self.fenc)
		wd = self._add_watch(
			self._inotify_fd,
			ctypes.c_char_p(bpath),

			# Ignore symlinks and watch only directories
			self.DONT_FOLLOW | self.ONLYDIR |

			self.MODIFY | self.CREATE | self.DELETE |
			self.MOVE_SELF | self.MOVED_FROM | self.MOVED_TO |
			self.ATTRIB | self.DELETE_SELF
		)
		if wd == -1:
			eno = ctypes.get_errno()
			if eno == errno.ENOTDIR:
				return False
			raise OSError(eno, 'Failed to add watch for: {0}: {1}'.format(path, self.os.strerror(eno)))
		self.watched_dirs[path] = wd
		self.watched_rmap[wd] = path
		return True

	def process_event(self, wd, mask, cookie, name):
		if wd == -1 and (mask & self.Q_OVERFLOW):
			# We missed some INOTIFY events, so we don't
			# know the state of any tracked dirs.
			self.watch_tree()
			self.modified = True
			return
		path = self.watched_rmap.get(wd, None)
		if path is not None:
			if not self.ignore_event(path, name):
				self.modified = True
			if mask & self.CREATE:
				# A new sub-directory might have been created, monitor it.
				try:
					if not isinstance(path, bytes):
						name = name.decode(self.fenc)
					self.add_watch(os.path.join(path, name))
				except OSError as e:
					if e.errno == errno.ENOENT:
						# Deleted before add_watch()
						pass
					elif e.errno == errno.ENOSPC:
						raise DirTooLarge(self.basedir)
					else:
						raise
			if (mask & self.DELETE_SELF or mask & self.MOVE_SELF) and path == self.basedir:
				raise BaseDirChanged('The directory %s was moved/deleted' % path)

	def __call__(self):
		self.read()
		ret = self.modified
		self.modified = False
		return ret