darwin-xrefresh-server 0.1.5

Sign up to get free protection for your applications and to get access to all the features.
data/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.1.5
@@ -0,0 +1,131 @@
1
+ #!/usr/bin/env ruby
2
+
3
+ # This script watches modifications on the given directories, using the new # FSEvents API in Leopard.
4
+ # Depends on rubycocoa!
5
+ # Needs rubygems and json gem!
6
+
7
+ # Based on code by Dave Dribin
8
+ # http://www.dribin.org/dave/blog/archives/2008/01/04/fswatch/
9
+
10
+ require "rubygems"
11
+ require 'set'
12
+ require 'optparse'
13
+ require 'ostruct'
14
+ require "yaml"
15
+ require File.join(File.expand_path(File.dirname(__FILE__)), '..', 'lib', 'xrefresh-server.rb') # this form is important for local development
16
+
17
+ module XRefreshServer
18
+
19
+ ################################################################################
20
+ # command-line parsing
21
+
22
+ COMMAND = File.basename($0)
23
+ USAGE = "Usage: #{COMMAND} [OPTIONS]"
24
+
25
+ options = OpenStruct.new
26
+ options.output = "-"
27
+ options.config = nil
28
+
29
+ opts = OptionParser.new do |o|
30
+ o.banner = USAGE
31
+ o.separator ""
32
+ o.separator "Specific options:"
33
+
34
+ o.on("-c", "--config FILE", "Config file") do |fn|
35
+ options.config = fn
36
+ end
37
+
38
+ o.on("-g", "--generate [FILE]", "Generates default config file on given path") do |fn|
39
+ fn = "~/#{CONFIG_FILE}" if fn.nil?
40
+ XRefreshServer::generate_config(File.expand_path(fn))
41
+ exit
42
+ end
43
+
44
+ o.on("-o", "--output FILE", "Write output to a file") do |fn|
45
+ options.output = fn
46
+ end
47
+
48
+ o.on_tail("-h", "--help", "Show this message") do
49
+ puts o
50
+ exit
51
+ end
52
+
53
+ o.on_tail("-v", "--version", "Show version") do
54
+ puts XRefreshServer::VERSION
55
+ exit
56
+ end
57
+ end
58
+
59
+ begin
60
+ opts.parse!(ARGV)
61
+ rescue
62
+ die "Unable to parse options: #{$!}"
63
+ end
64
+
65
+ # initialize output handle
66
+ if options.output == "-"
67
+ OUT = $stdout.clone
68
+ else
69
+ OUT = File.open(options.output, "w")
70
+ end
71
+
72
+ ################################################################################
73
+ # load config
74
+ unless options.config
75
+ path = File.expand_path("~/#{CONFIG_FILE}")
76
+ options.config = path if File.exists?(path)
77
+ path = File.expand_path("./#{CONFIG_FILE}")
78
+ options.config = path if File.exists?(path)
79
+ end
80
+ unless options.config
81
+ puts "Config file #{CONFIG_FILE} not found in current folder or home."
82
+ puts "It seems you are running xrefresh-server for first time."
83
+ puts "Do you want to generate default config file in home directory? [Yn]"
84
+ s = STDIN.getc.chr
85
+ if s=='y' || s=='Y'
86
+ options.config = File.expand_path("~/#{CONFIG_FILE}")
87
+ generate_config(options.config)
88
+ else
89
+ die "Please launch xrefresh-server with -c option and specify path to your config"
90
+ end
91
+ end
92
+ begin
93
+ CONFIG = YAML::load_file(options.config)
94
+ rescue
95
+ die "Unable to load or parse config: #{options.config}"
96
+ end
97
+
98
+ # sanitize config values
99
+ CONFIG["dir_include"] = '.*' unless CONFIG["dir_include"]
100
+ CONFIG["file_include"] = '.*' unless CONFIG["file_include"]
101
+ CONFIG["dir_exclude"] = '^$' unless CONFIG["dir_exclude"]
102
+ CONFIG["file_exclude"] = '^$' unless CONFIG["file_exclude"]
103
+ CONFIG["dir_include"] = Regexp.new(CONFIG["dir_include"])
104
+ CONFIG["file_include"] = Regexp.new(CONFIG["file_include"])
105
+ CONFIG["dir_exclude"] = Regexp.new(CONFIG["dir_exclude"])
106
+ CONFIG["file_exclude"] = Regexp.new(CONFIG["file_exclude"])
107
+ CONFIG["max_connections"] = 4 unless CONFIG["max_connections"]
108
+ CONFIG["host"] = GServer::DEFAULT_HOST unless CONFIG["host"]
109
+ CONFIG["debug"] = !!CONFIG["debug"]
110
+ CONFIG["audit"] = !!CONFIG["audit"]
111
+ CONFIG["defer_time"] = 0.5 unless CONFIG["defer_time"]
112
+ CONFIG["sleep_time"] = 0.1 unless CONFIG["sleep_time"]
113
+ CONFIG.freeze
114
+
115
+ ################################################################################
116
+ # run server
117
+ server = Server.new(CONFIG["port"], CONFIG["host"], CONFIG["max_connections"], $stderr, CONFIG["audit"], CONFIG["debug"])
118
+ server.start
119
+
120
+ ################################################################################
121
+ # run filesystem monitoring loop
122
+ start_id = FSEventsGetCurrentEventId()
123
+ start_time = Time.now.to_i # used to compare with mtime, which only has second accuracy
124
+ monitor = Monitor.new(server, CONFIG)
125
+ monitor.schedule(start_id)
126
+ monitor.run_loop(start_time) # blocking call
127
+
128
+ ################################################################################
129
+ # leave in peace
130
+ $out.flush
131
+ end
@@ -0,0 +1,65 @@
1
+ require 'json'
2
+ begin
3
+ require 'term/ansicolor'
4
+ include Term::ANSIColor
5
+ rescue LoadError
6
+ raise 'Run "sudo gem install term-ansicolor"'
7
+ end
8
+ # http://kpumuk.info/ruby-on-rails/colorizing-console-ruby-script-output/
9
+ if PLATFORM =~ /win32/ then
10
+ begin
11
+ require 'win32console'
12
+ include Win32::Console::ANSI
13
+ rescue LoadError
14
+ raise 'Run "sudo gem install win32console" to use terminal colors on Windows'
15
+ end
16
+ end
17
+
18
+ module XRefreshServer
19
+ VERSION = File.read(File.join(File.expand_path(File.dirname(__FILE__)), '..', 'VERSION'))
20
+ AGENT = "OSX xrefresh-server"
21
+ CONFIG_FILE = ".xrefresh-server.yml"
22
+
23
+ def self.die(s)
24
+ $stderr.puts s
25
+ exit 1
26
+ end
27
+
28
+ def self.generate_config(path)
29
+ puts "Generating config in #{path}"
30
+ File.open(path, "w") do |file|
31
+ file.puts <<CONFIG\
32
+ # here specify list of paths to monitor
33
+ paths:
34
+ - #{File.expand_path('~')} # by default watch user's home directory
35
+ # - /you/may/add/here/some/other/path
36
+ # - /you/may/add/here/some/another/path
37
+
38
+ # you can various filters (ruby regexp pattern)
39
+ # every file is split to dir and file part (for example /Users/mick/proj/coolapp and some_file.rb)
40
+ # both include filters must be satisfied
41
+ # both exclude filters must not be satisfied
42
+ # empty value means "apply no filtering"
43
+ dir_include:
44
+ dir_exclude: ^#{File.expand_path('~')}/Library|/\\.(svn|framework|app|pbproj|pbxproj|xcode(proj)?|bundle)/
45
+ file_include:
46
+ file_exclude: ^(CVS|SCCS|vssver.?.scc|\\.(cvsignore|git|svn|DS_Store)|_svn|Thumbs\\.db)$|~$|^(\\.(?!htaccess)[^/]*|\\.(tmproj|o|pyc)|svn-commit(\\.[2-9])?\\.tmp)$ # merged TextMate and Netbeans patterns
47
+
48
+ # xpert settings
49
+ host: #{GServer::DEFAULT_HOST}
50
+ port: 41258 # known port for clients to connect
51
+ max_connections: 4 # max client connections
52
+ debug: false # run in debug mode?
53
+ audit: false # audit server activity
54
+ defer_time: 0.5 # aggregation time for events
55
+ sleep_time: 0.1 # don't hung cpu in main loop
56
+ CONFIG
57
+ end
58
+ end
59
+
60
+ end
61
+
62
+ LIB_DIR = File.expand_path(File.dirname(__FILE__))
63
+ require File.join(LIB_DIR, 'xrefresh-server/client.rb')
64
+ require File.join(LIB_DIR, 'xrefresh-server/server.rb')
65
+ require File.join(LIB_DIR, 'xrefresh-server/monitor.rb')
@@ -0,0 +1,52 @@
1
+ require 'json'
2
+
3
+ module XRefreshServer
4
+
5
+ # client representation on server side
6
+ class Client
7
+ attr_accessor :id, :dead, :type, :agent
8
+
9
+ def initialize(id, socket)
10
+ @id = id
11
+ @socket = socket
12
+ @dead = false
13
+ @type = '?'
14
+ @agent = '?'
15
+ end
16
+
17
+ def name
18
+ green("#{@type}(#{@id})")
19
+ end
20
+
21
+ def send(data)
22
+ return if @dead
23
+ begin
24
+ @socket << data.to_json
25
+ rescue
26
+ OUT.puts "Client #{name} #{red("is dead")}"
27
+ @dead = true
28
+ end
29
+ end
30
+
31
+ def send_about(version, agent)
32
+ send({
33
+ "command" => "AboutMe",
34
+ "version" => version,
35
+ "agent" => agent
36
+ })
37
+ end
38
+
39
+ def send_do_refresh(root, name, type, date, time, files)
40
+ send({
41
+ "command" => "DoRefresh",
42
+ "root" => root,
43
+ "name" => name,
44
+ "date" => date,
45
+ "time" => time,
46
+ "type" => type,
47
+ "files" => files
48
+ })
49
+ end
50
+ end
51
+
52
+ end
@@ -0,0 +1,172 @@
1
+ require 'osx/foundation'
2
+ OSX.require_framework '/System/Library/Frameworks/CoreServices.framework/Frameworks/CarbonCore.framework'
3
+ include OSX
4
+
5
+ module XRefreshServer
6
+
7
+ class Monitor
8
+
9
+ def initialize(server, config)
10
+ @config = config
11
+ @server = server
12
+ @modified_dirs = Set.new
13
+ @paths_info = Hash.new
14
+ @streams = []
15
+ end
16
+
17
+ def schedule(start_event_id)
18
+ fsevents_cb = proc do |stream, ctx, numEvents, paths, marks, eventIDs|
19
+ # ctx doesn't work through rubycocoa?
20
+ root = FSEventStreamCopyPathsBeingWatched(stream).first
21
+ paths.regard_as('*')
22
+ numEvents.times do |n|
23
+ OUT.puts "Event: #{paths[n]}" if @config["debug"]
24
+ @modified_dirs.add({:root=>root, :dir=>paths[n]})
25
+ end
26
+ end
27
+
28
+ @config["paths"].each do |path|
29
+ OUT.puts " monitoring #{yellow(path)}"
30
+ # need to create new stream for every supplied path
31
+ # because we want to report registered sources of the changes
32
+ stream = FSEventStreamCreate(
33
+ KCFAllocatorDefault,
34
+ fsevents_cb,
35
+ nil,
36
+ [path],
37
+ start_event_id,
38
+ @config["defer_time"],
39
+ KFSEventStreamCreateFlagNone) #KFSEventStreamCreateFlagNoDefer
40
+ die "Failed to create the FSEventStream" unless stream
41
+
42
+ FSEventStreamScheduleWithRunLoop(
43
+ stream,
44
+ CFRunLoopGetCurrent(),
45
+ KCFRunLoopDefaultMode)
46
+
47
+ ok = FSEventStreamStart(stream)
48
+ die "Failed to start the FSEventStream" unless ok
49
+
50
+ @streams << stream
51
+ end
52
+ end
53
+
54
+ # blocking call
55
+ def run_loop(start_time)
56
+
57
+ activities = {"changed" => blue('*'), "deleted" => red('-'), "created" => green('+'), "renamed" => magenta('>')}
58
+
59
+ # main loop
60
+ OUT.puts "Waiting for file system events ..."
61
+ not_first_time = false
62
+ loop do
63
+ if @server.stopped?
64
+ OUT.puts "Server stopped"
65
+ break
66
+ end
67
+ @streams.each do |stream|
68
+ FSEventStreamFlushSync(stream)
69
+ end
70
+ buckets = Hash.new()
71
+ if not_first_time # all root folders are reported during first stream flush
72
+ @modified_dirs.each do |dir_info|
73
+ begin
74
+ dir = dir_info[:dir]
75
+ root = dir_info[:root]
76
+ unless dir=~@config["dir_include"]
77
+ OUT.puts "debug: #{dir} rejected because dir_include" if @config["debug"]
78
+ next
79
+ end
80
+ if dir=~@config["dir_exclude"]
81
+ OUT.puts "debug: #{dir} rejected because dir_exclude" if @config["debug"]
82
+ next
83
+ end
84
+
85
+ if File.exists?(dir)
86
+ OUT.puts "debug: checking dir #{dir}" if @config["debug"]
87
+ Dir.foreach(dir) do |file|
88
+ unless file=~@config["file_include"]
89
+ OUT.puts "debug: #{file} rejected because file_include" if @config["debug"]
90
+ next
91
+ end
92
+ if file=~@config["file_exclude"]
93
+ OUT.puts "debug: #{file} rejected because file_exclude" if @config["debug"]
94
+ next
95
+ end
96
+
97
+ full_path = File.join(dir, file)
98
+ next if File.directory?(full_path)
99
+ begin
100
+ stat = File.stat(full_path)
101
+ OUT.puts "debug: stat #{full_path}" if @config["debug"]
102
+ rescue
103
+ # file may not exist
104
+ OUT.puts "debug: stat failed #{full_path}" if @config["debug"]
105
+ next # keep silence
106
+ end
107
+ current_time = stat.mtime.to_i
108
+ original_time = @paths_info[full_path] || start_time
109
+
110
+ if (current_time > original_time)
111
+ OUT.puts "debug: reported #{full_path}" if @config["debug"]
112
+ relative_path = full_path[root.size+1..-1]
113
+ buckets[root]||=[]
114
+ buckets[root]<< {
115
+ "action" => "changed",
116
+ "path1" => relative_path,
117
+ "path2" => nil
118
+ }
119
+ end
120
+ @paths_info[full_path] = current_time
121
+ end
122
+ else
123
+ relative_path = dir[root.size+1..-1]
124
+ buckets[root]||=[]
125
+ buckets[root]<< {
126
+ "action" => "deleted",
127
+ "path1" => relative_path,
128
+ "path2" => nil
129
+ }
130
+ end
131
+ rescue
132
+ OUT.puts "debug: exception! #{dir}" if @config["debug"]
133
+ raise if @config["debug"]
134
+ next #keep silence
135
+ end
136
+ end
137
+ else
138
+ not_first_time = true
139
+ end
140
+
141
+ if buckets.size
142
+ buckets.each do |root, files|
143
+ OUT.puts " activity in #{yellow(root)}:"
144
+ files.each do |file|
145
+ OUT.puts " #{activities[file["action"]]} #{blue(file["path1"])}"
146
+ end
147
+ date = nil
148
+ time = nil
149
+ name = root
150
+ type = 'type?'
151
+
152
+ @server.clients.each do |client|
153
+ client.send_do_refresh(root, name, type, date, time, files)
154
+ end
155
+ end
156
+ buckets.clear
157
+ end
158
+
159
+ @modified_dirs.clear
160
+ sleep @config["sleep_time"]
161
+ end
162
+
163
+ streams.each do |stream|
164
+ FSEventStreamStop(stream)
165
+ FSEventStreamInvalidate(stream)
166
+ FSEventStreamRelease(stream)
167
+ end
168
+ end
169
+ end
170
+ end
171
+
172
+
@@ -0,0 +1,62 @@
1
+ require 'gserver'
2
+ require 'json'
3
+
4
+ module XRefreshServer
5
+
6
+ # server
7
+ class Server < GServer
8
+ attr_accessor :clients
9
+
10
+ def initialize(port, host, max_connections, *args)
11
+ super
12
+ @clients = Set.new
13
+ @last_client_id = 0
14
+ OUT.puts "#{green('Started server')} on #{blue("#{host}:#{port}")} (max #{max_connections} clients)"
15
+ end
16
+
17
+ def serve(socket)
18
+ socket.binmode
19
+ @last_client_id += 1
20
+ client = Client.new(@last_client_id, socket)
21
+ @clients.add(client)
22
+ buffer = ""
23
+ loop do
24
+ # accumulate incomming input in @buffer
25
+ begin
26
+ buffer += socket.gets
27
+ rescue
28
+ break
29
+ end
30
+
31
+ begin
32
+ # try to parse buffer
33
+ msg = JSON.parse buffer
34
+ rescue
35
+ # buffer may be incomplete due to packet fragmentation ...
36
+ else
37
+ # got whole message => process it
38
+ buffer = ""
39
+ process(client, msg)
40
+ end
41
+ end
42
+ end
43
+
44
+ def process(client, msg)
45
+ # see windows implementation in http://github.com/darwin/xrefresh/tree/master/src/winmonitor/Server.cs#ProcessMessage
46
+ case msg["command"]
47
+ when "Hello"
48
+ client.type = msg["type"] || '?'
49
+ client.agent = msg["agent"] || '?'
50
+ OUT.puts "Client #{client.name} [#{magenta(client.agent)}] has connected"
51
+ client.send_about(VERSION, AGENT)
52
+ when "Bye"
53
+ @clients.delete(client)
54
+ OUT.puts "Client #{client.name} has disconnected"
55
+ when "SetPage"
56
+ url = msg["url"] || '?'
57
+ OUT.puts "Client #{client.name} changed page to #{blue(url)}"
58
+ end
59
+ end
60
+ end
61
+
62
+ end
data/license.txt ADDED
@@ -0,0 +1,20 @@
1
+ Copyright (c) 2008-2009 Antonin Hildebrand
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHout WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, out OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/readme.md ADDED
@@ -0,0 +1,5 @@
1
+ # XRefresh Server for OSX
2
+
3
+ XRefresh is refresh automation for web developers. This is OSX filesystem monitor.
4
+
5
+ ## Visit [xrefresh.binaryage.com](http://xrefresh.binaryage.com)
metadata ADDED
@@ -0,0 +1,79 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: darwin-xrefresh-server
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.5
5
+ platform: ruby
6
+ authors:
7
+ - Antonin Hildebrand
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+
12
+ date: 2009-05-26 00:00:00 -07:00
13
+ default_executable: xrefresh-server
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: json
17
+ type: :runtime
18
+ version_requirement:
19
+ version_requirements: !ruby/object:Gem::Requirement
20
+ requirements:
21
+ - - ">="
22
+ - !ruby/object:Gem::Version
23
+ version: "0"
24
+ version:
25
+ - !ruby/object:Gem::Dependency
26
+ name: term-ansicolor
27
+ type: :runtime
28
+ version_requirement:
29
+ version_requirements: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: "0"
34
+ version:
35
+ description: XRefresh is browser refresh automation for web developers
36
+ email: antonin@hildebrand.cz
37
+ executables:
38
+ - xrefresh-server
39
+ extensions: []
40
+
41
+ extra_rdoc_files: []
42
+
43
+ files:
44
+ - VERSION
45
+ - bin/xrefresh-server
46
+ - lib/xrefresh-server.rb
47
+ - lib/xrefresh-server/client.rb
48
+ - lib/xrefresh-server/monitor.rb
49
+ - lib/xrefresh-server/server.rb
50
+ - license.txt
51
+ - readme.md
52
+ has_rdoc: false
53
+ homepage: http://github.com/darwin/xrefresh-server
54
+ post_install_message:
55
+ rdoc_options:
56
+ - --charset=UTF-8
57
+ require_paths:
58
+ - lib
59
+ required_ruby_version: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: "0"
64
+ version:
65
+ required_rubygems_version: !ruby/object:Gem::Requirement
66
+ requirements:
67
+ - - ">="
68
+ - !ruby/object:Gem::Version
69
+ version: "0"
70
+ version:
71
+ requirements: []
72
+
73
+ rubyforge_project: xrefresh-server
74
+ rubygems_version: 1.2.0
75
+ signing_key:
76
+ specification_version: 3
77
+ summary: XRefresh filesystem monitor for OSX
78
+ test_files: []
79
+