summary refs log tree commit diff stats
path: root/lib/packages/fsmonitor.nim
blob: b22e84f44635582d02a382892d8e7eac57e76c85 (plain) (blame)
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
#
#
#            Nim's Runtime Library
#        (c) Copyright 2012 Dominik Picheta
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

## This module allows you to monitor files or directories for changes using
## asyncio.
##
## **Warning**: This module will likely disappear soon and be moved into a
## new Nimble package.
##
## Windows support is not yet implemented.
##
## **Note:** This module uses ``inotify`` on Linux (Other Unixes are not yet
## supported). ``inotify`` was merged into the 2.6.13 Linux kernel, this
## module will therefore not work with any Linux kernel prior to that, unless
## it has been patched to support inotify.

when defined(linux) or defined(nimdoc):
  from posix import read
else:
  {.error: "Your platform is not supported.".}

import inotify, os, asyncio, tables

type
  FSMonitor* = ref FSMonitorObj
  FSMonitorObj = object of RootObj
    fd: cint
    handleEvent: proc (m: FSMonitor, ev: MonitorEvent) {.closure.}
    targets: Table[cint, string]

  MonitorEventType* = enum ## Monitor event type
    MonitorAccess,       ## File was accessed.
    MonitorAttrib,       ## Metadata changed.
    MonitorCloseWrite,   ## Writable file was closed.
    MonitorCloseNoWrite, ## Non-writable file closed.
    MonitorCreate,       ## Subfile was created.
    MonitorDelete,       ## Subfile was deleted.
    MonitorDeleteSelf,   ## Watched file/directory was itself deleted.
    MonitorModify,       ## File was modified.
    MonitorMoveSelf,     ## Self was moved.
    MonitorMoved,        ## File was moved.
    MonitorOpen,         ## File was opened.
    MonitorAll           ## Filter for all event types.

  MonitorEvent* = object
    case kind*: MonitorEventType  ## Type of the event.
    of MonitorMoveSelf, MonitorMoved:
      oldPath*: string          ## Old absolute location
      newPath*: string          ## New absolute location
    else:
      fullname*: string         ## Absolute filename of the file/directory affected.
    name*: string             ## Non absolute filepath of the file/directory
                              ## affected relative to the directory watched.
                              ## "" if this event refers to the file/directory
                              ## watched.
    wd*: cint                 ## Watch descriptor.

{.deprecated: [PFSMonitor: FSMonitor, TFSMonitor: FSMonitorObj,
  TMonitorEventType: MonitorEventType, TMonitorEvent: MonitorEvent].}

const
  MaxEvents = 100

proc newMonitor*(): FSMonitor =
  ## Creates a new file system monitor.
  new(result)
  result.targets = initTable[cint, string]()
  result.fd = inotifyInit()
  if result.fd < 0:
    raiseOSError(osLastError())

proc add*(monitor: FSMonitor, target: string,
               filters = {MonitorAll}): cint {.discardable.} =
  ## Adds ``target`` which may be a directory or a file to the list of
  ## watched paths of ``monitor``.
  ## You can specify the events to report using the ``filters`` parameter.

  var INFilter = 0
  for f in filters:
    case f
    of MonitorAccess: INFilter = INFilter or IN_ACCESS
    of MonitorAttrib: INFilter = INFilter or IN_ATTRIB
    of MonitorCloseWrite: INFilter = INFilter or IN_CLOSE_WRITE
    of MonitorCloseNoWrite: INFilter = INFilter or IN_CLOSE_NO_WRITE
    of MonitorCreate: INFilter = INFilter or IN_CREATE
    of MonitorDelete: INFilter = INFilter or IN_DELETE
    of MonitorDeleteSelf: INFilter = INFilter or IN_DELETE_SELF
    of MonitorModify: INFilter = INFilter or IN_MODIFY
    of MonitorMoveSelf: INFilter = INFilter or IN_MOVE_SELF
    of MonitorMoved: INFilter = INFilter or IN_MOVED_FROM or IN_MOVED_TO
    of MonitorOpen: INFilter = INFilter or IN_OPEN
    of MonitorAll: INFilter = INFilter or IN_ALL_EVENTS

  result = inotifyAddWatch(monitor.fd, target, INFilter.uint32)
  if result < 0:
    raiseOSError(osLastError())
  monitor.targets.add(result, target)

proc del*(monitor: FSMonitor, wd: cint) =
  ## Removes watched directory or file as specified by ``wd`` from ``monitor``.
  ##
  ## If ``wd`` is not a part of ``monitor`` an EOS error is raised.
  if inotifyRmWatch(monitor.fd, wd) < 0:
    raiseOSError(osLastError())

