summary refs log tree commit diff stats
path: root/lib/pure/fsmonitor.nim
blob: bf4aef61c98940c6dadaea768fc06d9b1123c34f (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
#
#
#            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.
##
## 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: TTable[cint, string]
  
  MonitorEventType* = enum ## Monitor event type
    MonitorAccess,       ## File was accessed.
    MonitorAttrib,       ## Metadata changed.
    MonitorCloseWrite,   ## Writtable file was closed.
    MonitorCloseNoWrite, ## Unwrittable 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*(): PFSMonitor =
  ## 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: PFSMonitor, 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 = -1
  for f in filters:
    case f
    of MonitorAccess: INFilter = INFilter and IN_ACCESS
    of MonitorAttrib: INFilter = INFilter and IN_ATTRIB
    of MonitorCloseWrite: INFilter = INFilter and IN_CLOSE_WRITE
    of MonitorCloseNoWrite: INFilter = INFilter and IN_CLOSE_NO_WRITE
    of MonitorCreate: INFilter = INFilter and IN_CREATE
    of MonitorDelete: INFilter = INFilter and IN_DELETE
    of MonitorDeleteSelf: INFilter = INFilter and IN_DELETE_SELF
    of MonitorModify: INFilter = INFilter and IN_MODIFY
    of MonitorMoveSelf: INFilter = INFilter and IN_MOVE_SELF
    of MonitorMoved: INFilter = INFilter and IN_MOVED_FROM and IN_MOVED_TO
    of MonitorOpen: INFilter = INFilter and IN_OPEN
    of MonitorAll: INFilter = INFilter and IN_ALL_EVENTS
  
  result = inotifyAddWatch(monitor.fd, target, INFilter.uint32)
  if result < 0:
    raiseOSError(osLastError())
  monitor.targets.add(result, target)

proc del*(monitor: PFSMonitor, 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: PFSMonitor, fd: cint): seq[TMonitorEvent] =
  result = @[]
  let size = (sizeof(TINotifyEvent)+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 TINotifyEvent](addr(buffer[i]))
    var mev: TMonitorEvent
    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(TINotifyEvent) + 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 TTable
      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(TINotifyEvent) + 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: TMonitorEvent
    mev.kind = MonitorDelete
    mev.wd = t.wd
    mev.name = t.old
    result.add(mev)

proc FSMonitorRead(h: PObject) =
  var events = PFSMonitor(h).getEvent(PFSMonitor(h).fd)
  #var newEv: TMonitorEvent
  for ev in events:
    var target = PFSMonitor(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
    PFSMonitor(h).handleEvent(PFSMonitor(h), newEv)

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

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

when isMainModule:
  proc main =
    var disp = newDispatcher()
    var monitor = newMonitor()
    echo monitor.add("/home/dom/inotifytests/")
    disp.register(monitor,
      proc (m: PFSMonitor, ev: TMonitorEvent) =
        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()