filewatch 0.2.5 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/bin/globtail +55 -0
- data/lib/filewatch/buftok.rb +2 -2
- data/lib/filewatch/tail.rb +171 -48
- data/lib/filewatch/watch.rb +145 -26
- data/test/globtail/Makefile +7 -0
- data/test/globtail/framework.sh +58 -0
- data/test/globtail/test1.data +5 -0
- data/test/globtail/test1.sh +17 -0
- data/test/globtail/test10.data +4 -0
- data/test/globtail/test10.sh +20 -0
- data/test/globtail/test2.data +2 -0
- data/test/globtail/test2.sh +17 -0
- data/test/globtail/test3.data +3 -0
- data/test/globtail/test3.sh +18 -0
- data/test/globtail/test4.data +4 -0
- data/test/globtail/test4.sh +16 -0
- data/test/globtail/test5.data +6 -0
- data/test/globtail/test5.sh +25 -0
- data/test/globtail/test6.data +6 -0
- data/test/globtail/test6.sh +29 -0
- data/test/globtail/test7.data +5 -0
- data/test/globtail/test7.sh +24 -0
- data/test/globtail/test8.data +5 -0
- data/test/globtail/test8.sh +23 -0
- data/test/globtail/test9.data +3 -0
- data/test/globtail/test9.sh +22 -0
- metadata +38 -38
- data/bin/gtail +0 -50
- data/lib/filewatch/exception.rb +0 -12
- data/lib/filewatch/inotify/emhandler.rb +0 -16
- data/lib/filewatch/inotify/event.rb +0 -101
- data/lib/filewatch/inotify/fd.rb +0 -319
- data/lib/filewatch/namespace.rb +0 -3
- data/lib/filewatch/rubyfixes.rb +0 -8
- data/lib/filewatch/stringpipeio.rb +0 -33
- data/lib/filewatch/tailglob.rb +0 -244
- data/lib/filewatch/watchglob.rb +0 -83
- data/test/log4j/LogTest.java +0 -21
- data/test/log4j/log4j.properties +0 -20
- data/test/logrotate/logrotate.conf +0 -5
data/bin/gtail
DELETED
@@ -1,50 +0,0 @@
|
|
1
|
-
#!/usr/bin/env ruby
|
2
|
-
require "rubygems"
|
3
|
-
require "filewatch/tailglob"
|
4
|
-
require "filewatch/buftok"
|
5
|
-
require "optparse"
|
6
|
-
|
7
|
-
def main(args)
|
8
|
-
with_filenames = true
|
9
|
-
exclude_patterns = []
|
10
|
-
|
11
|
-
opts = OptionParser.new do |opts|
|
12
|
-
opts.banner = "Usage: #{$0} [options] <path_or_glob> [path_or_glob2] [...]"
|
13
|
-
|
14
|
-
opts.on("-n", "--no-filename",
|
15
|
-
"Supress prefixing of output with file names") do |x|
|
16
|
-
with_filenames = false
|
17
|
-
end # -n
|
18
|
-
|
19
|
-
opts.on("-x EXCLUDE", "--exclude EXCLUDE",
|
20
|
-
"A pattern to ignore. Wildcard/globs accepted." \
|
21
|
-
" Can be specified multiple times") do |pattern|
|
22
|
-
exclude_patterns << pattern
|
23
|
-
end
|
24
|
-
end # OptionParser
|
25
|
-
|
26
|
-
opts.parse!(args)
|
27
|
-
|
28
|
-
if args.length == 0
|
29
|
-
puts opts.banner
|
30
|
-
return 1
|
31
|
-
end
|
32
|
-
|
33
|
-
tail = FileWatch::TailGlob.new
|
34
|
-
ARGV.each do |path|
|
35
|
-
tail.tail(path, :exclude => exclude_patterns)
|
36
|
-
end
|
37
|
-
|
38
|
-
buffer = BufferedTokenizer.new
|
39
|
-
tail.subscribe do |path, data|
|
40
|
-
buffer.extract(data).each do |line|
|
41
|
-
if with_filenames
|
42
|
-
puts "#{path}: #{line}"
|
43
|
-
else
|
44
|
-
puts line
|
45
|
-
end
|
46
|
-
end # buffer.extract
|
47
|
-
end # tail.subscribe
|
48
|
-
end # def main
|
49
|
-
|
50
|
-
exit(main(ARGV))
|
data/lib/filewatch/exception.rb
DELETED
@@ -1,16 +0,0 @@
|
|
1
|
-
require "filewatch/inotify/fd"
|
2
|
-
require "filewatch/namespace"
|
3
|
-
|
4
|
-
class FileWatch::Inotify::EMHandler < EventMachine::Connection
|
5
|
-
def initialize(inotify_fd, callback=nil)
|
6
|
-
@inotify = inotify_fd
|
7
|
-
@callback = callback
|
8
|
-
self.notify_readable = true
|
9
|
-
end
|
10
|
-
|
11
|
-
def notify_readable
|
12
|
-
@inotify.each do |event|
|
13
|
-
@callback.call(event)
|
14
|
-
end
|
15
|
-
end
|
16
|
-
end
|
@@ -1,101 +0,0 @@
|
|
1
|
-
require "ffi"
|
2
|
-
require "filewatch/inotify/fd"
|
3
|
-
require "filewatch/namespace"
|
4
|
-
require "filewatch/rubyfixes"
|
5
|
-
|
6
|
-
class FileWatch::Inotify::Event < FFI::Struct
|
7
|
-
layout :wd, :int,
|
8
|
-
:mask, :uint32,
|
9
|
-
:cookie, :uint32,
|
10
|
-
:len, :uint32
|
11
|
-
# last piece is the name, but don't hold it in the struct
|
12
|
-
#:name, :string,
|
13
|
-
|
14
|
-
# helper accessors
|
15
|
-
def wd; return self[:wd]; end
|
16
|
-
def mask; return self[:mask]; end
|
17
|
-
def cookie; return self[:cookie]; end
|
18
|
-
def len; return self[:len]; end
|
19
|
-
|
20
|
-
attr_accessor :name
|
21
|
-
|
22
|
-
# This attribute is only set for file renames
|
23
|
-
attr_accessor :old_name
|
24
|
-
|
25
|
-
# Enum of :directory or :file
|
26
|
-
attr_accessor :type
|
27
|
-
|
28
|
-
def initialize(pointer)
|
29
|
-
if pointer.is_a?(String)
|
30
|
-
pointer = FFI::MemoryPointer.from_string(pointer)
|
31
|
-
end
|
32
|
-
|
33
|
-
super(pointer)
|
34
|
-
end
|
35
|
-
|
36
|
-
def self.from_stringpipeio(io)
|
37
|
-
# I implemented a 'string pipe' IO because normal "buffered" IO reads
|
38
|
-
# on inotify fds fails in ruby 1.9.2 because it literally calls read(2)
|
39
|
-
# with 'self.size' as the byte size to read. This causes EINVAL from
|
40
|
-
# inotify, documented thusly in inotify(7):
|
41
|
-
#
|
42
|
-
# """ The behavior when the buffer given to read(2) is too small to
|
43
|
-
# return information about the next event depends on the kernel
|
44
|
-
# version: in kernels before 2.6.21, read(2) returns 0; since
|
45
|
-
# kernel 2.6.21, read(2) fails with the error EINVAL. """
|
46
|
-
#
|
47
|
-
# Working around this requires implementing our own read buffering
|
48
|
-
# unless comeone clues me in on how to make ruby 1.9.2 read larger
|
49
|
-
# blocks and actually do the nice buffered IO we've all come to
|
50
|
-
# know and love.
|
51
|
-
|
52
|
-
begin
|
53
|
-
data = io.read(self.size, true)
|
54
|
-
rescue Errno::EINVAL => e
|
55
|
-
$stderr.puts "Read was too small? Confused."
|
56
|
-
raise e
|
57
|
-
end
|
58
|
-
|
59
|
-
return nil if data == nil
|
60
|
-
|
61
|
-
pointer = FFI::MemoryPointer.from_string(data)
|
62
|
-
event = self.new(pointer)
|
63
|
-
|
64
|
-
event.from_stringpipeio(io)
|
65
|
-
return event
|
66
|
-
end
|
67
|
-
|
68
|
-
def from_stringpipeio(io)
|
69
|
-
begin
|
70
|
-
if self[:len] > 0
|
71
|
-
@name = io.read(self[:len], true)
|
72
|
-
else
|
73
|
-
@name = nil
|
74
|
-
end
|
75
|
-
rescue Errno::EINVAL => e
|
76
|
-
$stderr.puts "Read was too small? Confused."
|
77
|
-
raise e
|
78
|
-
end
|
79
|
-
return self if @name == nil
|
80
|
-
|
81
|
-
@name = @name.split("\0", 2).first
|
82
|
-
|
83
|
-
return self
|
84
|
-
end
|
85
|
-
|
86
|
-
def actions
|
87
|
-
# TODO(sissel): Skip these?
|
88
|
-
::FileWatch::Inotify::FD::WATCH_BITS.reject do |key, bitmask|
|
89
|
-
#p key => [self.mask, bitmask] if self.mask & bitmask != 0
|
90
|
-
self.mask & bitmask == 0
|
91
|
-
end.keys
|
92
|
-
end
|
93
|
-
|
94
|
-
def to_s
|
95
|
-
return "#{@name} (#{self.actions.join(", ")}) [type=#{type}]"
|
96
|
-
end
|
97
|
-
|
98
|
-
def partial?
|
99
|
-
return self[:len] > 0 && @name == nil
|
100
|
-
end # def partial?
|
101
|
-
end
|
data/lib/filewatch/inotify/fd.rb
DELETED
@@ -1,319 +0,0 @@
|
|
1
|
-
require "rubygems"
|
2
|
-
require "ffi"
|
3
|
-
require "fcntl"
|
4
|
-
require "filewatch/exception"
|
5
|
-
require "filewatch/inotify/event"
|
6
|
-
require "filewatch/namespace"
|
7
|
-
require "filewatch/rubyfixes"
|
8
|
-
require "filewatch/stringpipeio"
|
9
|
-
|
10
|
-
class FileWatch::Inotify::FD
|
11
|
-
include Enumerable
|
12
|
-
|
13
|
-
module CInotify
|
14
|
-
extend FFI::Library
|
15
|
-
ffi_lib FFI::Library::LIBC
|
16
|
-
|
17
|
-
attach_function :inotify_init, [], :int
|
18
|
-
attach_function :fcntl, [:int, :int, :long], :int
|
19
|
-
attach_function :inotify_add_watch, [:int, :string, :uint32], :int
|
20
|
-
|
21
|
-
# So we can read and poll inotify from jruby.
|
22
|
-
attach_function :read, [:int, :pointer, :size_t], :int
|
23
|
-
|
24
|
-
# Poll is pretty crappy, but it's better than nothing.
|
25
|
-
attach_function :poll, [:pointer, :int, :int], :int
|
26
|
-
end
|
27
|
-
|
28
|
-
INOTIFY_CLOEXEC = 02000000
|
29
|
-
INOTIFY_NONBLOCK = 04000
|
30
|
-
|
31
|
-
F_SETFL = 4
|
32
|
-
O_NONBLOCK = 04000
|
33
|
-
|
34
|
-
WATCH_BITS = {
|
35
|
-
:access => 1 << 0,
|
36
|
-
:modify => 1 << 1,
|
37
|
-
:attrib => 1 << 2,
|
38
|
-
:close_write => 1 << 3,
|
39
|
-
:close_nowrite => 1 << 4,
|
40
|
-
:open => 1 << 5,
|
41
|
-
:moved_from => 1 << 6,
|
42
|
-
:moved_to => 1 << 7,
|
43
|
-
:create => 1 << 8,
|
44
|
-
:delete => 1 << 9,
|
45
|
-
:delete_self => 1 << 10,
|
46
|
-
:move_self => 1 << 11,
|
47
|
-
}
|
48
|
-
|
49
|
-
# Shortcuts
|
50
|
-
WATCH_BITS[:close] = WATCH_BITS[:close_write] | WATCH_BITS[:close_nowrite]
|
51
|
-
WATCH_BITS[:move] = WATCH_BITS[:moved_from] \
|
52
|
-
| WATCH_BITS[:moved_to] | WATCH_BITS[:move_self]
|
53
|
-
WATCH_BITS[:delete] = WATCH_BITS[:delete] | WATCH_BITS[:delete_self]
|
54
|
-
|
55
|
-
attr_reader :fd
|
56
|
-
|
57
|
-
public
|
58
|
-
def self.can_watch?(filestat)
|
59
|
-
# TODO(sissel): implement.
|
60
|
-
return true
|
61
|
-
end # def self.can_watch?
|
62
|
-
|
63
|
-
# Create a new FileWatch::Inotify::FD instance.
|
64
|
-
# This is the main interface you want to use for watching
|
65
|
-
# files, directories, etc.
|
66
|
-
public
|
67
|
-
def initialize
|
68
|
-
@watches = {}
|
69
|
-
@buffer = FileWatch::StringPipeIO.new
|
70
|
-
|
71
|
-
# Can't use inotify_init1 since older kernels don't have it.
|
72
|
-
# Implement nonblock ourselves.
|
73
|
-
@fd = CInotify.inotify_init()
|
74
|
-
|
75
|
-
# Track movement cookies # since 'moved_from' and 'moved_to' are separate
|
76
|
-
# events.
|
77
|
-
@movement = {}
|
78
|
-
|
79
|
-
if java?
|
80
|
-
@io = nil
|
81
|
-
@rc = CInotify.fcntl(@fd, F_SETFL, O_NONBLOCK)
|
82
|
-
if @rc == -1
|
83
|
-
raise FileWatch::Exception.new(
|
84
|
-
"fcntl(#{@fd}, F_SETFL, O_NONBLOCK) failed. #{$?}", @fd, nil)
|
85
|
-
end
|
86
|
-
else
|
87
|
-
@io = IO.for_fd(@fd)
|
88
|
-
@io.fcntl(Fcntl::F_SETFL, Fcntl::O_NONBLOCK)
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
# Are we using java?
|
93
|
-
private
|
94
|
-
def java?
|
95
|
-
return RUBY_PLATFORM == "java"
|
96
|
-
end
|
97
|
-
|
98
|
-
# Add a watch.
|
99
|
-
# - path is a string file path
|
100
|
-
# - what_to_watch is any of the valid WATCH_BITS keys
|
101
|
-
#
|
102
|
-
# Possible values for what_to_watch:
|
103
|
-
# (See also the inotify(7) manpage under 'inotify events'
|
104
|
-
#
|
105
|
-
# :access - file was accesssed (a read)
|
106
|
-
# :attrib - permissions, timestamps, link count, owner, etc changed
|
107
|
-
# :close_nowrite - a file not opened for writing was closed
|
108
|
-
# :close_write - a file opened for writing was closed
|
109
|
-
# :create - file/directory was created, only valid on watched directories
|
110
|
-
# :delete - file/directory was deleted, only valid on watched directories
|
111
|
-
# :delete_self - a watched file or directory was deleted
|
112
|
-
# :modify - A watched file was modified
|
113
|
-
# :moved_from - A file was moved out of a watched directory
|
114
|
-
# :moved_to - A file was moved into a watched directory
|
115
|
-
# :move_self - A watched file/directory was moved
|
116
|
-
# :open - A file was opened
|
117
|
-
# # Shortcuts
|
118
|
-
# :close - close_nowrite or close_write
|
119
|
-
# :move - move_self or moved_from or moved_to
|
120
|
-
# :delete - delete or delete_self
|
121
|
-
#
|
122
|
-
# (Some of the above data copied from the inotify(7) manpage, which comes from
|
123
|
-
# the linux man-pages project. http://www.kernel.org/doc/man-pages/ )
|
124
|
-
#
|
125
|
-
# Example:
|
126
|
-
# fd = FileWatch::Inotify::FD.new
|
127
|
-
# fd.watch("/tmp", :create, :delete)
|
128
|
-
# fd.watch("/var/log/messages", :modify)
|
129
|
-
public
|
130
|
-
def watch(path, *what_to_watch)
|
131
|
-
mask = what_to_watch.uniq.inject(0) { |m, val| m |= WATCH_BITS[val] }
|
132
|
-
watch_descriptor = CInotify.inotify_add_watch(@fd, path, mask)
|
133
|
-
|
134
|
-
if watch_descriptor == -1
|
135
|
-
raise FileWatch::Exception.new(
|
136
|
-
"inotify_add_watch(#{@fd}, #{path}, #{mask}) failed. #{$?}", @fd, path)
|
137
|
-
end
|
138
|
-
|
139
|
-
@watches[watch_descriptor] = {
|
140
|
-
:path => path,
|
141
|
-
:partial => nil,
|
142
|
-
:is_directory => File.directory?(path),
|
143
|
-
}
|
144
|
-
return watch_descriptor
|
145
|
-
end # def watch
|
146
|
-
|
147
|
-
# TODO(sissel): Implement inotify_rm_watch as 'cancel'
|
148
|
-
|
149
|
-
private
|
150
|
-
def normal_read(timeout=nil)
|
151
|
-
loop do
|
152
|
-
begin
|
153
|
-
data = @io.sysread(4096)
|
154
|
-
@buffer.write(data)
|
155
|
-
rescue Errno::EAGAIN
|
156
|
-
# No data left to read, moveon.
|
157
|
-
break
|
158
|
-
end
|
159
|
-
end
|
160
|
-
return nil
|
161
|
-
end # def normal_read
|
162
|
-
|
163
|
-
private
|
164
|
-
def jruby_read(timeout=nil)
|
165
|
-
# TODO(sissel): instantiate this once to prevent extra memory usage? safe?
|
166
|
-
@jruby_read_buffer = FFI::MemoryPointer.new(:char, 4096)
|
167
|
-
|
168
|
-
# TODO(sissel): Block with select.
|
169
|
-
# Will have to use FFI to call select, too.
|
170
|
-
|
171
|
-
# We have to call libc's read(2) because JRuby/Java can't trivially
|
172
|
-
# be told about existing file descriptors.
|
173
|
-
loop do
|
174
|
-
bytes = CInotify.read(@fd, @jruby_read_buffer, 4096)
|
175
|
-
|
176
|
-
# read(2) returns -1 on error, which we expect to be EAGAIN, but...
|
177
|
-
# TODO(sissel): maybe we should check errno properly... Then again,
|
178
|
-
# errno is supposedly thread-safe, but I believe that is in reference to
|
179
|
-
# pthreads. Ruby's green threads, fibers, etc, are not likely subject to
|
180
|
-
# the same safeties, so we have to assume errno is effed.
|
181
|
-
# This code is run in jruby only, so maybe we can guarantee that
|
182
|
-
# a thread is a pthread and has errno safety. Which leads us to the next
|
183
|
-
# question - how to access errno from jruby/ffi?
|
184
|
-
break if bytes == -1
|
185
|
-
|
186
|
-
@buffer.write(@jruby_read_buffer.get_bytes(0, bytes))
|
187
|
-
end
|
188
|
-
return nil
|
189
|
-
end # def jruby_read
|
190
|
-
|
191
|
-
# Make any necessary corrections to the event
|
192
|
-
private
|
193
|
-
def prepare(event)
|
194
|
-
watch = @watches[event[:wd]]
|
195
|
-
|
196
|
-
if event.name == nil
|
197
|
-
# Some events don't have the name at all, so add what we know.
|
198
|
-
# This usually occurs on events for files where inotify expects us to
|
199
|
-
# track the file name anyway.
|
200
|
-
event.name = watch[:path]
|
201
|
-
event.type = :file
|
202
|
-
else
|
203
|
-
# In cases where we have event names, they are always(?) directory events
|
204
|
-
# and contain the name of the file changed. Event paths are relative to
|
205
|
-
# the watch, if a directory. Prefix to make the full path.
|
206
|
-
if watch[:is_directory]
|
207
|
-
event.name = File.join(watch[:path], event.name)
|
208
|
-
event.type = :directory
|
209
|
-
end
|
210
|
-
end
|
211
|
-
|
212
|
-
return event
|
213
|
-
end # def prepare
|
214
|
-
|
215
|
-
# Get one inotify event.
|
216
|
-
#
|
217
|
-
# TIMEOUT NOT SUPPORTED YET;
|
218
|
-
#
|
219
|
-
# If timeout is not given, this call blocks.
|
220
|
-
# If a timeout occurs and no event was read, nil is returned.
|
221
|
-
#
|
222
|
-
# Returns nil on timeout or an FileWatch::Inotify::Event on success.
|
223
|
-
private
|
224
|
-
def get(timeout_not_supported_yet=nil)
|
225
|
-
# This big 'loop' is to support pop { |event| ... } shipping each available event.
|
226
|
-
# It's not very rubyish (we should probably use Enumerable and such.
|
227
|
-
if java?
|
228
|
-
#jruby_read(timeout)
|
229
|
-
jruby_read
|
230
|
-
else
|
231
|
-
#normal_read(timeout)
|
232
|
-
normal_read
|
233
|
-
end
|
234
|
-
|
235
|
-
# Recover any previous partial event.
|
236
|
-
if @partial
|
237
|
-
event = @partial.from_stringpipeio(@buffer)
|
238
|
-
else
|
239
|
-
event = FileWatch::Inotify::Event.from_stringpipeio(@buffer)
|
240
|
-
return nil if event == nil
|
241
|
-
end
|
242
|
-
|
243
|
-
if event.partial?
|
244
|
-
@partial = event
|
245
|
-
return nil
|
246
|
-
end
|
247
|
-
@partial = nil
|
248
|
-
|
249
|
-
return prepare(event)
|
250
|
-
end # def get
|
251
|
-
|
252
|
-
# For Enumerable support
|
253
|
-
#
|
254
|
-
# Yields one FileWatch::Inotify::Event per iteration. If there are no more events
|
255
|
-
# at the this time, then this method will end.
|
256
|
-
public
|
257
|
-
def each(&block)
|
258
|
-
loop do
|
259
|
-
event = get
|
260
|
-
break if event == nil
|
261
|
-
|
262
|
-
# inotify claims to guarantee order of events, so we should always see
|
263
|
-
# 'moved_from' events before 'moved_to'
|
264
|
-
if event.actions.include?(:moved_from)
|
265
|
-
@movement[event.cookie] = event.name
|
266
|
-
end
|
267
|
-
|
268
|
-
if event.actions.include?(:moved_to) and @movement.include?(event.cookie)
|
269
|
-
event.old_name = @movement[event.cookie]
|
270
|
-
@movement.delete(event.cookie)
|
271
|
-
|
272
|
-
# If we are watching this file, update the path with the new filename
|
273
|
-
@watches.each do |wd, watch|
|
274
|
-
if watch[:path] == event.old_name
|
275
|
-
watch[:old_path] = watch[:path]
|
276
|
-
watch[:path] = event.name
|
277
|
-
end
|
278
|
-
end # @watches.each
|
279
|
-
end # if event.actions.include? :moved_to
|
280
|
-
|
281
|
-
# We're dong mangling the event. Ship it.
|
282
|
-
yield event
|
283
|
-
end # loop
|
284
|
-
end # def each
|
285
|
-
|
286
|
-
# Subscribe to inotify events for this instance.
|
287
|
-
#
|
288
|
-
# If you are running in EventMachine, this will set up a
|
289
|
-
# subscription that behaves sanely in EventMachine
|
290
|
-
#
|
291
|
-
# If you are not running in EventMachine, this method
|
292
|
-
# blocks forever, invoking the given block for each event.
|
293
|
-
# Further, if you are not using EventMachine, you should
|
294
|
-
# not pass a handler, only a block, like this:
|
295
|
-
#
|
296
|
-
# fd.subscribe do |event|
|
297
|
-
# puts event
|
298
|
-
# end
|
299
|
-
public
|
300
|
-
def subscribe(handler=nil, &block)
|
301
|
-
if defined?(EventMachine) && EventMachine.reactor_running?
|
302
|
-
require "filewatch/inotify/emhandler"
|
303
|
-
handler = FileWatch::Inotify::EMHandler if handler == nil
|
304
|
-
EventMachine::watch(@fd, handler, self, block)
|
305
|
-
else
|
306
|
-
loop do
|
307
|
-
if java?
|
308
|
-
# No way to select on FFI-derived file descriptors yet,
|
309
|
-
# when I grab poll(2) via FFI, this sleep will become
|
310
|
-
# a poll or sleep invocation.
|
311
|
-
sleep(1)
|
312
|
-
else
|
313
|
-
IO.select([@io], nil, nil, nil)
|
314
|
-
end
|
315
|
-
each(&block)
|
316
|
-
end
|
317
|
-
end
|
318
|
-
end
|
319
|
-
end # class FileWatch::Inotify::FD
|