class Universa::UMI
Universa
Method Invocation remote interface.
By default, it creates UMI
interface to the included UMI
server which gives almost full access to the Universa
Java API:
Uasge:
>> umi = Universa::UMI.new() >> # create a new key and new contract with this key as creator: >> contract = umi.instantiate "Contract", umi.instantiate("PrivateKey", 2048)
Use {#instantiate} to create new instances of remote classes, which return {Ref} instances, and just call their methods as if these are usual ruby methods. For example in the example above:
address = contract.getKeysToSignWith()[0].getPublicKey().getShortAddress().toString()
In the sample above all the methods are called on the remote side, returning links to remote objects which are all {Ref} instances, and the last `toString()` call return a string, which is converted to ruby string and saved into variable. This sentence, therefore, get the first signer key and transofrms it to the string short address.
Having several `UMI` interfaces.¶ ↑
It is possible to have several UMI
instances, by default, it will create separate process with isolated data space, which is perfectly safe to use in various scenarios.
It still means the object from different interfaces can't be interchanged. {Ref} instances created by one interface should be used with this interface only, or the {InterchangeError} will be raised.
Remote exceptions¶ ↑
If remote part will thow an Exception performing a method, it will be raised as an instance of {www.rubydoc.info/gems/farcall/Farcall/RemoteError Farcall::RemoteError} class which carries remote exception information.
Transport level¶ ↑
UMI
uses {github.com/sergeych/farcall/wiki Farcall} transport in woth JSON adapter and ānā as separator.
Constants
- EMPTY_KWARGS
Public Class Methods
Create UMI
instance. It starts the private child process wit UMI
server and securely connects to it so no other connection could occur.
# create UNI interface umi = Universa::UMI.new() # create a new key and new contract with this key as creator: contract = umi.instantiate "Contract", umi.instantiate("PrivateKey", 2048) contract.seal() # binary packed string returned contract.check() #=> true
@param [String] path to custom UMI
server build. Use bundled one (leave as nil) @param [Regexp] version_check check version against @param [String] system expected on the remote side. 'UMI' us a universa umi server. @param [Boolean] convert_case it true, convert ruby style snake case `get_some_stuff()` to java style lower camel
case `getSomeStuff()` while calling methods. Does not affect class names on {instantiate}.
# File lib/universa/umi.rb, line 76 def initialize(path = nil, version_check: /./, system: "UMI", log: nil, convert_case: true, factory: nil) log ||= @@session_log_path path ||= File.expand_path(File.split(__FILE__)[0] + "/../../bin/umi/bin/umi") @in, @out, @err, @wtr = Open3.popen3("#{path} #{log ? "-log #{log}" : ''}") @endpoint = Farcall::Endpoint.new( Farcall::JsonTransport.create(delimiter: "\n", input: @out, output: @in) ) @lock = Monitor.new @cache = {} @closed = false @convert_case, @factory = convert_case, factory @references = {} start_cleanup_queue @version = call("version") raise Error, "Unsupported system: #{@version}" if @version.system != "UMI" raise Error, "Unsupported version: #{@version}" if @version.version !~ /0\.8\.\d+/ rescue Errno::ENOENT @err and STDERR.puts @err.read raise Error, "missing java binaries" end
Public Instance Methods
Close child process. No remote calls should occur after it.
# File lib/universa/umi.rb, line 144 def close @queue.push :poison_pill @cleanup_thread.join @closed = true @endpoint.close @in.close @out.close @wtr.value.exited? end
@return Universa
network library core version
# File lib/universa/umi.rb, line 103 def core_version @core_version ||= begin invoke_static "Core", "getVersion" end end
debug use only. Looks for the cached e.g. (alive) remote object. Does not check the remote side.
# File lib/universa/umi.rb, line 161 def find_by_remote_id remote_id @lock.synchronize {@cache[remote_id]&.get} end
# File lib/universa/umi.rb, line 130 def get_field(remote_object, name) encode_result call("get_field", remote_object._remote_id, name) end
short data label for UMI
interface
# File lib/universa/umi.rb, line 155 def inspect "<UMI:#{__id__}:#{version}>" end
Create instance of some Universa
Java API class, for example 'Contract', passing any arguments to its constructor. The returned reference could be used much like local instance, nu the actual work will happen in the child process. Use references as much as possible as they take all the housekeeping required, like memory leaks prevention and direct method calling.
@return [Ref] reference to the remotely created object. See {Ref}.
# File lib/universa/umi.rb, line 115 def instantiate(object_class_name, *args, adapter: nil) ensure_open create_reference call("instantiate", object_class_name, *prepare_args(args)), adapter end
Invoke method by name. Should not be used directly; use {Ref} instance to call its methods.
# File lib/universa/umi.rb, line 121 def invoke(ref, method, *args) ensure_open ref._umi == self or raise InterchangeError @convert_case and method = method.to_s.camelize_lower # p ["invoke", ref._remote_id, method, *prepare_args(args)] result = call("invoke", ref._remote_id, method, *prepare_args(args)) encode_result result end
# File lib/universa/umi.rb, line 139 def invoke_static(class_name, method, *args) encode_result call("invoke", class_name, method.to_s.camelize_lower, *prepare_args(args)) end
# File lib/universa/umi.rb, line 134 def set_field(remote_object, name, value) call("set_field", remote_object._remote_id, name, prepare(value)) value end
@return version of the connected UMI
server. It is different from the gem version.
# File lib/universa/umi.rb, line 98 def version @version.version end
Execute the block with trace mode on. Will spam the output with protocol information. These calls could be nested, on exit it restores previous trace state
# File lib/universa/umi.rb, line 167 def with_trace &block current_state, @trace = @trace, true result = block.call() @trace = current_state result end
Private Instance Methods
Create a reference from UMI
remote object reference structure. Returns existing object if any. Takes care of dropping remote object when ruby object gets collected.
# File lib/universa/umi.rb, line 209 def build_reference(reference_record, proxy) @lock.synchronize { remote_id = reference_record.id ref = @cache[remote_id]&.get if !ref # log "Creating new reference to remote #{remote_id}" ref = Ref.new(self, reference_record) # IF we provide proxy that will consume the ref, we'll cache the proxy object, # otherwise we run factory and cahce whatever it returns or the ref itself obj = if proxy # Proxy object will delegate the ref we return from there # no action need proxy else # new object: factory may create proxy for us and we'll cache it for later # use: @factory and ref = @factory.call(ref) ref end # Important: we set finalizer fot the target object ObjectSpace.define_finalizer(obj, create_finalizer(remote_id)) # and we cache target object @cache[remote_id] = WeakReference.new(obj) end # but we return reference: it the proxy constructor calls us, it'll need the ref: ref } end
perform UMI
remote call
# File lib/universa/umi.rb, line 337 def call(command, *args) log ">> #{command}(#{args})" mx = Mutex.new cv = ConditionVariable.new() error, result = nil, nil mx.synchronize { @endpoint.call(command, *args, **EMPTY_KWARGS) { |_error, _result| error, result = _error, _result mx.synchronize{ cv.signal } } cv.wait(mx) } if error log "<<**ERROR #{error}" raise NoMethodError, error.message if (cls = error[:class]) == 'NoSuchMethodException' raise Farcall::RemoteError.new(cls, error.text) else log "<< #{result}" result end end
create a finalizer that will drop remote object
# File lib/universa/umi.rb, line 177 def create_finalizer(remote_id) -> (id) { begin @lock.synchronize { @cache.delete(remote_id) # log "=== removing remote ref #{id} -> #{remote_id}" @queue.push(remote_id) } rescue ThreadError # can't be called from trap contect - life is life ;) # silently ignore rescue $!.print_stack_trace end } end
Create a reference correcting adapting remote types to ruby ecosystem, for example loads remote Java Set to a local ruby Set.
# File lib/universa/umi.rb, line 196 def create_reference(reference_record, adapter = nil) r = build_reference reference_record, adapter return r if adapter case reference_record.className when 'java.util.HashSet' r.toArray() else r end end
Convert remote call result from UMI
structures to ruby types
# File lib/universa/umi.rb, line 307 def encode_result value case value when Hash type = value.__type case type when 'RemoteObject'; create_reference value when 'binary'; Base64.decode64(value.base64) when 'unixtime'; Time.at(value.seconds) else # Deep hash conversion value.transform_values! {|v| encode_result(v)} end when Hashie::Array value.map {|x| encode_result x} else value end end
@raise Error
if interface is closed
# File lib/universa/umi.rb, line 330 def ensure_open raise Error, "UMI interface is closed" if @closed end
# File lib/universa/umi.rb, line 359 def log msg @trace and puts "UMI #{msg}" end
convert single argument to UMI
value to pass
# File lib/universa/umi.rb, line 267 def prepare(x) # p [:pre, x] if x.respond_to?(:_as_umi_arg) # p ["uniarg"] x._as_umi_arg(self) else case x when Array # deep convert all array items x.map {|a| prepare a} when Set # Make a Java Set r = call("instantiate", "Set", x.to_a.map {|i| i._as_umi_arg(self)}) # Ref will garbage collect it Ref.new(self, r) # but we need a ref struct only: r when Time {__type: 'unixtime', seconds: x.to_i} when String x.encoding == Encoding::BINARY ? {__type: 'binary', base64: Base64.encode64(x)} : x when Ref # p [:ref] x._as_umi_arg(self) when RemoteAdapter # p [:ra, x.__getobj__._as_umi_arg(self)] # this need special treatment with direct call: x.__getobj__._as_umi_arg(self) when Hash # p [:hash] result = {} x.each {|k, v| result[k] = prepare(v)} result else x end end end
convert ruby arguments array to corresponding UMI
values
# File lib/universa/umi.rb, line 261 def prepare_args args raise "pp bug" if args == [:pretty_print] # this often happens while tracing args.map {|x| prepare x} end
Start the remote object drop queue processing.
# File lib/universa/umi.rb, line 239 def start_cleanup_queue return if @queue @queue = Queue.new @cleanup_thread = Thread.start { while (!@closed) id = @queue.pop() if id == :poison_pill # log "leaving cleanup queue" break else begin call("drop_objects", id) # log "remote object dropped: #{id}" rescue $!.print_stack_trace end end end } end