Class: EasyServe

Inherits:
Object
  • Object
show all
Defined in:
lib/easy-serve.rb,
lib/easy-serve/remote.rb,
lib/easy-serve/service.rb,
lib/easy-serve/remote/drb.rb,
lib/easy-serve/remote/run.rb,
lib/easy-serve/remote/eval.rb,
lib/easy-serve/service/tunnelled.rb

Defined Under Namespace

Classes: EasyFormatter, RemoteError, Service, ServicesExistError, TCPService, UNIXService

Constant Summary collapse

VERSION =
"0.13"
MAX_TRIES =
10

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(**opts) ⇒ EasyServe

Options:

services_file: filename

    name of file that server addresses are written to (if this process
    is creating them) or read from (if this process is accessing them).
    If not specified, services will be available to child processes,
    but harder to access from other processes.

    If the filename has a ':' in it, we assume that it is a remote
    file, specified as [user@]host:path/to/file as in scp and rsync,
    and attempt to read its contents over an ssh connection.

interactive: true|false

    true means do not propagate ^C to child processes.
    This is useful primarily when running in irb.


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
# File 'lib/easy-serve.rb', line 75

def initialize **opts
  @services_file = opts[:services_file]
  @created_services_file = false
  @interactive = opts[:interactive]
  @log = opts[:log] || self.class.null_logger
  @children = [] # pid
  @passive_children = [] # pid
  @owner = false
  @sibling = true
  @ssh_sessions = []
  @tmpdir = nil
  @services = opts[:services] # name => service
  
  unless services
    if services_file
      @services =
        begin
          load_service_table
        rescue Errno::ENOENT
          init_service_table
        end
    else
      init_service_table
    end
  end
end

Instance Attribute Details

#childrenObject (readonly)

Returns the value of attribute children.



35
36
37
# File 'lib/easy-serve.rb', line 35

def children
  @children
end

#interactiveObject (readonly)

True means do not propagate ^C to child processes.



40
41
42
# File 'lib/easy-serve.rb', line 40

def interactive
  @interactive
end

#logObject

Returns the value of attribute log.



33
34
35
# File 'lib/easy-serve.rb', line 33

def log
  @log
end

#passive_childrenObject (readonly)

Returns the value of attribute passive_children.



36
37
38
# File 'lib/easy-serve.rb', line 36

def passive_children
  @passive_children
end

#servicesObject

Returns the value of attribute services.



34
35
36
# File 'lib/easy-serve.rb', line 34

def services
  @services
end

#services_fileObject (readonly)

Returns the value of attribute services_file.



37
38
39
# File 'lib/easy-serve.rb', line 37

def services_file
  @services_file
end

#siblingObject (readonly)

Is this a sibling process, started by the same parent process that started the services, even if started remotely? Implies not owner, but not conversely.



45
46
47
# File 'lib/easy-serve.rb', line 45

def sibling
  @sibling
end

Class Method Details

.bump_socket_filename(name) ⇒ Object



241
242
243
# File 'lib/easy-serve.rb', line 241

def self.bump_socket_filename name
  name =~ /-\d+\z/ ? name.succ : name + "-0"
end

.default_loggerObject



21
22
23
24
25
# File 'lib/easy-serve.rb', line 21

def self.default_logger
  log = Logger.new($stderr)
  log.formatter = EasyFormatter.new
  log
end

.handle_remote_eval_messagesObject



55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/easy-serve/remote/eval-mgr.rb', line 55

def EasyServe.handle_remote_eval_messages
  unpacker = MessagePack::Unpacker.new($stdin)
  unpacker.each do |msg|
    case
    when msg["service_names"]
      Thread.new {manage_remote_eval_client(msg); exit}
    when msg["exit"]
      puts "exiting"
      exit
    when msg["request"]
      response = self.send(*msg["command"])
      puts "response: #{response.inspect}"
    else
      puts "unhandled: #{msg.inspect}"
    end
  end

