expedite 0.0.2 → 0.1.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d23f0d668e02d87588fa2838e18afe60ca22e42a6df958c28418a5e3629a596c
4
- data.tar.gz: 99086610fca24d08680ca1824315b179e904b31aade25aba8365f0d0873d6ac5
3
+ metadata.gz: 7c3af01b23cd76fd62beb91411f18fd9535c49cca41b530496f2031abc294b67
4
+ data.tar.gz: 260e78502a54f80f3c101f0880a79f738d68bf414adc401253d5cfec0a2837b9
5
5
  SHA512:
6
- metadata.gz: 1e26f98230e95ae04828b9500f1e5e1dabd76296fa0d41a18ef55ea653e115b2283190ddfa706f1280e3bb78959e1ab9ab36f4d17ecd09b91f9a6c954ee06789
7
- data.tar.gz: d46a15f0bf0bbf68e87a0cd58d4df5d9af7850c51619199d1006f6313b44c746f6d3188fc09bed1ef0cefa636a61671a1c283fa8201e0c9bbbf018a775618dac
6
+ metadata.gz: 0b0fdaa31cf55dd0fde167748208e5f1f1be5b8cb1b2def68cae75516df458614855e4c1c2bb4affec937dc7d7c43c4d8f04bb830998ff5c9ab7d6c6b65bf9e7
7
+ data.tar.gz: d59acca092ec657cadc977c2af264cf811b4cf1b601e5bd8074bd21d91424b3fc07e81930bde3443bdce51e05965541dd132a819a1d5fa4812f6fd32b0809b95
data/README.md CHANGED
@@ -8,69 +8,76 @@ derivatives to start faster.
8
8
 
9
9
  ## Usage
10
10
 
11
- To use expedite you need to register variants and commands in an `expedite_helper.rb`
11
+ To use expedite you need to define agents and actions in an `expedite_helper.rb`
12
12
  that is placed in the root directory of your application. The sample discussed
13
13
  in this section is in the [examples/simple](examples/simple) folder.
14
14
 
15
- This is the "parent" variant:
15
+ This is the "parent" agent:
16
16
  ```
17
- # You can pass `keep_alive: true` if you want the variant to restart
18
- # automatically if it is terminated. This option defaults to false.
19
- Expedite::Variants.register('parent') do
20
- $sleep_parent = 1
17
+ Expedite.define do
18
+ agent :parent do
19
+ $parent_var = 1
20
+ end
21
21
  end
22
22
  ```
23
23
 
24
- You can register variants that are based on other variants. You can also have wildcard
24
+ You can define agents that are based on other agents. You can also have wildcard
25
25
  matchers.
26
+
26
27
  ```
27
- Expedite::Variants.register('development/*', parent: 'parent') do |name|
28
- $sleep_child = name
28
+ Expedite.define do
29
+ agent "development/*", parent: :parent do |name|
30
+ $development_var = name
31
+ end
29
32
  end
30
33
  ```
31
34
 
32
- You register commands by creating classes in the `Expedite::Command` module. For example,
33
- this defines a `custom` command.
35
+ The following defines an `info` action.
34
36
 
35
37
  ```
36
- Expedite::Commands.register("custom") do
37
- puts "[#{Expedite.variant}] sleeping for 5"
38
- puts "$sleep_parent = #{$sleep_parent}"
39
- puts "$sleep_child = #{$sleep_child}"
40
- puts "[#{Expedite.variant}] done"
38
+ Expedite.define do
39
+ action :info do
40
+ puts " Process.pid = #{Process.pid}"
41
+ puts " Process.ppid = #{Process.ppid}"
42
+ puts " $parent_var = #{$parent_var}"
43
+ puts "$development_var = #{$development_var}"
44
+ end
41
45
  end
42
46
  ```
43
47
 
44
- After registering your variant and commands, you can then use it. In the simple
45
- example, the `main.rb` calls the `custom` command on the `development/abc`
46
- variant.
48
+ After defining your agents and actions, you can then use it. In the simple
49
+ example, the `main.rb` calls the `info` command on the `development/abc`
50
+ agent.
47
51
 
