Class: WineOLE::Client

Inherits:
Object
  • Object
show all
Defined in:
lib/wineole/client.rb

Constant Summary collapse

WINDOWS =
!(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).nil?
ARCH_TRIPLES =
{
  'x86_64' => 'x86_64-pc-windows-gnu',
  'i386' => 'i686-pc-windows-gnu',
  'i686' => 'i686-pc-windows-gnu',
}.freeze
DEFAULT_SPAWNER =
lambda do |port|
  # wineole-bridge.exe is a native Windows binary either way -- `wine`
  # is only needed to run it on a non-Windows host. On Windows itself
  # there is no `wine` command, so running it under one would fail
  # immediately (Errno::ENOENT) rather than run natively as it should.
  # The null device path is also platform-specific ('/dev/null' does
  # not exist on Windows).
  command = WINDOWS ? [default_bridge_path] : ['wine', default_bridge_path]
  null_device = WINDOWS ? 'NUL' : '/dev/null'
  Process.spawn(*command, port.to_s, %i[out err] => null_device)
end

Class Method Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(socket) ⇒ Client

Returns a new instance of Client.



115
116
117
118
119
120
# File 'lib/wineole/client.rb', line 115

def initialize(socket)
  @socket = socket
  @next_id = 0
  @mutex = Mutex.new
  ObjectSpace.define_finalizer(self, self.class.finalizer(socket))
end

Class Method Details

.bridge_path_for_arch(host_cpu) ⇒ Object



18
19
20
21
22
23
24
25
# File 'lib/wineole/client.rb', line 18

def self.bridge_path_for_arch(host_cpu)
  triple = ARCH_TRIPLES[host_cpu] || (ARCH_TRIPLES['i686'] if host_cpu =~ /^i.86$/)
  if triple.nil?
    raise Error, "no prebuilt wineole-bridge binary for host architecture #{host_cpu.inspect} " \
                  "(available: #{ARCH_TRIPLES.values.uniq.join(', ')})"
  end
  File.expand_path("../../wineole-bridge-dist/#{triple}/wineole-bridge.exe", __dir__)
end

.default_bridge_pathObject



27
28
29
# File 'lib/wineole/client.rb', line 27

def self.default_bridge_path
  bridge_path_for_arch(RbConfig::CONFIG['host_cpu'])
end

.default_lockfile(port) ⇒ Object



43
44
45
46
47
48
49
50
# File 'lib/wineole/client.rb', line 43

def self.default_lockfile(port)
  # The lock is per-port, not global: two bridges on two ports are
  # entirely independent, and a single shared lock would make a client
  # starting one wait on a client starting the other (design doc ยง7.1
  # step 2). Dir.tmpdir rather than a hardcoded '/tmp' resolves
  # correctly on Windows (%TEMP%) as well as every POSIX platform.
  File.join(Dir.tmpdir, "wineole-bridge.#{port}.lock")
end

.finalizer(socket) ⇒ Object

A proc to close over the raw socket for ObjectSpace.define_finalizer. It deliberately does NOT close over self (the Client instance) -- capturing the instance being finalized would keep it permanently reachable through ObjectSpace's own finalizer table, and it could never actually be collected. Capturing only the socket avoids this.



105
106
107
108
109
110
111
112
113
# File 'lib/wineole/client.rb', line 105

def self.finalizer(socket)
  proc do
    begin
      socket.close
    rescue StandardError
      nil
    end
  end
end

.open(host: '127.0.0.1', port: 47800, spawner: DEFAULT_SPAWNER, lockfile: nil, timeout: 15, token: nil) ⇒ Object



52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
# File 'lib/wineole/client.rb', line 52

def self.open(host: '127.0.0.1', port: 47800, spawner: DEFAULT_SPAWNER,
               lockfile: nil, timeout: 15, token: nil)
  lockfile ||= default_lockfile(port)

  socket = try_connect(host, port)
  return handshake(new(socket), token) if socket

  File.open(lockfile, File::CREAT | File::RDWR, 0o644) do |lock|
    lock.flock(File::LOCK_EX)

    socket = try_connect(host, port)
    return handshake(new(socket), token) if socket

    spawner.call(port)
    deadline = Time.now + timeout
    loop do
      socket = try_connect(host, port)
      return handshake(new(socket), token) if socket
      raise Error, "wineole-bridge did not start within #{timeout}s" if Time.now > deadline
      sleep 0.2
    end
  end
end

Instance Method Details

#call(method, params = {}) ⇒ Object



122
123
124
125
126
127
128
129
130
131
132
133
134
135
# File 'lib/wineole/client.rb', line 122

def call(method, params = {})
  @mutex.synchronize do
    id = (@next_id += 1)
    @socket.write(JSON.generate({id: id, method: method, params: params}) + "\n")
    line = @socket.gets
    raise ProtocolError, 'connection closed' if line.nil?
    response = JSON.parse(line)
    unless response['id'] == id
      raise ProtocolError, "id mismatch: expected #{id}, got #{response['id']}"
    end
    raise RemoteError.new(response['error']['class'], response['error']['message']) if response['error']
    response['result']
  end
end

#closeObject



137
138
139
# File 'lib/wineole/client.rb', line 137

def close
  @socket.close
end

#connect(class_name) ⇒ Object



145
146
147
# File 'lib/wineole/client.rb', line 145

def connect(class_name)
  Proxy.connect(class_name, self)
end

#connect_or_create(class_name) ⇒ Object



149
150
151
# File 'lib/wineole/client.rb', line 149

def connect_or_create(class_name)
  Proxy.connect_or_create(class_name, self)
end

#create(class_name) ⇒ Object



141
142
143
# File 'lib/wineole/client.rb', line 141

def create(class_name)
  Proxy.create(class_name, self)
end