rescue => ex
  puts "ez error", ex, ex.backtrace
end

.handle_remote_run_messagesObject



57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
# File 'lib/easy-serve/remote/run-mgr.rb', line 57

def EasyServe.handle_remote_run_messages
  unpacker = MessagePack::Unpacker.new($stdin)
  unpacker.each do |msg|
    case
    when msg["service_names"]
      Thread.new {manage_remote_run_client(msg); exit}
    when msg["exit"]
      puts "exiting"
      exit
    when msg["request"]
      response = self.send(*msg["command"])
      puts "response: #{response.inspect}"
    else
      puts "unhandled: #{msg.inspect}"
    end
  end

rescue => ex
  puts "ez error", ex, ex.backtrace
end

.host_nameObject



249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
# File 'lib/easy-serve.rb', line 249

def EasyServe.host_name
  @host_name ||= begin
    hn = Socket.gethostname
    begin
      official_hostname = Socket.gethostbyname(hn)[0]
      if /\./ =~ official_hostname
        official_hostname
      else
        official_hostname + ".local"
      end
    rescue
      'localhost'
    end
  end
end

.manage_remote_eval_client(msg) ⇒ Object



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
# File 'lib/easy-serve/remote/eval-mgr.rb', line 4

def EasyServe.manage_remote_eval_client msg
  $VERBOSE = msg["verbose"]
  service_names, services_list, log_level, eval_string, host =
    msg.values_at(*%w{service_names services_list log_level eval_string host})

  services = {}
  service_names = Marshal.load(service_names)
  services_list = Marshal.load(services_list)
  services_list.each do |service|
    services[service.name] = service
  end
  
  log_args = msg["log"]
  log =
    case log_args
    when Array
      Logger.new(*log_args)
    when true
      EasyServe.default_logger
    when nil, false
      EasyServe.null_logger
    end

  EasyServe.start services: services, log: log do |ez|
    log = ez.log
    log.level = log_level
    log.formatter = nil if $VERBOSE

    ez.local *service_names do |*conns|
      begin
        pr = eval "proc do |conns, host, log| #{eval_string}; end"
        pr[conns, host, log]
      rescue => ex
        puts "ez error", ex.inspect
        lineno = (Integer(ex.backtrace[0][/(\d+):/, 1]) rescue nil)
        if lineno
          lines = eval_string.lines
          puts "    #{lineno-1} --> " + lines[lineno-1]
        end
        puts ex.backtrace
      end
    end
    
    log.info "done"
  end
rescue LoadError, ScriptError, StandardError => ex
  puts "ez error", ex, ex.backtrace
end

.manage_remote_run_client(msg) ⇒ Object



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
# File 'lib/easy-serve/remote/run-mgr.rb', line 4

def EasyServe.manage_remote_run_client msg
  $VERBOSE = msg["verbose"]
  service_names, services_list, log_level, host, dir, file, class_name, args =
    msg.values_at(*%w{
      service_names services_list log_level host
      dir file class_name args
    })

  services = {}
  service_names = Marshal.load(service_names)
  services_list = Marshal.load(services_list)
  services_list.each do |service|
    services[service.name] = service
  end
  
### opt for tmpdir and send files to it via ssh
  Dir.chdir(dir) if dir
  load file

  log_args = msg["log"]
  log =
    case log_args
    when Array
      Logger.new(*log_args)
    when true, :default
      EasyServe.default_logger
    when nil, false
      EasyServe.null_logger
    end

  EasyServe.start services: services, log: log do |ez|
    log = ez.log
    log.level = log_level
    log.formatter = nil if $VERBOSE

    ez.local *service_names do |*conns|
      begin
        cl = Object.const_get(class_name)
        ro = cl.new(conns, host, log, *args)
        ro.run
      rescue => ex
        puts "ez error", ex.inspect, ex.backtrace
      end
    end
    
    log.info "done"
  end
rescue LoadError, ScriptError, StandardError => ex
  puts "ez error", ex, ex.backtrace
