summaryrefslogtreecommitdiff
path: root/vendor/bundle/ruby/3.4.0/gems/rb-fsevent-0.11.2/lib/rb-fsevent/fsevent.rb
blob: fac2364819c8d0f155c50823748a37742d3db7a9 (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
# -*- encoding: utf-8 -*-

require 'otnetstring'

class FSEvent
  class << self
    class_eval <<-END
      def root_path
        "#{File.expand_path(File.join(File.dirname(__FILE__), '..', '..'))}"
      end
    END
    class_eval <<-END
      def watcher_path
        "#{File.join(FSEvent.root_path, 'bin', 'fsevent_watch')}"
      end
    END
  end

  attr_reader :paths, :callback

  def initialize args = nil, &block
    watch(args, &block) unless args.nil?
  end

  def watch(watch_paths, options=nil, &block)
    @paths      = watch_paths.kind_of?(Array) ? watch_paths : [watch_paths]
    @callback   = block

    if options.kind_of?(Hash)
      @options  = parse_options(options)
    elsif options.kind_of?(Array)
      @options  = options
    else
      @options  = []
    end
  end

  def run
    @pipe    = open_pipe
    @running = true

    # please note the use of IO::select() here, as it is used specifically to
    # preserve correct signal handling behavior in ruby 1.8.
    while @running && IO::select([@pipe], nil, nil, nil)
      # managing the IO ourselves allows us to be careful and never pass an
      # incomplete message to OTNetstring.parse()
      message = String.new
      length = String.new
      byte = nil

      reading_length = true
      found_length = false

      while reading_length
        byte = @pipe.read_nonblock(1)
        if "#{byte}" =~ /\d/
          length << byte
          found_length = true
        elsif found_length == false
          next
        else
          reading_length = false
        end
      end
      length = Integer(length, 10)
      type = byte

      message << "#{length}#{type}"
      message << @pipe.read(length)

      decoded = OTNetstring.parse(message)
      modified_paths = decoded["events"].map {|event| event["path"]}
      # passing the full info as a second block param feels icky, but such is
      # the trap of backward compatibility.
      case callback.arity
        when 1
          callback.call(modified_paths)
        when 2
          callback.call(modified_paths, decoded)
      end
    end
  rescue Interrupt, IOError, Errno::EBADF
  ensure
    stop
  end

  def stop
    unless @pipe.nil?
      Process.kill('KILL', @pipe.pid) if process_running?(@pipe.pid)
      @pipe.close
    end
  rescue IOError, Errno::EBADF
  ensure
    @running = false
  end

  def process_running?(pid)
    begin
      Process.kill(0, pid)
      true
    rescue Errno::ESRCH
      false
    end
  end

  if RUBY_VERSION < '1.9'
    def open_pipe
      IO.popen("'#{self.class.watcher_path}' #{options_string} #{shellescaped_paths}")
    end

    private

    def options_string
      @options.join(' ')
    end

    def shellescaped_paths
      @paths.map {|path| shellescape(path)}.join(' ')
    end

    # for Ruby 1.8.6  support
    def shellescape(str)
      # An empty argument will be skipped, so return empty quotes.
      return "''" if str.empty?

      str = str.dup

      # Process as a single byte sequence because not all shell
      # implementations are multibyte aware.
      str.gsub!(/([^A-Za-z0-9_\-.,:\/@\n])/n, "\\\\\\1")

      # A LF cannot be escaped with a backslash because a backslash + LF
      # combo is regarded as line continuation and simply ignored.
      str.gsub!(/\n/, "'\n'")

      return str
    end
  else
    def open_pipe
      IO.popen([self.class.watcher_path] + @options + @paths)
    end
  end

  private

  def parse_options(options={})
    opts = ['--format=otnetstring']
    opts.concat(['--since-when', options[:since_when]]) if options[:since_when]
    opts.concat(['--latency', options[:latency]]) if options[:latency]
    opts.push('--no-defer') if options[:no_defer]
    opts.push('--watch-root') if options[:watch_root]
    opts.push('--file-events') if options[:file_events]
    # ruby 1.9's IO.popen(array-of-stuff) syntax requires all items to be strings
    opts.map {|opt| "#{opt}"}
  end

end