editserver 0.1.3 → 0.1.4

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -5,3 +5,5 @@ Gemfile.lock
5
5
  extra/editserver.scpt
6
6
  tags
7
7
  test/editserverrc
8
+ coverage/
9
+ profiling/
data/README.markdown CHANGED
@@ -20,9 +20,10 @@ Everything works, and core tests are in place. More information forthcoming.
20
20
 
21
21
  ### TODO
22
22
 
23
- * Finish remaining tests
24
23
  * Improve applescript reliability
25
- * Clean up `Editserver::` namespace
24
+ * Avoid dynamic creation of Editor subclasses in `Editserver::` namespace
25
+ (a consequence of an earlier design decision)
26
+ * Special case: OS X's `Terminal.app` as `editor['terminal']`
26
27
 
27
28
 
28
29
  [1]: https://chrome.google.com/webstore/detail/ppoadiihggafnhokfkpphojggcdigllp
@@ -1,4 +1,5 @@
1
1
  require 'optparse'
2
+ require 'fileutils'
2
3
  require 'yaml'
3
4
  require 'webrick/log'
4
5
  require 'rack'
@@ -24,6 +25,7 @@ class Editserver
24
25
  :AccessLog => [], # rack does its own access logging, so keep this blank
25
26
  :pid => nil,
26
27
  :config => '',
28
+ :daemonize => false,
27
29
  :environment => 'deployment'
28
30
  }
29
31
  end
@@ -38,36 +40,65 @@ class Editserver
38
40
  Options:
39
41
  ).gsub /^ +/, ''
40
42
 
41
- opt.on '-p', '--port NUMBER', Integer, "default: #{rackopts[:Port]}" do |arg|
43
+ opt.on '-H', '--host HOST', "IP/Hostname to bind to; #{rackopts[:Host]} by default" do |arg|
44
+ @rackopts[:Host] = arg
45
+ end
46
+
47
+ opt.on '-p', '--port NUMBER', Integer, "Port to bind; #{rackopts[:Port]} by default" do |arg|
42
48
  @rackopts[:Port] = arg
43
49
  end
44
50
 
51
+ opt.on '-d', '--default EDITOR', 'Editor to launch at root path; May be one of:',
52
+ (Editserver.new(editoropts).editors.keys - ['default']).join(', ') do |arg|
53
+ @editoropts['default'] = arg
54
+ end
55
+
45
56
  opt.on '-t', '--terminal CMD', 'Terminal to launch for console editors' do |arg|
46
57
  @editoropts['terminal'] = arg
47
58
  end
48
59
 
49
- opt.on '--rc PATH', "Path to rc file; #{@opts[:rcfile]} by default",
50
- '(Also can be set by exporting EDITSERVERRC to environment)' do |arg|
51
- @rcopts = nil # reset cached user opts
60
+ opt.on '-f', '--fork', 'Fork and daemonize; returns pid of daemon' do
61
+ @rackopts[:daemonize] = true
62
+ @rackopts[:pid] = "/tmp/#{File.basename $0}/#{File.basename $0}.pid"
63
+ end
64
+
65
+ opt.on '-q', '--quiet', 'Produce no output' do
66
+ @opts[:quiet] = true
67
+ @rackopts[:Logger] = WEBrick::Log.new nil, WEBrick::BasicLog::FATAL - 1 # zero, essentially
68
+ @rackopts[:environment] = 'none'
69
+ end
70
+
71
+ opt.on '--rc PATH', "Path to rc file; #{@opts[:rcfile]} by default" do |arg|
72
+ @rcopts = nil # reset cached user opts
52
73
  @opts[:rcfile] = File.expand_path arg
53
74
  end
54
75
 
55
76
  opt.on '--no-rc', 'Suppress reading of rc file' do
77
+ @rcopts = nil
56
78
  @opts[:norcfile] = true
57
79
  end
80
+
81
+ # normally implicit, but must be explicit when having an option beginning with `h'
82
+ opt.on_tail '-h', '--help' do
83
+ puts opt; exit
84
+ end
58
85
  end
59
86
  end
60
87
 
88
+ def say str
89
+ puts str unless @opts[:quiet]
90
+ end
91
+
61
92
  def rcopts
62
93
  @rcopts ||= begin
63
94
  empty = { 'rack' => {}, 'editor' => {} }
64
- rcfile = File.expand_path ENV['EDITSERVERRC'] || @opts[:rcfile]
95
+ rcfile = File.expand_path @opts[:rcfile]
65
96
 
66
97
  if @opts[:norcfile]
67
98
  empty
68
99
  elsif File.exists? rcfile