end

.null_loggerObject



27
28
29
30
31
# File 'lib/easy-serve.rb', line 27

def self.null_logger
  log = Logger.new('/dev/null')
  log.level = Logger::FATAL
  log
end

.ssh_supports_dynamic_ports_forwardsObject



265
266
267
# File 'lib/easy-serve.rb', line 265

def EasyServe.ssh_supports_dynamic_ports_forwards
  @ssh_6 ||= (Integer(`ssh -V 2>&1`[/OpenSSH_(\d)/i, 1]) >= 6 rescue false)
end

.start(log: default_logger, **opts) ⇒ Object



47
48
49
50
51
52
53
54
55
# File 'lib/easy-serve.rb', line 47

def self.start(log: default_logger, **opts)
  ez = new(**opts, log: log)
  yield ez
rescue => ex
  log.error ex
  raise
ensure
  ez.cleanup if ez
end

Instance Method Details

#accessible_services(host, tunnel: false) ⇒ Object

Returns list of services that are accessible from host, setting up an ssh tunnel if specified. This is for the ‘ssh -R’ type of tunneling: a process, started remotely by some main process, needs to connect back to its siblings, other children of that main process. OpenSSH 6.0 or later is advised, but not necessary, for the tunnel option.



371
372
373
374
375
376
377
378
379
380
381
382
# File 'lib/easy-serve.rb', line 371

def accessible_services host, tunnel: false
  tcp_svs = services.values.grep(TCPService)
  return tcp_svs unless tunnel and host != "localhost" and host != "127.0.0.1"

  require 'easy-serve/service/accessible'

  tcp_svs.map do |service|
    service, ssh_session = service.accessible(host, log)
    @ssh_sessions << ssh_session # let GC close them
    service
  end
end

#child(*service_names, passive: false) ⇒ Object

A passive client child may be stopped after all active clients exit.



339
340
341
342
343
344
345
346
347
# File 'lib/easy-serve.rb', line 339

def child *service_names, passive: false
  c = fork do
    conns = service_names.map {|sn| services[sn].connect}
    yield(*conns) if block_given?
    no_interrupt_if_interactive
  end
  (passive ? passive_children : children) << c
  c
end

#choose_socket_filename(name, base: nil) ⇒ Object



233
234
235
236
237
238
239
# File 'lib/easy-serve.rb', line 233

def choose_socket_filename name, base: nil
  if base
    "#{base}-#{name}"
  else
    File.join(tmpdir, "sock-#{name}") ## permissions?
  end
end

#clean_tmpdirObject



229
230
231
# File 'lib/easy-serve.rb', line 229

def clean_tmpdir
  FileUtils.remove_entry @tmpdir if @tmpdir
end

#cleanupObject



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
158
159
160
161
162
163
164
165
166
# File 'lib/easy-serve.rb', line 126

def cleanup
  handler = trap("INT") do
    trap("INT", handler)
  end

  children.each do |pid|
    log.debug {"waiting for client pid=#{pid} to stop"}
    begin
      Process.waitpid pid
    rescue Errno::ECHILD
      log.debug {"client pid=#{pid} was already waited for"}
    end
  end

  passive_children.each do |pid|
    log.debug {"stopping client pid=#{pid}"}
    Process.kill("TERM", pid)
    begin
      Process.waitpid pid
    rescue Errno::ECHILD
      log.debug {"client pid=#{pid} was already waited for"}
    end
  end
  
  if @owner
    services.each do |name, service|
      log.info "stopping #{name}"
      service.cleanup
    end

    if @created_services_file
      begin
        FileUtils.rm services_file
      rescue Errno::ENOENT
        log.warn "services file #{services_file.inspect} was deleted already"
      end
    end
  end
  
  clean_tmpdir
end

#host_nameObject



245
246
247
# File 'lib/easy-serve.rb', line 245

def host_name
  EasyServe.host_name
end

#init_service_tableObject



119
120
121
122
123
124
# File 'lib/easy-serve.rb', line 119

