Class: Terret::Boot
- Inherits:
-
Object
- Object
- Terret::Boot
- Defined in:
- lib/terret/boot.rb
Overview
The impure half: requiring the code a composition names, turning plugin names into constants, and mounting. Split out from Composition so the pure half stays runnable on a machine with none of this installed.
Constant Summary collapse
- Error =
Class.new(StandardError)
Instance Attribute Summary collapse
-
#allow_config_ruby ⇒ Object
readonly
Returns the value of attribute allow_config_ruby.
-
#resolved ⇒ Object
readonly
Returns the value of attribute resolved.
Class Method Summary collapse
-
.shutdown(ctx, loader: nil) ⇒ Object
Take a booted context down through the loader, so every row's own stop hook runs: the shell closes its bash, the sandbox discards its container, the SQLite store closes its handle.
- .step(what) ⇒ Object
Instance Method Summary collapse
- #boot! ⇒ Object
- #constantize(name, id) ⇒ Object
-
#initialize(resolved, allow_config_ruby: false) ⇒ Boot
constructor
A new instance of Boot.
-
#loader ⇒ Object
The loader rather than the context, for a caller that wants reconfigure!/unload! on the composition it just booted.
-
#require_code! ⇒ Object
A bundle's requires first, then the profile's own
plugins:— code that is not a bundle loads last so it can reopen what a bundle defined. - #rows ⇒ Object
Constructor Details
#initialize(resolved, allow_config_ruby: false) ⇒ Boot
Returns a new instance of Boot.
44 45 46 47 |
# File 'lib/terret/boot.rb', line 44 def initialize(resolved, allow_config_ruby: false) @resolved = resolved @allow_config_ruby = allow_config_ruby end |
Instance Attribute Details
#allow_config_ruby ⇒ Object (readonly)
Returns the value of attribute allow_config_ruby.
42 43 44 |
# File 'lib/terret/boot.rb', line 42 def allow_config_ruby @allow_config_ruby end |
#resolved ⇒ Object (readonly)
Returns the value of attribute resolved.
42 43 44 |
# File 'lib/terret/boot.rb', line 42 def resolved @resolved end |
Class Method Details
.shutdown(ctx, loader: nil) ⇒ Object
Take a booted context down through the loader, so every row's own stop hook runs: the shell closes its bash, the sandbox discards its container, the SQLite store closes its handle. A hand-written list of seams runs none of the hooks it does not happen to name, and grows a hole every time a bundle mounts something new.
Reverse MOUNT order, which is exact reverse-dependency order: a consumer comes down before the service it injects, and the session store — which everything else may still be writing to — closes last.
Best-effort means each step is separately best-effort. One wedged seam aborting the rest is how a container survives the process that owned it.
A context built by hand rather than by Terret.boot has no loader to find,
and this says so rather than quietly disposing and running no hooks at
all — pass loader: to get the real teardown.
132 133 134 135 136 137 138 139 140 141 |
# File 'lib/terret/boot.rb', line 132 def self.shutdown(ctx, loader: nil) loader ||= ctx[:loader] if ctx.service?(:loader) if loader loader.mounted.keys.reverse_each { |id| step("row #{id}") { loader.unload!(id) } } else warn "terret: shutdown: no loader for this context, so no service's stop hook ran. " \ "Boot through Terret.boot, or pass shutdown(ctx, loader:) — disposing registrations only." end step("dispose") { ctx.dispose! } end |
.step(what) ⇒ Object
143 144 145 146 147 |
# File 'lib/terret/boot.rb', line 143 def self.step(what) yield rescue StandardError => e warn "terret: shutdown: #{what}: #{e.class}: #{e.}" end |
Instance Method Details
#boot! ⇒ Object
49 |
# File 'lib/terret/boot.rb', line 49 def boot! = loader.boot! |
#constantize(name, id) ⇒ Object
101 102 103 104 105 106 107 108 109 110 111 112 113 114 |
# File 'lib/terret/boot.rb', line 101 def constantize(name, id) klass = Object.const_get(name) unless klass.is_a?(Class) && klass.method_defined?(:apply) raise Error, "row #{id.inspect}: #{name} is not a plugin — a plugin is a " \ "class whose instances respond to #apply(ctx), which is what " \ "subclassing Hames::Service gives you" end klass rescue NameError raise Error, "row #{id.inspect}: no such plugin #{name}. The constant did not " \ "resolve, so either the name is wrong or the gem that defines it " \ "is not required by any bundle in this profile." end |
#loader ⇒ Object
The loader rather than the context, for a caller that wants
reconfigure!/unload! on the composition it just booted. Memoized, because
a second loader would be a second context: b.loader then b.boot!
would hand back two unrelated worlds built from the same rows.
55 56 57 58 59 60 61 62 63 64 65 66 |
# File 'lib/terret/boot.rb', line 55 def loader @loader ||= begin require_code! built = Hames::Loader.new.layer(rows) # Reachable from the context it boots. Terret.boot returns the ctx and # nothing else, so without this a caller holding one has no way to # reconfigure a row, unload one, or shut the composition down through # the services' own stop hooks. built.ctx.register_service(:loader, built) built end end |
#require_code! ⇒ Object
A bundle's requires first, then the profile's own plugins: — code that
is not a bundle loads last so it can reopen what a bundle defined.
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 |
# File 'lib/terret/boot.rb', line 75 def require_code! # A bundle's requires: ship inside a gem the operator added to the Gemfile # and installed, so they are trusted and may name a path to their own lib. # A profile's plugins: is portable config a profile "downloaded from # anywhere" (docs/composition.md §5) carries, so a path-shaped one there is # arbitrary code execution — gated behind the same --allow-config-ruby # consent as !ruby. Validate up front so a bad path fails before any # require has run a line of code. resolved.plugins.each do |file| next if allow_config_ruby || Composition.load_path_feature?(file) raise Error, "profile #{resolved.profile.inspect} refuses to require #{file.inspect}: " \ "it is a filesystem path, not a load-path feature name. Loading Ruby by " \ "path is arbitrary code execution; pass --allow-config-ruby to permit it " \ "(docs/composition.md §5, docs/security.md)." end (resolved.requires + resolved.plugins).each do |file| require file rescue LoadError => e raise Error, "profile #{resolved.profile.inspect} needs #{file.inspect}, " \ "which is not on the load path (#{e.}). Is the gem " \ "that ships it in your Gemfile?" end end |
#rows ⇒ Object
68 69 70 71 |
# File 'lib/terret/boot.rb', line 68 def rows @rows ||= resolved.materialize(allow_config_ruby: allow_config_ruby) .map { |row| row.merge(plugin: constantize(row[:plugin], row[:id])) } end |