69
100
  opts = YAML.load_file File.expand_path(rcfile)
70
- opts ||= {}
101
+ opts = {} unless opts.is_a? Hash
71
102
  opts['rack'] ||= {}
72
103
  opts['editor'] ||= {}
73
104
  opts
@@ -103,13 +134,30 @@ class Editserver
103
134
 
104
135
  def run
105
136
  options.parse @args
137
+ $0 = 'editserver'
138
+
139
+ # Rack::Server issues shutdown on SIGINT only
140
+ trap :TERM do
141
+ trap :TERM, 'DEFAULT'
142
+ Process.kill :INT, $$
143
+ end
106
144
 
107
- begin
108
- $0 = 'editserver'
109
- puts banner
110
- server.start
111
- ensure
112
- puts fx("\nGoodbye!", [32,1])
145
+ if rackopts[:daemonize]
146
+ FileUtils.mkdir_p File.dirname(rackopts[:pid])
147
+ Process.wait fork { server.start }
148
+ sleep 0.1 until File.exists? rackopts[:pid] and File.read(rackopts[:pid]).to_i > 0
149
+
150
+ say host_and_port
151
+ say "Editserver PID: #{fx File.read(rackopts[:pid]), [36,1]}"
152
+ else
153
+ begin
154
+ say banner
155
+ server.start
156
+ say fx("\nGoodbye!", [32,1])
157
+ rescue StandardError => e
158
+ say fx(e.to_s, [31,1])
159
+ exit e.respond_to?(:errno) ? e.errno : 1
160
+ end
113
161
  end
114
162
  end
115
163
 
@@ -125,10 +173,15 @@ class Editserver
125
173
  \\ \\____\\ \\___,_\\ \\_\\ \\__\\/\\____/\\ \\____\\\\ \\_\\ \\ \\___/ \\ \\____\\\\ \\_\\
126
174
  \\/____/\\/__,_ /\\/_/\\/__/\\/___/ \\/____/ \\/_/ \\/__/ \\/____/ \\/_/
127
175
 
128
- Listening on #{fx "#{rackopts[:Host]}:#{rackopts[:Port]}", [32,1]}\
176
+ #{host_and_port}
177
+ Press #{fx 'Ctrl-C', [36,1]} to exit.\
129
178
  ).gsub(/^ {8}/, '')
130
179
  end
131
180
 
181
+ def host_and_port
182
+ "Listening on #{fx "#{rackopts[:Host]}:#{rackopts[:Port]}", [32,1]}"
183
+ end
184
+
132
185
  def fx str, effects = []
133
186
  return str unless $stdout.tty?
134
187
  str.gsub /^(.*)$/, "\e[#{[effects].flatten.join ';'}m\\1\e[0m"
@@ -1,3 +1,3 @@
1
1
  class Editserver
2
- VERSION = '0.1.3'
2
+ VERSION = '0.1.4'
3
3
  end
data/lib/editserver.rb CHANGED
@@ -12,9 +12,8 @@ class Editserver
12
12
  # OS X editors
13
13
  'mate' => 'mate -w',
14
14
  'mvim' => 'mvim --nofork --servername EDITSERVER', # does not return when app must be launched
15
- 'kod' => 'open -a Kod -W', # app must quit to release control
16
15
  'bbedit' => 'bbedit -w' # does not open file properly when app is launched
