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

new(path = nil, version_check: /./, system: "UMI", log: nil, convert_case: true, factory: nil) click to toggle source

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
session_log_path=(path) click to toggle source

Set the detault UMI session log path (including file) that will be used if no log parameter will be passed to the UMI constructor

# File lib/universa/umi.rb, line 56
def self.session_log_path= path
  @@session_log_path = path
end

Public Instance Methods

close() click to toggle source

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
core_version() click to toggle source

@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
find_by_remote_id(remote_id) click to toggle source

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
get_field(remote_object, name) click to toggle source
# File lib/universa/umi.rb, line 130
def get_field(remote_object, name)
  encode_result call("get_field", remote_object._remote_id, name)
end
inspect() click to toggle source

short data label for UMI interface

# File lib/universa/umi.rb, line 155
def inspect
  "<UMI:#{__id__}:#{version}>"
end
instantiate(object_class_name, *args, adapter: nil) click to toggle source

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(ref, method, *args) click to toggle source

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
invoke_static(class_name, method, *args) click to toggle source
# 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
set_field(remote_object, name, value) click to toggle source
# 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
version() click to toggle source

@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
with_trace(&block) click to toggle source

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

build_reference(reference_record, proxy) click to toggle source

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
call(command, *args) click to toggle source

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_finalizer(remote_id) click to toggle source

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_reference(reference_record, adapter = nil) click to toggle source

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
encode_result(value) click to toggle source

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
ensure_open() click to toggle source

@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
log(msg) click to toggle source
# File lib/universa/umi.rb, line 359
def log msg
  @trace and puts "UMI #{msg}"
end
prepare(x) click to toggle source

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
prepare_args(args) click to toggle source

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_cleanup_queue() click to toggle source

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