Class: Terret::Boot

Inherits:
Object
  • Object
show all
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

Class Method Summary collapse

Instance Method Summary collapse

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_rubyObject (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

#resolvedObject (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.message}"
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

#loaderObject

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.message}). Is the gem " \
                 "that ships it in your Gemfile?"
  end
end

#rowsObject



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