52
+ The `invoke` method will execute the action, and return the result. There
53
+ is also an `exec` method that will replace the current executable with
54
+ the action; in that case, the return result is the exit code.
48
55
  ```
49
56
  require 'expedite'
50
57
 
51
- Expedite.v("development/abc").call("custom")
58
+ Expedite.agent("development/abc").invoke("info")
52
59
  ```
53
60
 
54
61
  When you run `main.rb`, the following output is produced. Note that `$sleep_parent`
55
- comes from teh `parent` variant, and `$sleep_child` comes from the `development/abc`
56
- variant.
62
+ comes from teh `parent` agent, and `$sleep_child` comes from the `development/abc`
63
+ agent.
57
64
 
58
65
  ```
59
66
  # bundle exec ./main.rb
60
- [development/abc] sleeping for 5
61
- $sleep_parent = 1
62
- $sleep_child = development/abc
63
- [development/abc] done
67
+ Process.pid = 3855
68
+ Process.ppid = 3854
69
+ $parent_var = 1
70
+ $development_var = development/abc
64
71
  ```
65
72
 
66
73
  Calling `main.rb` automatically started the expedite server in the background.
67
74
  In the above example, it does the following:
68
75
 
69
- 1. Launch the `base` variant
70
- 2. Fork from the `base` variant to create the `development/abc` variant
71
- 3. Fork from the `development/abc` variant
76
+ 1. Launch the `parent` agent.
77
+ 2. Fork from the `parent` agent to create the `development/abc` agent.
78
+ 3. Fork from the `development/abc` agent to run the `info` command, and then quit.
72
79
 
73
- To explicitly stop the server and all the variants, you use:
80
+ To explicitly stop the server and all the agents, you use:
74
81
 
