Class: Canopus::Plugins::Registry

Inherits:
Object
  • Object
show all
Defined in:
lib/canopus/plugins/registry.rb,
sig/canopus.rbs

Constant Summary collapse

API_VERSION =
"1"
NAME_PATTERN =
/\Acanopus-plugin-[a-z0-9][a-z0-9-]*\z/
PERMISSIONS =

Returns:

  • (Array[String])
Plugins::PERMISSIONS

Instance Method Summary collapse

Constructor Details

#initialize(workspace, state_path: nil) ⇒ Registry

Returns a new instance of Registry.

Parameters:

  • workspace (Workspace)
  • state_path: (String, nil) (defaults to: nil)


14
15
16
17
18
# File 'lib/canopus/plugins/registry.rb', line 14

def initialize(workspace, state_path: nil)
  @workspace, @loaded = workspace, []
  @state_path = state_path || File.join(ENV["XDG_STATE_HOME"] || File.expand_path("~/.local/state"), "canopus", "plugins.json")
  @disabled = load_state
end

Instance Method Details

#close ⇒ void

This method returns an undefined value.



97
# File 'lib/canopus/plugins/registry.rb', line 97

def close = @loaded.each(&:close)

#disable(name) ⇒ true

Parameters:

  • name (String)

Returns:

  • (true)


52
53
54
55
56
57
# File 'lib/canopus/plugins/registry.rb', line 52

def disable(name)
  name = validate_name(name)
  @disabled[name] = true
  save_state
  true
end

#disabled?(name) ⇒ Boolean

Parameters:

  • name (String)

Returns:

  • (Boolean)


66
# File 'lib/canopus/plugins/registry.rb', line 66

def disabled?(name) = @disabled.key?(validate_name(name))

#enable(name) ⇒ true

Parameters:

  • name (String)

Returns:

  • (true)


59
60
61
62
63
64
# File 'lib/canopus/plugins/registry.rb', line 59

def enable(name)
  name = validate_name(name)
  @disabled.delete(name)
  save_state
  true
end

#install(name, version: nil) ⇒ Object

Parameters:

  • name (String)
  • version: (Object) (defaults to: nil)

Returns:

  • (Object)


37
38
39
40
41
42
43
44
45
46
# File 'lib/canopus/plugins/registry.rb', line 37

def install(name, version: nil)
  require "rubygems/dependency_installer"
  name = validate_name(name)
  specifications = Gem::DependencyInstaller.new.install(name, version || Gem::Requirement.default)
  specification = specifications.reverse.find { |candidate| candidate.name == name }
  validate_specification!(specification)
  specification
rescue Gem::Exception => error
  raise Canopus::Error, "plugin install failed: #{error.message}"
end

#load(path, trusted: false, permissions: [], isolated: true, timeout: 2) ⇒ Object

Parameters:

  • path (String)
  • isolated: (Boolean) (defaults to: true)
  • permissions: (Array[String]) (defaults to: [])
  • trusted: (Boolean) (defaults to: false)
  • timeout: (Numeric) (defaults to: 2)

Returns:

  • (Object)

Raises:



82
83
84
85
86
87
88
89
90
91
92
93
94
95
# File 'lib/canopus/plugins/registry.rb', line 82

def load(path, trusted: false, permissions: [], isolated: true, timeout: 2)
  raise PermissionDenied, "plugins execute Ruby code; explicitly mark this plugin trusted" unless trusted
  permissions = permissions.map { |permission| permission.to_s == "process" ? "exec" : permission.to_s }.uniq
  raise PermissionDenied, "unknown plugin permission" unless (permissions - PERMISSIONS).empty?
  source = File.read(path, 256 * 1024 + 1, encoding: "UTF-8")
  raise Error, "plugin source exceeds 256KB" if source.bytesize > 256 * 1024
  plugin = if isolated
    IsolatedRuntime.new(@workspace, source, path, permissions, timeout: timeout)
  else
    LocalRuntime.new(@workspace, source, path, permissions)
  end
  @loaded << plugin
  plugin
end

#load_gem(name, trusted: false, permissions: [], isolated: true, timeout: 2) ⇒ Object

Parameters:

  • name (String)
  • isolated: (Boolean) (defaults to: true)
  • permissions: (Array[String]) (defaults to: [])
  • trusted: (Boolean) (defaults to: false)
  • timeout: (Numeric) (defaults to: 2)

Returns:

  • (Object)


68
69
70
71
72
73
74
75
76
77
78
79
80
# File 'lib/canopus/plugins/registry.rb', line 68

def load_gem(name, trusted: false, permissions: [], isolated: true, timeout: 2)
  name = validate_name(name)
  raise Canopus::Error, "plugin is disabled: #{name}" if disabled?(name)
  specification = validate_specification!(Gem::Specification.find_all_by_name(name).max_by(&:version))
  entrypoint = specification.["canopus_plugin_entrypoint"] || "lib/#{name}.rb"
  path = File.realpath(File.expand_path(entrypoint, specification.full_gem_path))
  prefix = File.realpath(specification.full_gem_path) + File::SEPARATOR
  raise Canopus::Error, "plugin entrypoint escapes gem directory" unless path.start_with?(prefix)

  load(path, trusted: trusted, permissions: permissions, isolated: isolated, timeout: timeout)
rescue Errno::ENOENT
  raise Canopus::Error, "plugin entrypoint not found: #{name}"
end

#search(query, limit: 20) ⇒ Array[Hash[Symbol, untyped]]

Parameters:

  • query (String)
  • limit: (Integer) (defaults to: 20)

Returns:

  • (Array[Hash[Symbol, untyped]])

Raises:

  • (ArgumentError)


20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
# File 'lib/canopus/plugins/registry.rb', line 20

def search(query, limit: 20)
  require "rubygems/spec_fetcher"
  query = String(query)
  raise ArgumentError, "plugin search query must not be empty" if query.strip.empty?
  raise ArgumentError, "invalid plugin search limit" unless limit.is_a?(Integer) && limit.between?(1, 100)

  dependency = Gem::Dependency.new(query.start_with?("canopus-plugin-") ? query : "canopus-plugin-#{query}", Gem::Requirement.default)
  Gem::SpecFetcher.fetcher.search_for_dependency(dependency).first.filter_map do |entry, _source|
    spec = entry.is_a?(Array) ? entry.first : entry
    spec = spec.spec if spec.respond_to?(:spec)
    name = spec.respond_to?(:name) ? spec.name : nil
    next unless plugin_name?(name)
    {name: name, version: spec.respond_to?(:version) ? spec.version.to_s : "", summary: spec.respond_to?(:summary) ? spec.summary.to_s : "",
     api_version: spec.respond_to?(:metadata) ? spec.["canopus_plugin_api_version"] : nil}
  end.uniq { |plugin| plugin[:name] }.first(limit)
end

#update(name) ⇒ Object

Parameters:

  • name (String)

Returns:

  • (Object)


48
49
50
# File 'lib/canopus/plugins/registry.rb', line 48

def update(name)
  install(name)
end