def init_service_table
  @services ||= begin
    @owner = true
    {}
  end
end

#load_service_tableObject



102
103
104
105
106
107
108
109
110
111
112
113
# File 'lib/easy-serve.rb', line 102

def load_service_table
  case services_file
  when /\A(\S*):(.*)/
    IO.popen ["ssh", $1, "cat #$2"], "r" do |f|
      load_service_table_from_io f
    end
  else
    File.open(services_file) do |f|
      load_service_table_from_io f
    end
  end
end

#load_service_table_from_io(io) ⇒ Object



115
116
117
# File 'lib/easy-serve.rb', line 115

def load_service_table_from_io io
  YAML.load(io).tap {@sibling = false}
end

#local(*service_names) ⇒ Object

A local client runs in the same process, not a child process.



350
351
352
353
354
355
356
357
358
# File 'lib/easy-serve.rb', line 350

def local *service_names
  conns = service_names.map {|sn| services[sn].connect}
  yield(*conns) if block_given?
ensure
  conns and conns.each do |conn|
    conn.close unless conn.closed?
  end
  log.info "stopped local client"
end

#no_interrupt_if_interactiveObject

^C in the irb session (parent process) should not kill the service (child process)



362
363
364
# File 'lib/easy-serve.rb', line 362

def no_interrupt_if_interactive
  trap("INT") {} if interactive
end

#remote(*service_names, host: nil, **opts) ⇒ Object

Raises:

  • (ArgumentError)


6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# File 'lib/easy-serve/remote.rb', line 6

def remote *service_names, host: nil, **opts
  raise ArgumentError, "no host specified" unless host

  if opts[:eval]
    require 'easy-serve/remote/eval'
    remote_eval(*service_names, host: host, **opts)

  elsif opts[:file]
    require 'easy-serve/remote/run'
    remote_run(*service_names, host: host, **opts)

  elsif block_given?
    require 'easy-serve/remote/drb'
    remote_drb(*service_names, host: host, **opts, &Proc.new)

  else
    raise ArgumentError, "cannot select remote mode based on arguments"
  end
end

#remote_drb(*service_names, host: nil) ⇒ Object

useful for testing only – use _eval or _run for production. Note: as with #local, the code block runs in the main thread, by default. It’s up to you to start another thread inside the code block if you want more concurrency. This is for convenience when testing (cases in which concurrency needs to be controlled explicitly).



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
# File 'lib/easy-serve/remote/drb.rb', line 9

