editserver 0.1.3 → 0.1.4

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/.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'