75
82
  ```
76
83
  $ bundle exec expedite stop
@@ -1,7 +1,7 @@
1
1
 
2
2
  module Expedite
3
- module Command
4
- class Basic
3
+ module Action
4
+ class Block
5
5
  attr_reader :runs_in
6
6
 
7
7
  def initialize(runs_in: :application, &block)
@@ -1,13 +1,13 @@
1
1
  module Expedite
2
- module Command
2
+ module Action
3
3
  class Boot
4
- def call
5
- variant = ARGV[0]
4
+ def call(*args)
5
+ agent = args[0]
6
6
 
7
- require "expedite/application"
7
+ require "expedite/server/agent"
8
8
 
9
- Expedite::Application.new(
10
- variant: variant,
9
+ Expedite::Server::Agent.new(
10
+ agent: agent,
11
11
  manager: UNIXSocket.for_fd(@child_socket.fileno),
12
12
  env: Expedite::Env.new(
13
13
  root: ENV['EXPEDITE_ROOT'],
@@ -1,10 +1,10 @@
1
- require 'expedite/command/basic'
2
- require 'expedite/command/boot'
1
+ require 'expedite/action/block'
2
+ require 'expedite/action/boot'
3
3
 
4
4
  module Expedite
5
- class Commands
5
+ class Actions
6
6
  def self.current
7
- @current ||= Commands.new
7
+ @current ||= Actions.new
8
8
  end
9
9
 
10
10
  def self.lookup(name)
@@ -12,16 +12,16 @@ module Expedite
12
12
  end
13
13
 
14
14
  ##
15
- # Registers a command. If multiple commands are registered with the
15
+ # Registers an action. If multiple actions are registered with the
16
16
  # same name, the last one takes precedence.
17
17
  #
18
- # [name] Name of the command. Expedite internal commands are prefixed
18
+ # [name] Name of the action. Expedite internal actions are prefixed
19
19
  # with "expedite/"
20
- # [klass_or_nil] Class of the command. If omitted, will default to
21
- # Expedite::Command::Basic.
22
- # [named_options] Command options. Passed to the initializer.
20
+ # [klass_or_nil] Class of the action. If omitted, will default to
21
+ # Expedite::Action::Block.
22
+ # [named_options] Action options. Passed to the initializer.
23
23
  def self.register(name, klass_or_nil = nil, **named_options, &block)
24
- self.current.register(name, klass_or_nil, **named_options, &block)
24
+ self.current.register(name.to_s, klass_or_nil, **named_options, &block)
25
25
  end
26
26
 
27
27
  ##
@@ -36,13 +36,13 @@ module Expedite
36
36
 
37
37
  def lookup(name)
38
38
  ret = @registrations[name]
39
- raise NotImplementedError, "Command #{name.inspect} not found" if ret.nil?
39
+ raise NotImplementedError, "Action #{name.inspect} not found" if ret.nil?
40
40
  ret
41
41
  end
42
42
 
43
43
  def register(name, klass_or_nil = nil, **named_options, &block)
44
44
  cmd = if klass_or_nil.nil?
45
- Command::Basic.new(**named_options, &block)
45
+ Action::Block.new(**named_options, &block)
46
46
  else
47
47
  klass_or_nil.new(**named_options)
48
48
  end
@@ -54,7 +54,7 @@ module Expedite
54
54
  @registrations = {}
55
55
 
56
56
  # Default registrations
57
- register("expedite/boot", Expedite::Command::Boot)
57
+ register("expedite/boot", Expedite::Action::Boot)
58
58
 
59
59
  nil
60
60
  end
@@ -0,0 +1,101 @@
1
+
2
+ module Expedite
3
+ # Definition of a Agent
4
+ class Agent
5
+ ##
6
+ # Name of the parent agent. This allows you to create agents from
7
+ # an existing agent.
8
+ # Defaults to nil.
9
+ attr_accessor :parent
10
+
11
+ ##
12
+ # If set to true, agent will be restarted automatically if it is killed.
13
+ # Defaults to false.
14
+ attr_accessor :keep_alive
15
+
16
+ ##
17
+ # [parent] Name of parent agent.
18
+ # [keep_alive] Specifies if the agent should be automatically restarted if it is terminated. Defaults to false.
19
+ # [after_fork] Block is executed when agent is first preloaded.
20
+ def initialize(parent: nil, keep_alive: false, &after_fork)
21
+ @parent = parent
22
+ @keep_alive = keep_alive
23
+ @after_fork_proc = after_fork
24
+ end
25
+
26
+ ##
27
+ # Called when agent if first preloaded. This version calls the after_fork
28
+ # block provided in the initializer.
29
+ def after_fork(agent)
30
+ @after_fork_proc&.call(agent)
31
+ end
32
+ end
33
+
34
+ class Agents
35
+ Registration = Struct.new(:matcher, :agent) do
36
+ def match?(name)
37
+ File.fnmatch?(matcher, name.to_s)
38
+ end
39
+ end
40
+
41
+ def self.current
42
+ @current ||= Agents.new
43
+ end
44
+
45
+ ##
46
+ # Retrieves the specified agent
47
+ def self.lookup(agent)
48
+ self.current.lookup(agent.to_s)
49
+ end
50
+
51
+ ##
52
+ # Registers a agent. Agents are matched in the
53
+ # order they are registered.
54
+ #
55
+ # [matcher] Wildcard to match a name against.
56
+ # [named_options] Agent options.
57
+ # [after_fork] Optional block that is called when
58
+ # agent is preloaded.
59
+ #
60
+ # = Example
61
+ # Expedite::Agents.register('base' do |name|
62
+ # puts "Base #{name} started"
63
+ # end
64
+ # Expedite::Agents.register('development/abc', parent: 'base') do |name|
65
+ # puts "Agent #{name} started"
66
+ # end
67
+ def self.register(matcher, **named_options, &after_fork)
68
+ self.current.register(matcher.to_s, **named_options, &after_fork)
69
+ end
70
+
71
+ ##
72
+ # Resets registrations to default
73
+ def self.reset
74
+ self.current.reset
75
+ end
76
+
77
+ def initialize
78
+ @registrations = []
79
+ end
80
+
81
+ def lookup(agent)
82
+ ret = @registrations.find do |r|
83
+ r.match?(agent)
84
+ end
85
+ raise NotImplementedError, "Agent #{agent.inspect} not found" if ret.nil?
86
+ ret.agent
87
+ end
88
+
89
+ def register(matcher, **named_options, &after_fork)
90
+ @registrations << Registration.new(
91
+ matcher,
92
+ Agent.new(**named_options, &after_fork)
93
+ )
94
+ end
95
+
96
+ def reset
97
+ @registrations = []
98
+ nil
99
+ end
100
+ end
101
+ end
@@ -2,9 +2,9 @@ module Expedite
2
2
  module Cli
3
3
  class Server
4
4
  def run(args)
5
- require 'expedite/server'
5
+ require 'expedite/server/controller'
6
6
 
7
- server = Expedite::Server.new(foreground: true)
7
+ server = Expedite::Server::Controller.new(foreground: true)
8
8
  server.boot
9
9
  end
10
10
 
@@ -0,0 +1,34 @@
1
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/client/status.rb
2
+ require 'expedite'
3
+ require 'expedite/cli/server'
4
+ require 'expedite/cli/stop'
5
+
6
+ module Expedite
7
+ module Cli
8
+ class Status
9
+ def run(args)
10
+ require 'expedite/server/controller'
11
+
12
+ ctrl = Expedite::Server::Controller.new(foreground: true)
13
+ if ctrl.running?
14
+ puts "Expedite is running (pid=#{ctrl.pid})"
15
+ puts
16
+ print_process ctrl.pid
17
+ Expedite.agent("__server__").invoke("application_pids").each do |pid|
18
+ print_process pid
19
+ end
20
+ else
21
+ puts "Expedite is not running"
22
+ end
23
+ end
24
+
25
+ def summary
26
+ 'Expedite server status'
27
+ end
28
+
29
+ def print_process(pid)
30
+ puts `ps -p #{pid} -o pid= -o command=`
31
+ end
32
+ end
33
+ end
34
+ end
@@ -2,10 +2,10 @@ module Expedite
2
2
  module Cli