def remote_drb *service_names, host: nil
  ## remote logfile option?

  DRb.start_service("druby://#{host_name}:0", nil)

  hostname = host.sub(/.*@/,"")
  host_uri = "druby://#{hostname}:0"

  log.progname = "remote_drb #{host}"

  IO.popen ["ssh", host, "ruby"], "w+" do |ssh|
    ssh.puts %Q{
      $stdout.sync = true
      begin
        require 'drb'
        require 'yaml'
        require 'easy-serve'
        
        service_names = #{service_names.inspect}
        services = YAML.load(#{YAML.dump(services).inspect})
        log_level = #{log.level}
        host_uri = #{host_uri.inspect}
        
        EasyServe.start services: services do |ez|
          log = ez.log
          log.level = log_level
          log.formatter = nil if $VERBOSE

          ez.local *service_names do |*conns|
            begin
              DRb.start_service(host_uri, {conns: conns})
              puts DRb.uri
              
              Thread.new do
                loop do
                  sleep 1
                  begin
                    puts "."
                  rescue
                    exit
                  end
                end
              end
              
              DRb.thread.join

            rescue => ex
              puts "ez error", ex, ex.backtrace
            end
          end
        end
      rescue => ex
        puts "ez error", ex, ex.backtrace
      end
    }
    
    ssh.close_write
    result = ssh.gets
    
    if !result
      raise RemoteError, "problem with ssh connection to remote"
    else
      error = result[/ez error/]
      if error
        raise RemoteError, "error raised in remote: #{ssh.read}"
      else
        uri = result[/druby:\/\/\S+/]
        if uri
          Thread.new do
            loop do
              ssh.gets # consume the "."
            end
          end
        
          log.debug "remote is at #{uri}"
          ro = DRbObject.new_with_uri(uri)
          conns = ro[:conns]
          conns_ary = []
          conns.each {|c| conns_ary << c} # needed because it's a DRbObject
          yield(*conns_ary) if block_given?
        else
          raise RemoteError,
            "no druby uri in string from remote: #{result.inspect}"
        end
      end
    end
  end
end

#remote_eval(*service_names, host: nil, passive: false, tunnel: false, **opts) ⇒ Object

useful simple cases in testing and in production, but long eval strings can be hard to debug – use _run instead. Returns pid of child managing the ssh connection.

Note, unlike #local and #child, by default logging goes to the null logger. If you want to see logs from the remote, you need to choose:

  1. Log to remote file: pass log: [args…] with args as in Logger.new

  2. Log back over ssh: pass log: true.



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
# File 'lib/easy-serve/remote/eval.rb', line 15

def remote_eval *service_names,
                host: nil, passive: false, tunnel: false, **opts
  child_pid = fork do
    log.progname = "remote_eval #{host}"

    IO.popen [
        "ssh", host, "ruby",
        "-r", "easy-serve/remote/eval-mgr",
        "-e", "EasyServe.handle_remote_eval_messages"
      ],
      "w+" do |ssh|

      ssh.sync = true
      services_list = accessible_services(host, tunnel: tunnel)

      MessagePack.pack(
        {
          verbose:        $VERBOSE,
          service_names:  Marshal.dump(service_names),
          services_list:  Marshal.dump(services_list),
          log_level:      log.level,
          eval_string:    opts[:eval],
          host:           host,
          log:            opts[:log]
        },
        ssh)

      while s = ssh.gets
        case s
        when /^ez error/
          raise RemoteError, "error raised in remote: #{ssh.read}"
        else
          puts s
        end
      end
    end
  end

  (passive ? passive_children : children) << child_pid
  child_pid
end

#remote_run(*service_names, host: nil, passive: false, tunnel: false, **opts) ⇒ Object

useful in production, though it requires remote lib files to be set up. Returns pid of child managing the ssh connection.

Note, unlike #local and #child, by default logging goes to the null logger. If you want to see logs from the remote, you need to choose:

  1. Log to remote file: pass log: [args…] with args as in Logger.new

  2. Log back over ssh: pass log: true.



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
# File 'lib/easy-serve/remote/run.rb', line 14

def remote_run *service_names, host: nil, passive: false, tunnel: false, **opts
  child_pid = fork do
    log.progname = "remote_run #{host}"

    IO.popen [
        "ssh", host, "ruby",
        "-r", "easy-serve/remote/run-mgr",
        "-e", "EasyServe.handle_remote_run_messages"
      ],
      "w+" do |ssh|

      ssh.sync = true
      services_list = accessible_services(host, tunnel: tunnel)

      MessagePack.pack(
        {
          verbose:        $VERBOSE,
          service_names:  Marshal.dump(service_names),
          services_list:  Marshal.dump(services_list),
          log_level:      log.level,
          host:           host,
          dir:            opts[:dir],
          file:           opts[:file],
          class_name:     opts[:class_name],
          args:           opts[:args],
          log:            opts[:log]
        },
        ssh)

      while s = ssh.gets
        case s
        when /^ez error/
          raise RemoteError, "error raised in remote: #{ssh.read}"
        else
          puts s
        end
      end
    end
  end

  (passive ? passive_children : children) << child_pid
  child_pid
end

#service(name, proto = nil, **opts) ⇒ Object

Start a service named name. The name is referenced in #child, #local, and #remote to connect a new process to this service.

The proto can be either :unix (the default) or :tcp; the value can also be specifed with the proto: key-value argument.

Other key-value arguments are:

:path

for unix sockets, path to the socket file to be created

:base

for unix sockets, a base string for constructing the socket filename, if :path option is not provided; if neither :path nor :base specified, socket is in a tmp dir with filename based on name.

:bind_host

interface this service listens on, such as:

"0.0.0.0", "<any>" (same)

"localhost", "127.0.0.1" (same)

or a specific hostname.
:connect_host

host specifier used by remote clients to connect. By default, this is constructed from the bind_host. For example, with bind_host: “<any>”, the default connect_host is the current hostname (see #host_name).

:port

port this service listens on; defaults to 0 to choose a free port



301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
# File 'lib/easy-serve.rb', line 301

def service name, proto = nil, **opts
  proto ||= opts.delete(:proto) || :unix
  case proto
  when :unix
    opts[:path] ||= choose_socket_filename(name, base: opts[:base])

  when :tcp
    opts[:connect_host] ||=
      case opts[:bind_host]
      when nil, "0.0.0.0", /\A<any>\z/i
        host_name
      when "localhost", "127.0.0.1"
        "localhost"
      end
  end

  service = Service.for(name, proto, **opts)
  rd, wr = IO.pipe
  pid = fork do
    rd.close
    log.progname = name
    log.info "starting"
    
    svr = service.serve(max_tries: MAX_TRIES, log: log)
    yield svr if block_given?
    no_interrupt_if_interactive

    Marshal.dump service, wr
    wr.close
    sleep
  end

  wr.close
  services[name] = Marshal.load rd
  rd.close
end

#start_servicesObject



168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
# File 'lib/easy-serve.rb', line 168

def start_services
  return unless @owner

  if not services_file
    log.debug {"starting services without services_file"}
    yield
    return
  end

  lock_success = with_lock_file *File.split(services_file) do
    # Successful creation of the lock file gives this process the
    # right to check and create the services_file itself.

    if File.exist? services_file
      raise ServicesExistError,
        "Services at #{services_file.inspect} already exist."
    end

    log.debug {"starting services stored in #{services_file.inspect}"}
    yield

    tmp = services_file + ".tmp"
    File.open(tmp, "w") do |f|
      YAML.dump(services, f)
    end
    FileUtils.mv(tmp, services_file)
    @created_services_file = true
  end

  unless lock_success
    raise ServicesExistError,
      "Services at #{services_file.inspect} are being created."
  end
end

#tmpdirObject



222
223
224
225
226
227
# File 'lib/easy-serve.rb', line 222

def tmpdir
  @tmpdir ||= begin
    require 'tmpdir'
    Dir.mktmpdir "easy-serve-"
  end
end

#tunnel_to_remote_servicesObject

Set up tunnels as needed and modify the service list so that connections will go to local endpoints in those cases. Call this method in non-sibling invocations, such as when the server file has been copied to a remote host and used to start a new client. This is for the ‘ssh -L’ type of tunneling: a process needs to connect to a cluster of remote EasyServe processes that already exist and do not know about this process.



390
391
392
393
394
395
396
397
398
399
400
401
402
# File 'lib/easy-serve.rb', line 390

def tunnel_to_remote_services
  return if sibling

  require 'easy-serve/service/tunnelled'

  tunnelled_services = {}
  services.each do |service_name, service|
    service, ssh_session = service.tunnelled
    tunnelled_services[service_name] = service
    @ssh_sessions << ssh_session if ssh_session # let GC close them
  end
  @services = tunnelled_services
end

#with_lock_file(dir, base) ⇒ Object

Returns true if this process got the lock.



204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
# File 'lib/easy-serve.rb', line 204

def with_lock_file dir, base
  lock_file = File.join(dir, ".lock.#{base}")

  begin
    FileUtils.ln_s ".#{Process.pid}.#{base}", lock_file
  rescue Errno::EEXIST
    return false
  end

  begin
    yield
  ensure
    FileUtils.rm_f lock_file
  end

  true
end