proc getEvent(m: FSMonitor, fd: cint): seq[MonitorEvent] =
  result = @[]
  let size = (sizeof(INotifyEvent)+2000)*MaxEvents
  var buffer = newString(size)

  let le = read(fd, addr(buffer[0]), size)

  var movedFrom = initTable[cint, tuple[wd: cint, old: string]]()

  var i = 0
  while i < le:
    var event = cast[ptr INotifyEvent](addr(buffer[i]))
    var mev: MonitorEvent
    mev.wd = event.wd
    if event.len.int != 0:
      let cstr = event.name.addr.cstring
      mev.name = $cstr
    else:
      mev.name = ""

    if (event.mask.int and IN_MOVED_FROM) != 0:
      # Moved from event, add to m's collection
      movedFrom.add(event.cookie.cint, (mev.wd, mev.name))
      inc(i, sizeof(INotifyEvent) + event.len.int)
      continue
    elif (event.mask.int and IN_MOVED_TO) != 0:
      mev.kind = MonitorMoved
      assert movedFrom.hasKey(event.cookie.cint)
      # Find the MovedFrom event.
      mev.oldPath = movedFrom[event.cookie.cint].old
      mev.newPath = "" # Set later
      # Delete it from the Table
      movedFrom.del(event.cookie.cint)
    elif (event.mask.int and IN_ACCESS) != 0: mev.kind = MonitorAccess
    elif (event.mask.int and IN_ATTRIB) != 0: mev.kind = MonitorAttrib
    elif (event.mask.int and IN_CLOSE_WRITE) != 0:
      mev.kind = MonitorCloseWrite
    elif (event.mask.int and IN_CLOSE_NOWRITE) != 0:
      mev.kind = MonitorCloseNoWrite
    elif (event.mask.int and IN_CREATE) != 0: mev.kind = MonitorCreate
    elif (event.mask.int and IN_DELETE) != 0:
      mev.kind = MonitorDelete
    elif (event.mask.int and IN_DELETE_SELF) != 0:
      mev.kind = MonitorDeleteSelf
    elif (event.mask.int and IN_MODIFY) != 0: mev.kind = MonitorModify
    elif (event.mask.int and IN_MOVE_SELF) != 0:
      mev.kind = MonitorMoveSelf
    elif (event.mask.int and IN_OPEN) != 0: mev.kind = MonitorOpen

    if mev.kind != MonitorMoved:
      mev.fullname = ""

    result.add(mev)
    inc(i, sizeof(INotifyEvent) + event.len.int)

  # If movedFrom events have not been matched with a moveTo. File has
  # been moved to an unwatched location, emit a MonitorDelete.
  for cookie, t in pairs(movedFrom):
    var mev: MonitorEvent
    mev.kind = MonitorDelete
    mev.wd = t.wd
    mev.name = t.old
    result.add(mev)

proc FSMonitorRead(h: RootRef) =
  var events = FSMonitor(h).getEvent(FSMonitor(h).fd)
  #var newEv: MonitorEvent
  for ev in events:
    var target = FSMonitor(h).targets[ev.wd]
    var newEv = ev
    if newEv.kind == MonitorMoved:
      newEv.oldPath = target / newEv.oldPath
      newEv.newPath = target / newEv.name
    else:
      newEv.fullName = target / newEv.name
    FSMonitor(h).handleEvent(FSMonitor(h), newEv)

proc toDelegate(m: FSMonitor): Delegate =
  result = newDelegate()
  result.deleVal = m
  result.fd = (type(result.fd))(m.fd)
  result.mode = fmRead
  result.handleRead = FSMonitorRead
  result.open = true

proc register*(d: Dispatcher, monitor: FSMonitor,
               handleEvent: proc (m: FSMonitor, ev: MonitorEvent) {.closure.}) =
  ## Registers ``monitor`` with dispatcher ``d``.
  monitor.handleEvent = handleEvent
  var deleg = toDelegate(monitor)
  d.register(deleg)

when not defined(testing) and isMainModule:
  proc main =
    var
      disp = newDispatcher()
      monitor = newMonitor()
      n = 0
    n = monitor.add("/tmp")
    assert n == 1
    n = monitor.add("/tmp", {MonitorAll})
    assert n == 1
    n = monitor.add("/tmp", {MonitorCloseWrite, MonitorCloseNoWrite})
    assert n == 1
    n = monitor.add("/tmp", {MonitorMoved, MonitorOpen, MonitorAccess})
    assert n == 1
    disp.register(monitor,
      proc (m: FSMonitor, ev: MonitorEvent) =
        echo("Got event: ", ev.kind)
        if ev.kind == MonitorMoved:
          echo("From ", ev.oldPath, " to ", ev.newPath)
          echo("Name is ", ev.name)
        else:
          echo("Name ", ev.name, " fullname ", ev.fullName))

    while true:
      if not disp.poll(): break
  main()