3
3
  class Stop
4
4
  def run(args)
5
- require 'expedite/server'
5
+ require 'expedite/server/controller'
6
6
 
7
- server = Expedite::Server.new
8
- server.stop
7
+ ctrl = Expedite::Server::Controller.new
8
+ ctrl.stop
9
9
  end
10
10
 
11
11
  def summary
data/lib/expedite/cli.rb CHANGED
@@ -1,4 +1,5 @@
1
1
  require 'expedite/cli/server'
2
+ require 'expedite/cli/status'
2
3
  require 'expedite/cli/stop'
3
4
 
4
5
  module Expedite
@@ -26,6 +27,7 @@ module Expedite
26
27
  COMMANDS = {
27
28
  'help' => Cli::Help,
28
29
  'server' => Cli::Server,
30
+ 'status' => Cli::Status,
29
31
  'stop' => Cli::Stop,
30
32
  }
31
33
 
@@ -0,0 +1,124 @@
1
+
2
+ # Based on https://github.com/rails/spring/blob/master/lib/spring/client/run.rb
3
+
4
+ require 'bundler'
5
+ require 'rbconfig'
6
+ require 'socket'
7
+
8
+ require 'expedite/client/invoke'
9
+ require 'expedite/env'
10
+ require 'expedite/errors'
11
+ require 'expedite/protocol'
12
+
13
+ module Expedite
14
+ module Client
15
+ class Exec < Invoke
16
+ FORWARDED_SIGNALS = %w(INT QUIT USR1 USR2 INFO WINCH) & Signal.list.keys
17
+
18
+ def initialize(env: nil, agent: nil)
19
+ super
20
+
21
+ @signal_queue = []
22
+ end
23
+
24
+ def call(*args)
25
+ @args = args
26
+ begin
27
+ connect
28
+ rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
29
+ cold_run
30
+ else
31
+ warm_run
32
+ end
33
+ ensure
34
+ server.close if server
35
+ end
36
+
37
+ def warm_run
38
+ run
39
+ rescue CommandNotFound
40
+ raise
41
+ require "expedite/command"
42
+
43
+ if Expedite.command(args.first)
44
+ # Command installed since Expedite started
45
+ stop_server
46
+ cold_run
47
+ else
48
+ raise
49
+ end
50
+ end
51
+
52
+ def cold_run
53
+ boot_server
54
+ connect
55
+ run
56
+ end
57
+
58
+ def run
59
+ status = perform(*@args)
60
+
61
+ exit status.to_i
62
+ rescue Errno::ECONNRESET
63
+ exit 1
64
+ end
65
+
66
+ def verify_server_version
67
+ server_version = server.gets.chomp
68
+ if server_version != env.version
69
+ $stderr.puts "There is a version mismatch between the Expedite client " \
70
+ "(#{env.version}) and the server (#{server_version})."
71
+
72
+ if server_booted?
73
+ $stderr.puts "We already tried to reboot the server, but the mismatch is still present."
74
+ exit 1
75
+ else
76
+ $stderr.puts "Restarting to resolve."
77
+ stop_server
78
+ cold_run
79
+ end
80
+ end
81
+ end
82
+
83
+ def queue_signals
84
+ FORWARDED_SIGNALS.each do |sig|
85
+ trap(sig) { @signal_queue << sig }
86
+ end
87
+ end
88
+
89
+ def suspend_resume_on_tstp_cont(pid)
90
+ trap("TSTP") {
91
+ log "suspended"
92
+ Process.kill("STOP", pid.to_i)
93
+ Process.kill("STOP", Process.pid)
94
+ }
95
+ trap("CONT") {
96
+ log "resumed"
97
+ Process.kill("CONT", pid.to_i)
98
+ }
99
+ end
100
+
101
+ def forward_signals(application)
102
+ @signal_queue.each { |sig| kill sig, application }
103
+
104
+ FORWARDED_SIGNALS.each do |sig|
105
+ trap(sig) { forward_signal sig, application }
106
+ end
107
+ end
108
+
109
+ def forward_signal(sig, application)
110
+ if kill(sig, application) != 0
111
+ # If the application process is gone, then don't block the
112
+ # signal on this process.
113
+ trap(sig, 'DEFAULT')
114
+ Process.kill(sig, Process.pid)
115
+ end
116
+ end
117
+
118
+ def kill(sig, application)
119
+ application.puts(sig)
120
+ application.gets.to_i
121
+ end
122
+ end
123
+ end
124
+ end
@@ -0,0 +1,155 @@
1
+ require 'socket'
2
+
3
+ require 'expedite/env'
4
+ require 'expedite/errors'
5
+ require 'expedite/protocol'
6
+
7
+ module Expedite
8
+ module Client
9
+ class Invoke
10
+ CONNECT_TIMEOUT = 1
11
+ BOOT_TIMEOUT = 20
12
+
13
+ attr_reader :args, :env, :agent
14
+ attr_reader :server
15
+
16
+ def initialize(env: nil, agent: nil)
17
+ @env = env || Env.new
18
+ @agent = agent
19
+
20
+ @server_booted = false
21
+ end
22
+
23
+ def call(*args)
24
+ begin
25
+ connect
26
+ rescue Errno::ENOENT, Errno::ECONNRESET, Errno::ECONNREFUSED
27
+ boot_server
28
+ connect
29
+ end
30
+
31
+ perform(*args)
32
+ ensure
33
+ server.close if server
34
+ end
35
+
36
+ def perform(*args)
37
+ verify_server_version
38
+
39
+ agent, client = UNIXSocket.pair
40
+ connect_to_agent(client, args)
41
+ run_command(client, agent, args)
42
+ end
43
+
44
+ def verify_server_version
45
+ server_version = server.gets.chomp
46
+ raise ArgumentError, "Server mismatch. Expected #{env.version}, got #{server_version}." if server_version != env.version
47
+ end
48
+
49
+ def connect_to_agent(client, args)
50
+ server.send_io client
51
+
52
+ server.send_object("args" => args, "agent" => agent)
53
+
54
+ if IO.select([server], [], [], CONNECT_TIMEOUT)
55
+ server.gets or raise CommandNotFound
56
+ else
57
+ raise "Error connecting to Expedite server"
58
+ end
59
+ end
60
+
61
+ def run_command(client, agent, args)
62
+ log "sending command"
63
+
64
+ agent.send_io STDOUT
65
+ agent.send_io STDERR
66
+ agent.send_io STDIN
67
+
68
+ agent.send_object("args" => args, "env" => ENV.to_hash)
69
+
70
+ pid = server.gets
71
+ pid = pid.chomp if pid
72
+
73
+ # We must not close the client socket until we are sure that the application has
74
+ # received the FD. Otherwise the FD can end up getting closed while it's in the server
75
+ # socket buffer on OS X. This doesn't happen on Linux.
76
+ client.close
77
+
78
+ if pid && !pid.empty?
79
+ log "got pid: #{pid}"
80
+
81
+ ## suspend_resume_on_tstp_cont(pid)
82
+
83
+ ## forward_signals(application)
84
+ result = agent.recv_object
85
+ if result.key?("exception")
86
+ e = result["exception"]
87
+ log "got exception #{e}"
88
+ raise e
89
+ end
90
+
91
+ ret = result["return"]
92
+ log "got return value #{ret}"
93
+ return ret
94
+ else
95
+ log "got no pid"
96
+ raise UnknownError, "got no pid"
97
+ end
98
+ ensure
99
+ agent.close
100
+ end
101
+
102
+ def boot_server
103
+ env.socket_path.unlink if env.socket_path.exist?
104
+
105
+ pid = Process.spawn(gem_env, env.server_command, out: File::NULL)
106
+ timeout = Time.now + BOOT_TIMEOUT
107
+
108
+ @server_booted = true
109
+
110
+ until env.socket_path.exist?
111
+ _, status = Process.waitpid2(pid, Process::WNOHANG)
112
+
113
+ if status
114
+ # Server did not start
115
+ raise ArgumentError, "Server exited: #{status.exitstatus}"
116
+ elsif Time.now > timeout
117
+ $stderr.puts "Starting Expedite server with `#{env.server_command}` " \
118
+ "timed out after #{BOOT_TIMEOUT} seconds"
119
+ exit 1
120
+ end
121
+
122
+ sleep 0.1
123
+ end
124
+ end
125
+
126
+ def server_booted?
127
+ @server_booted
128
+ end
129
+
130
+ def stop_server
131
+ server.close
132
+ @server = nil
133
+ env.stop
134
+ end
135
+
136
+ def log(message)
137
+ env.log "[client] #{message}"
138
+ end
139
+
140
+ def connect
141
+ @server = UNIXSocket.open(env.socket_path)
142
+ end
143
+
144
+ def gem_env
145
+ bundle = Bundler.bundle_path.to_s
146
+ paths = Gem.path + ENV["GEM_PATH"].to_s.split(File::PATH_SEPARATOR)
147
+
148
+ {
149
+ "GEM_PATH" => [bundle, *paths].uniq.join(File::PATH_SEPARATOR),
150
+ "GEM_HOME" => bundle
151
+ }
152
+ end
153
+ end
154
+ end
155
+ end
data/lib/expedite/env.rb CHANGED
@@ -1,6 +1,7 @@
1
1
  require 'digest'
2
2
  require 'pathname'
3
3
  require 'expedite/version'
4
+ require 'expedite/server/agent_manager'
4
5
 
5
6
  module Expedite
6
7
  class Env
@@ -18,7 +19,7 @@ module Expedite
18
19
 
19
20
  env = self
20
21
  @applications = Hash.new do |h, k|
21
- h[k] = ApplicationManager.new(k, env)
22
+ h[k] = Server::AgentManager.new(k, env)
22
23
  end
23
24
  end
24
25