Class: Canopus::Plugins::IsolatedRuntime
- Inherits:
-
Object
- Object
- Canopus::Plugins::IsolatedRuntime
- Defined in:
- lib/canopus/plugins/isolated_runtime.rb
Constant Summary collapse
- WORKER =
<<~'RUBY' require "json" require "stringio" require "open3" protocol = STDOUT $stdout = StringIO.new class PluginAPI attr_reader :edits, :messages def initialize(context, permissions) @context, @permissions, @edits, @messages = context, permissions, [], [] end def permit!(name) normalized = name.to_s == "process" ? "exec" : name.to_s raise "plugin requires #{name}" unless @permissions.include?(normalized) end def text permit!("read_buffer") @context.fetch("text") { raise @context.fetch("text_error", "buffer text is not available") } end def files permit!("read_project") @context.fetch("files") end def replace(range, text) permit!("edit_buffer") @edits << [range.begin, range.end + (range.exclude_end? ? 0 : 1), text] end def notify(message) = @messages << message.to_s def run(command) permit!("exec") raise "command must be an argument array" unless command.is_a?(Array) && !command.empty? Open3.capture3(*command, chdir: @context.fetch("root")) end def http_get(url) permit!("network") require "net/http" uri = URI(url) raise "HTTP or HTTPS URL required" unless %w[http https].include?(uri.scheme) body = +"" Net::HTTP.start(uri.host, uri.port, use_ssl: uri.scheme == "https", open_timeout: 5, read_timeout: 5) do |http| http.request_get(uri.request_uri) do |response| response.read_body do |chunk| raise "plugin HTTP response exceeds 1 MiB" if body.bytesize + chunk.bytesize > 1 << 20 body << chunk end end end body end end class PluginDefinition attr_reader :actions, :panels, :languages, :servers def initialize @actions, @panels, @languages, @servers = {}, {}, {}, {} end def register_action(name, description: name, &block) = @actions[name] = [description, block] def register_panel(name, side: :right, &block) = @panels[name] = [side, block] def register_language(name, **options) = @languages[name] = options def configure_lsp(language, command) = @servers[language] = command end begin bootstrap = JSON.parse(STDIN.gets || raise("missing plugin source")) definition = PluginDefinition.new definition.instance_eval(bootstrap.fetch("source"), bootstrap.fetch("path"), 1) protocol.puts(JSON.generate(actions: definition.actions.transform_values(&:first), panels: definition.panels.transform_values(&:first), languages: definition.languages, servers: definition.servers)) protocol.flush STDIN.each_line do |line| begin request = JSON.parse(line) api = PluginAPI.new(request.fetch("context"), bootstrap.fetch("permissions")) table = request["kind"] == "panel" ? definition.panels : definition.actions result = table.fetch(request.fetch("name"))[1].call(api) protocol.puts(JSON.generate(result: result.is_a?(String) ? result : nil, edits: api.edits, messages: api.messages)) rescue StandardError, ScriptError => error protocol.puts(JSON.generate(error: "#{error.class}: #{error.message}")) end protocol.flush $stdout.truncate(0) $stdout.rewind end rescue StandardError, ScriptError => error protocol.puts(JSON.generate(error: "#{error.class}: #{error.message}")) protocol.flush end RUBY
Instance Method Summary collapse
- #close ⇒ Object
-
#initialize(workspace, source, path, permissions, timeout: 2) ⇒ IsolatedRuntime
constructor
A new instance of IsolatedRuntime.
- #invoke(kind, name) ⇒ Object
Constructor Details
#initialize(workspace, source, path, permissions, timeout: 2) ⇒ IsolatedRuntime
Returns a new instance of IsolatedRuntime.
98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 |
# File 'lib/canopus/plugins/isolated_runtime.rb', line 98 def initialize(workspace, source, path, , timeout: 2) @workspace, @plugin_id = workspace, path @permissions, @timeout = .map { || .to_s == "process" ? "exec" : .to_s }.uniq, timeout @panel_lock = Mutex.new @panel_cache, @panel_refreshed, @panel_refreshing, @panel_threads = {}, {}, {}, {} @closed = false start_process @input.sync = true @output.binmode @pending, @lock = +"".b, Mutex.new @input.puts(JSON.generate(source: source, path: path, permissions: @permissions)) manifest = response servers = manifest.fetch("servers") if !servers.empty? && !@permissions.include?("exec") raise Canopus::Plugins::PermissionDenied, "plugin requires process to configure language servers" end manifest.fetch("actions").each do |name, description| workspace.register_action(name, description: description) { invoke(:action, name) } end manifest.fetch("panels").each do |name, side| workspace.register_panel(name, side: side.to_sym, cache: false) { panel_element(name) } end @panel_names = manifest.fetch("panels").keys manifest.fetch("languages").each { |name, | workspace.register_language(name, **.transform_keys(&:to_sym)) } servers.each { |language, command| workspace.configure_plugin_language_server(@plugin_id, language, command) } rescue StandardError close raise end |
Instance Method Details
#close ⇒ Object
156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 |
# File 'lib/canopus/plugins/isolated_runtime.rb', line 156 def close threads = @panel_lock.synchronize do @closed = true @panel_threads.keys end threads.each do |thread| next if thread.equal?(Thread.current) thread.kill thread.join(0.2) end @input&.close unless @input&.closed? if @process && !@process.join(0.2) Process.kill("KILL", @pid) rescue Errno::ESRCH @process.join end @output&.close unless @output&.closed? end |
#invoke(kind, name) ⇒ Object
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 |
# File 'lib/canopus/plugins/isolated_runtime.rb', line 127 def invoke(kind, name) @lock.synchronize do raise Canopus::Error, "plugin runtime is closed" if @closed buffer = @workspace.editor&.buffer version = buffer&.version context = {root: @workspace.root} if @permissions.include?("read_buffer") if buffer && buffer.rope.bytesize <= Plugins::BUFFER_CONTEXT_LIMIT context[:text] = buffer.text else context[:text_error] = "plugin buffer text exceeds 1 MiB" end end context[:files] = @workspace.files if @permissions.include?("read_project") @input.puts(JSON.generate(kind: kind, name: name, context: context)) result = response edits = result.fetch("edits") unless edits.empty? raise Canopus::Plugins::PermissionDenied, "plugin requires edit_buffer" unless @permissions.include?("edit_buffer") raise Canopus::Error, "buffer changed while plugin was running" unless buffer && buffer.version == version buffer.edit(edits.map { |first, last, text| [first...last, text] }, kind: :plugin) end @workspace. = result.fetch("messages").last.to_s unless result.fetch("messages").empty? refresh_panels if kind == :action result["result"] end rescue IOError, Errno::EPIPE, EOFError => error raise Canopus::Error, "plugin process ended: #{error.}" end |