17
- }
16
+ }.reject { |k,v| not File.executable? %x(which #{k.shellsplit[0]}).chomp }
18
17
 
19
18
  attr_reader :editors
20
19
 
@@ -78,8 +77,8 @@ class Editserver
78
77
  klass = editor request.path_info
79
78
  Response.new(klass.new, request).call
80
79
  rescue RoutingError => e
81
- warn e.to_s
82
80
  res = Rack::Response.new
81
+ res.write e.to_s
83
82
  res.status = 500
84
83
  res.finish
85
84
  end
@@ -1,37 +1,57 @@
1
1
  $:.unshift File.expand_path('../../lib', __FILE__)
2
+ $:.unshift File.dirname(__FILE__)
2
3
 
4
+ require 'tempfile'
5
+ require 'yaml'
6
+ require 'webrick/log'
7
+ require 'rack/server'
3
8
  require 'editserver/command'
4
9
  require 'minitest/pride' if $stdout.tty?
5
10
  require 'minitest/autorun'
11
+ require 'socket-test'
6
12
 
7
13
  describe Editserver::Command do
14
+ before { @cmd = Editserver::Command.new ['--no-rc'] }
15
+
8
16
  describe :initialize do
9
17
  it 'should take a single optional argument' do
10
18
  Editserver::Command.method(:initialize).arity.must_equal -1
11
19
  end
12
20
 
13
21
  it 'should set internal state' do
14
- cmd = Editserver::Command.new ['--no-rc']
15
- cmd.instance_variable_get(:@args).must_equal ['--no-rc']
16
- cmd.instance_variable_get(:@opts).must_equal(:rcfile => '~/.editserverrc')
17
- cmd.instance_variable_get(:@editoropts).must_equal('default' => nil, 'terminal' => nil)
18
- cmd.instance_variable_get(:@rackopts).keys.sort_by(&:to_s).must_equal [
19
- :Host, :Port, :Logger, :AccessLog, :pid, :config, :environment
20
- ].sort_by &:to_s
22
+ @cmd.instance_variable_get(:@args).must_equal ['--no-rc']
23
+ @cmd.instance_variable_get(:@opts).must_equal(:rcfile => '~/.editserverrc')
24
+ @cmd.instance_variable_get(:@editoropts).must_equal('default' => nil, 'terminal' => nil)
25
+ @cmd.instance_variable_get(:@rackopts).keys.sort_by(&:to_s).must_equal [
26
+ :Host, :Port, :Logger, :AccessLog, :pid, :config, :daemonize, :environment
27
+ ].sort_by(&:to_s)
21
28
  end
22
29
  end
23
30
 
24
31
  describe :options do
25
- before { @cmd = Editserver::Command.new ['--no-rc'] }
26
-
27
32
  it 'should return an OptionParser object' do
28
33
  @cmd.options.must_be_kind_of OptionParser
29
34
  end
30
35
 
31
36
  it 'should modify internal state when parsing a list of arguments' do
37
+ @cmd.options.parse %w[--host 0.0.0.0]
38
+ @cmd.instance_variable_get(:@rackopts)[:Host].must_equal '0.0.0.0'
39
+
32
40
  @cmd.options.parse %w[--port 1000]
33
41
  @cmd.instance_variable_get(:@rackopts)[:Port].must_equal 1000
34
42
 
43
+ @cmd.options.parse %w[--fork]
44
+ @cmd.instance_variable_get(:@rackopts)[:daemonize].must_equal true
45
+ @cmd.instance_variable_get(:@rackopts)[:pid].must_equal "/tmp/#{File.basename $0}/#{File.basename $0}.pid"
46
+
47
+ @cmd.options.parse %w[--quiet]
48
+ @cmd.instance_variable_get(:@opts)[:quiet].must_equal true
49
+ @cmd.instance_variable_get(:@rackopts)[:Logger].level.must_equal WEBrick::BasicLog::FATAL - 1
50
+ @cmd.instance_variable_get(:@rackopts)[:environment].must_equal 'none'
51
+
52
+ @cmd.options.parse %w[--default mate]
53
+ @cmd.instance_variable_get(:@editoropts)['default'].must_equal 'mate'
54
+
35
55
  @cmd.options.parse %w[--terminal xterm]
36
56
  @cmd.instance_variable_get(:@editoropts)['terminal'].must_equal 'xterm'
37
57
 
@@ -43,22 +63,185 @@ describe Editserver::Command do
43
63
  end
44
64
  end
45
65
 
46
- #
47
- # TODO: finish tests
48
- #
66
+ describe :say do
67
+ it 'should write to $stdout unless --quiet option is specified' do
68
+ capture_io { @cmd.say 'AHHHHHH!' }.first.must_equal "AHHHHHH!\n"
69
+ @cmd.instance_variable_get(:@opts)[:quiet] = true
70
+ capture_io { @cmd.say 'AHHHHHH!' }.first.must_equal ''
71
+ end
72
+ end
49
73
 
50
74
  describe :rcopts do
51
- end
75
+ before do
76
+ @rcfile = Tempfile.new 'editserverrc'
77
+ @cmd.instance_variable_get(:@opts)[:rcfile] = @rcfile.path
78
+ end
79
+
80
+ after { (@rcfile.close; @rcfile.unlink) if @rcfile.path }
81
+
82
+ it 'should load the YAML file specified at @opts[:rcfile]' do
83
+ opts = { 'editor' => { 'default' => 'emacs', 'terminal' => 'xterm' }, 'rack' => { 'port' => 1000 } }
84
+ @rcfile.write opts.to_yaml
85
+ @rcfile.rewind
86
+ @cmd.rcopts.must_equal opts
87
+ end
88
+
89
+ it 'should create any missing top level keys' do
90
+ @rcfile.write({ 'foo' => 'bar' }.to_yaml)
91
+ @rcfile.rewind
92
+ @cmd.rcopts.must_equal 'foo' => 'bar', 'rack' => {}, 'editor' => {}
93
+ end
94
+
95
+ it 'should return bare options if --no-rc is specified' do
96
+ opts = { 'editor' => { 'default' => 'emacs', 'terminal' => 'xterm' }, 'rack' => { 'port' => 1000 } }
97
+ @rcfile.write opts.to_yaml
98
+ @rcfile.rewind
99
+ @cmd.instance_variable_get(:@opts)[:norcfile] = true
100
+ @cmd.rcopts.must_equal 'rack' => {}, 'editor' => {}
101
+ end
102
+
103
+ it 'should return bare options if rcfile does not exist' do
104
+ opts = { 'editor' => { 'default' => 'emacs', 'terminal' => 'xterm' }, 'rack' => { 'port' => 1000 } }
105
+ @rcfile.write opts.to_yaml
106
+ @rcfile.rewind
107
+ @rcfile.close
108
+ @rcfile.unlink
109
+ @cmd.rcopts.must_equal 'rack' => {}, 'editor' => {}
110
+ end
111
+ end # rcopts
52
112
 
53
113
  describe :rackopts do
114
+ it 'should return @rackopts masked with @rcopts' do
115
+ @cmd.instance_variable_get(:@opts)[:rcfile] = '/dev/null'
116
+ @cmd.rackopts[:Port].must_equal 9999
117
+ @cmd.rackopts[:Host].must_equal '127.0.0.1'
118
+ @cmd.instance_variable_get(:@rcopts).merge!('rack' => { 'port' => 65535, 'host' => '1.1.1.1' })
119
+ @cmd.rackopts[:Port].must_equal 65535
120
+ @cmd.rackopts[:Host].must_equal '1.1.1.1'
121
+ end
54
122
  end
55
123
 
56
124
  describe :editoropts do
125
+ it 'should return @editoropts masked with @rcopts' do
126
+ @cmd.instance_variable_get(:@opts)[:rcfile] = '/dev/null'
127
+ @cmd.editoropts['default'].must_equal nil
128
+ @cmd.editoropts['terminal'].must_equal nil
129
+ @cmd.instance_variable_get(:@rcopts).merge!('editor' => { 'default' => 'pony', 'terminal' => 'sparkles' })
130
+ @cmd.editoropts['default'].must_equal 'pony'
131
+ @cmd.editoropts['terminal'].must_equal 'sparkles'
132
+ end
57
133
  end
58
134
 
59
135
  describe :server do
136
+ it 'should return an instance of Rack::Server' do
137
+ @cmd.server.must_be_kind_of Rack::Server
138
+ end
139
+
140
+ it 'should pass rack options to new server instance' do
141
+ @cmd.rcopts['rack']['port'] = 4000
142
+ @cmd.server.options[:Port].must_equal 4000
143
+ @cmd.rcopts['rack']['pid'] = '/dev/null'
144
+ @cmd.server.options[:pid].must_equal '/dev/null'
145
+ end
146
+
147
+ it "should set the server's @app to an instance of Editserver" do
148
+ @cmd.server.app.must_be_kind_of Editserver
149
+ end
60
150
  end
61
151
 
62
152
  describe :run do
153
+ it 'should parse the arguments stored in @args' do
154
+ @cmd.instance_variable_set :@args, ['--help']
155
+
156
+ rd, wr = IO.pipe
157
+
158
+ pid = fork do
159
+ rd.close
160
+ $stdout.reopen wr
161
+ @cmd.run
162
+ end
163
+
164
+ wr.close
165
+ Process.wait2(pid).last.exitstatus.must_equal 0
166
+ rd.read.must_match /Usage:.*--help/m
167
+ end
168
+
169
+ it 'should start the server, and shutdown on SIGINT and SIGTERM' do
170
+ # thread, because we don't want to serially boot two servers
171
+ pool = []
172
+ [[:INT, 10000], [:TERM, 10001]].each do |sig, port|
173
+ pool << Thread.new do
174
+ cmd = @cmd.dup
175
+ rd, wr = IO.pipe
176
+
177
+ cmd.instance_variable_get(:@rackopts)[:Port] = port
178
+
179
+ pid = fork do
180
+ rd.close
181
+ $stdout.reopen wr
182
+ cmd.run
183
+ end
184
+
185
+ wr.close
186
+
187
+ sleep 0.1 until SocketTest.open? cmd.rackopts[:Host], port
188
+
189
+ Process.kill sig, pid
190
+
191
+ # give the server some time to shutdown
192
+ tries = 0
193
+ until Process.wait pid, Process::WNOHANG
194
+ sleep 0.1
195
+ # but no more than 2 seconds
196
+ Process.kill :KILL, pid if (tries += 1) > 20
197
+ end
198
+
199
+ (tries <= 20).must_equal true
200
+ rd.read.must_match /Listening.*#{cmd.rackopts[:Host]}:#{port}/
201
+ end
202
+ end
203
+
204
+ pool.each &:join
205
+ end
206
+
207
+ it 'should daemonize and write pidfile in daemon mode' do
208
+ @cmd.options.parse ['--fork', '--port=10002']
209
+
210
+ begin
211
+ bgpid = fork do
212
+ $stdout.reopen '/dev/null' # we're not too interested in this output
213
+ @cmd.run
214
+ end
215
+
216
+ sleep 0.1 until SocketTest.open? @cmd.rackopts[:Host], @cmd.rackopts[:Port]
217
+ Process.wait bgpid
218
+
219
+ pidfile = @cmd.rackopts[:pid]
220
+ pidbuf = File.read pidfile
221
+ pid = pidbuf.to_i
222
+
223
+ pidbuf.must_match /\A\d+\z/ # does it look like a pid?
224
+ Process.kill(0, pid).must_equal 1 # is it really alive?
225
+ ensure
226
+ # make sure the sucker dies
227
+ Process.kill :INT, pid
228
+ tries = 0
229
+ loop do
230
+ begin
231
+ Process.kill 0, pid
232
+ sleep 0.1
233
+ rescue Errno::ESRCH
234
+ break
235
+ end
236
+ if (tries += 1) > 20
237
+ Process.kill :KILL, pid
238
+ break
239
+ end
240
+ end
241
+ (tries <= 20).must_equal true
242
+ end
243
+
244
+ File.exists?(@cmd.rackopts[:pid]).must_equal false
245
+ end
63
246
  end
64
247
  end
@@ -0,0 +1,16 @@
1
+ require 'socket'
2
+
3
+ module SocketTest
4
+ class << self
5
+ include Socket::Constants
6
+
7
+ def open? host, port
8
+ sock = Socket.new AF_INET, SOCK_STREAM, 0
9
+ addr = Socket.sockaddr_in port, host
10
+ sock.connect addr
11
+ true
12
+ rescue Errno::ECONNREFUSED
13
+ false
14
+ end
15
+ end
16
+ end
metadata CHANGED
@@ -5,8 +5,8 @@ version: !ruby/object:Gem::Version
5
5
  segments:
6
6
  - 0
7
7
  - 1
8
- - 3
9
- version: 0.1.3
8
+ - 4
9
+ version: 0.1.4
10
10
  platform: ruby
11
11
  authors:
12
12
  - Sung Pae
@@ -14,7 +14,7 @@ autorequire:
14
14
  bindir: bin
15
15
  cert_chain: []
16
16
 
17
- date: 2011-02-16 00:00:00 -06:00
17
+ date: 2011-02-17 00:00:00 -06:00
18
18
  default_executable:
19
19
  dependencies:
20
20
  - !ruby/object:Gem::Dependency
@@ -86,8 +86,7 @@ files:
86
86
  - test/editserver/command.test.rb
87
87
  - test/editserver/editor.test.rb
88
88
  - test/editserver/response.test.rb
89
- - test/editserver/terminal/emacs.test.rb
90
- - test/editserver/terminal/vim.test.rb
89
+ - test/editserver/socket-test.rb
91
90
  - test/test-editor
92
91
  has_rdoc: true
93
92
  homepage: http://github.com/guns/editserver
@@ -126,6 +125,5 @@ test_files:
126
125
  - test/editserver/command.test.rb
127
126
  - test/editserver/editor.test.rb
128
127
  - test/editserver/response.test.rb
129
- - test/editserver/terminal/emacs.test.rb
130
- - test/editserver/terminal/vim.test.rb
128
+ - test/editserver/socket-test.rb
131
129
  - test/test-editor
@@ -1,5 +0,0 @@
1
- $:.unshift File.expand_path('../../../lib', __FILE__)
2
-
3
- require 'editserver/terminal/emacs'
4
- require 'minitest/pride' if $stdout.tty?
5
- require 'minitest/autorun'
@@ -1,5 +0,0 @@
1
- $:.unshift File.expand_path('../../../lib', __FILE__)
2
-
3
- require 'editserver/terminal/vim'
4
- require 'minitest/pride' if $stdout.tty?
5
- require 'minitest/autorun'