wineole 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/lib/wineole/client.rb +153 -0
- data/lib/wineole/errors.rb +15 -0
- data/lib/wineole/proxy.rb +148 -0
- data/lib/wineole.rb +57 -0
- data/wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe +0 -0
- data/wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe +0 -0
- metadata +52 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 6bdd93426e11cfe60fca09d51d9f12e32c5ea984767e18ec271a2ad6a471aba9
|
|
4
|
+
data.tar.gz: a04c2be3c95c7283e06327bea2b4e7725f4794351ab21439a8593fc0c4798693
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 3cdd18036f72e3bd4d149c0fa71e9aab446cafa325936711bc2e5049584000431b9a224cd3c12cf508b6ac6d8c6f536039d4d86ea0adb94711dfc1f6f3e51dd2
|
|
7
|
+
data.tar.gz: 149e5ffe74bab247cf3849137956b425424a75598e2c6d24c54b36ecdabb6a2b8462eb58e12a73e75c7f7d3ae9da1ba170b1192c6249be00829bf4fbd29806d0
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 firelzrd
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
require 'socket'
|
|
2
|
+
require 'json'
|
|
3
|
+
require 'rbconfig'
|
|
4
|
+
require 'tmpdir'
|
|
5
|
+
require_relative 'errors'
|
|
6
|
+
require_relative 'proxy'
|
|
7
|
+
|
|
8
|
+
module WineOLE
|
|
9
|
+
class Client
|
|
10
|
+
WINDOWS = !(RbConfig::CONFIG['host_os'] =~ /mswin|mingw|cygwin/).nil?
|
|
11
|
+
|
|
12
|
+
ARCH_TRIPLES = {
|
|
13
|
+
'x86_64' => 'x86_64-pc-windows-gnu',
|
|
14
|
+
'i386' => 'i686-pc-windows-gnu',
|
|
15
|
+
'i686' => 'i686-pc-windows-gnu',
|
|
16
|
+
}.freeze
|
|
17
|
+
|
|
18
|
+
def self.bridge_path_for_arch(host_cpu)
|
|
19
|
+
triple = ARCH_TRIPLES[host_cpu] || (ARCH_TRIPLES['i686'] if host_cpu =~ /^i.86$/)
|
|
20
|
+
if triple.nil?
|
|
21
|
+
raise Error, "no prebuilt wineole-bridge binary for host architecture #{host_cpu.inspect} " \
|
|
22
|
+
"(available: #{ARCH_TRIPLES.values.uniq.join(', ')})"
|
|
23
|
+
end
|
|
24
|
+
File.expand_path("../../wineole-bridge-dist/#{triple}/wineole-bridge.exe", __dir__)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.default_bridge_path
|
|
28
|
+
bridge_path_for_arch(RbConfig::CONFIG['host_cpu'])
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
DEFAULT_SPAWNER = lambda do |port|
|
|
32
|
+
# wineole-bridge.exe is a native Windows binary either way -- `wine`
|
|
33
|
+
# is only needed to run it on a non-Windows host. On Windows itself
|
|
34
|
+
# there is no `wine` command, so running it under one would fail
|
|
35
|
+
# immediately (Errno::ENOENT) rather than run natively as it should.
|
|
36
|
+
# The null device path is also platform-specific ('/dev/null' does
|
|
37
|
+
# not exist on Windows).
|
|
38
|
+
command = WINDOWS ? [default_bridge_path] : ['wine', default_bridge_path]
|
|
39
|
+
null_device = WINDOWS ? 'NUL' : '/dev/null'
|
|
40
|
+
Process.spawn(*command, port.to_s, %i[out err] => null_device)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.default_lockfile(port)
|
|
44
|
+
# The lock is per-port, not global: two bridges on two ports are
|
|
45
|
+
# entirely independent, and a single shared lock would make a client
|
|
46
|
+
# starting one wait on a client starting the other (design doc §7.1
|
|
47
|
+
# step 2). Dir.tmpdir rather than a hardcoded '/tmp' resolves
|
|
48
|
+
# correctly on Windows (%TEMP%) as well as every POSIX platform.
|
|
49
|
+
File.join(Dir.tmpdir, "wineole-bridge.#{port}.lock")
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.open(host: '127.0.0.1', port: 47800, spawner: DEFAULT_SPAWNER,
|
|
53
|
+
lockfile: nil, timeout: 15, token: nil)
|
|
54
|
+
lockfile ||= default_lockfile(port)
|
|
55
|
+
|
|
56
|
+
socket = try_connect(host, port)
|
|
57
|
+
return handshake(new(socket), token) if socket
|
|
58
|
+
|
|
59
|
+
File.open(lockfile, File::CREAT | File::RDWR, 0o644) do |lock|
|
|
60
|
+
lock.flock(File::LOCK_EX)
|
|
61
|
+
|
|
62
|
+
socket = try_connect(host, port)
|
|
63
|
+
return handshake(new(socket), token) if socket
|
|
64
|
+
|
|
65
|
+
spawner.call(port)
|
|
66
|
+
deadline = Time.now + timeout
|
|
67
|
+
loop do
|
|
68
|
+
socket = try_connect(host, port)
|
|
69
|
+
return handshake(new(socket), token) if socket
|
|
70
|
+
raise Error, "wineole-bridge did not start within #{timeout}s" if Time.now > deadline
|
|
71
|
+
sleep 0.2
|
|
72
|
+
end
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Design doc §7.1 step 1: ping after connecting, to confirm protocol
|
|
77
|
+
# compatibility before using the connection. This is also the only place a
|
|
78
|
+
# token can be presented — a bridge started with WINEOLE_TOKEN rejects
|
|
79
|
+
# every non-loopback request until a matching ping has been accepted, so
|
|
80
|
+
# without this a tokened bridge is unreachable from this client.
|
|
81
|
+
#
|
|
82
|
+
# A failed ping (bad token, incompatible bridge) raises out of
|
|
83
|
+
# `Client#call` as a RemoteError; it must not be swallowed.
|
|
84
|
+
def self.handshake(client, token)
|
|
85
|
+
client.call('ping', token ? {token: token} : {})
|
|
86
|
+
client
|
|
87
|
+
rescue StandardError
|
|
88
|
+
client.close
|
|
89
|
+
raise
|
|
90
|
+
end
|
|
91
|
+
private_class_method :handshake
|
|
92
|
+
|
|
93
|
+
def self.try_connect(host, port)
|
|
94
|
+
TCPSocket.new(host, port)
|
|
95
|
+
rescue Errno::ECONNREFUSED, Errno::ETIMEDOUT, Errno::EHOSTUNREACH
|
|
96
|
+
nil
|
|
97
|
+
end
|
|
98
|
+
private_class_method :try_connect
|
|
99
|
+
|
|
100
|
+
# A proc to close over the raw socket for ObjectSpace.define_finalizer.
|
|
101
|
+
# It deliberately does NOT close over `self` (the Client instance) --
|
|
102
|
+
# capturing the instance being finalized would keep it permanently
|
|
103
|
+
# reachable through ObjectSpace's own finalizer table, and it could
|
|
104
|
+
# never actually be collected. Capturing only the socket avoids this.
|
|
105
|
+
def self.finalizer(socket)
|
|
106
|
+
proc do
|
|
107
|
+
begin
|
|
108
|
+
socket.close
|
|
109
|
+
rescue StandardError
|
|
110
|
+
nil
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def initialize(socket)
|
|
116
|
+
@socket = socket
|
|
117
|
+
@next_id = 0
|
|
118
|
+
@mutex = Mutex.new
|
|
119
|
+
ObjectSpace.define_finalizer(self, self.class.finalizer(socket))
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def call(method, params = {})
|
|
123
|
+
@mutex.synchronize do
|
|
124
|
+
id = (@next_id += 1)
|
|
125
|
+
@socket.write(JSON.generate({id: id, method: method, params: params}) + "\n")
|
|
126
|
+
line = @socket.gets
|
|
127
|
+
raise ProtocolError, 'connection closed' if line.nil?
|
|
128
|
+
response = JSON.parse(line)
|
|
129
|
+
unless response['id'] == id
|
|
130
|
+
raise ProtocolError, "id mismatch: expected #{id}, got #{response['id']}"
|
|
131
|
+
end
|
|
132
|
+
raise RemoteError.new(response['error']['class'], response['error']['message']) if response['error']
|
|
133
|
+
response['result']
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def close
|
|
138
|
+
@socket.close
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def create(class_name)
|
|
142
|
+
Proxy.create(class_name, self)
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
def connect(class_name)
|
|
146
|
+
Proxy.connect(class_name, self)
|
|
147
|
+
end
|
|
148
|
+
|
|
149
|
+
def connect_or_create(class_name)
|
|
150
|
+
Proxy.connect_or_create(class_name, self)
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
module WineOLE
|
|
2
|
+
class Error < StandardError; end
|
|
3
|
+
class NotSerializableError < Error; end
|
|
4
|
+
class StaleReferenceError < Error; end
|
|
5
|
+
class ProtocolError < Error; end
|
|
6
|
+
|
|
7
|
+
class RemoteError < Error
|
|
8
|
+
attr_reader :remote_class
|
|
9
|
+
|
|
10
|
+
def initialize(remote_class, message)
|
|
11
|
+
@remote_class = remote_class
|
|
12
|
+
super("#{remote_class}: #{message}")
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
require 'time'
|
|
2
|
+
require_relative 'errors'
|
|
3
|
+
|
|
4
|
+
module WineOLE
|
|
5
|
+
class Proxy
|
|
6
|
+
def self.create(class_name, client)
|
|
7
|
+
handle = client.call('create', {class_name: class_name})['$ole_ref']
|
|
8
|
+
new(client, session_id: client.object_id, handle: handle, created: true)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.connect(class_name, client)
|
|
12
|
+
handle = client.call('connect', {class_name: class_name})['$ole_ref']
|
|
13
|
+
new(client, session_id: client.object_id, handle: handle, created: false)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.connect_or_create(class_name, client)
|
|
17
|
+
result = client.call('connect_or_create', {class_name: class_name})
|
|
18
|
+
new(client, session_id: client.object_id, handle: result['$ole_ref'], created: result['created'])
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.wrap(client, session_id, ole_ref)
|
|
22
|
+
new(client, session_id: session_id, handle: ole_ref, created: nil)
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
# Ruby's implicit-conversion protocol. The interpreter probes these on
|
|
26
|
+
# arbitrary objects behind the scenes — `puts`/`p` and `Array()` look for
|
|
27
|
+
# `to_ary`/`to_a`, multiple assignment looks for `to_ary`, string
|
|
28
|
+
# interpolation and `Kernel#String` look for `to_str`, `IO` methods look
|
|
29
|
+
# for `to_io`, `&obj` looks for `to_proc`, `Integer()`/`format('%d', _)`/
|
|
30
|
+
# `"ab" * _`/array indexing look for `to_int`/`to_i`, `Float()` looks for
|
|
31
|
+
# `to_f`, `File.open` looks for `to_path`, and `1 + _` looks for `coerce`.
|
|
32
|
+
# Forwarding these to the remote object turns every `puts proxy` into a
|
|
33
|
+
# round trip that ends in DISP_E_UNKNOWNNAME, so they must behave exactly
|
|
34
|
+
# as they would on a plain Ruby object that doesn't define them:
|
|
35
|
+
# NoMethodError, and `respond_to?` == false (which is what stops the
|
|
36
|
+
# interpreter calling them at all).
|
|
37
|
+
IMPLICIT_CONVERSIONS = %i[
|
|
38
|
+
to_ary to_a to_hash to_str to_io to_proc
|
|
39
|
+
to_int to_i to_f to_path coerce
|
|
40
|
+
].freeze
|
|
41
|
+
|
|
42
|
+
attr_reader :ole_handle, :ole_session_id
|
|
43
|
+
|
|
44
|
+
def initialize(client, session_id:, handle:, created:)
|
|
45
|
+
@client = client
|
|
46
|
+
@ole_session_id = session_id
|
|
47
|
+
@ole_handle = handle
|
|
48
|
+
@created = created
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def method_missing(name, *args)
|
|
52
|
+
return super if IMPLICIT_CONVERSIONS.include?(name)
|
|
53
|
+
|
|
54
|
+
check_live!
|
|
55
|
+
named = args.last.is_a?(Hash) ? args.pop : {}
|
|
56
|
+
invoke(name.to_s, args, named)
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def respond_to_missing?(name, _include_private = false)
|
|
60
|
+
!IMPLICIT_CONVERSIONS.include?(name)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def [](*args)
|
|
64
|
+
check_live!
|
|
65
|
+
invoke('', args, {})
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
# Was this instance freshly created by connect_or_create's
|
|
69
|
+
# fallback, or attached to something already running? `true` for
|
|
70
|
+
# `.create`, `false` for `.connect`, whatever the bridge reported for
|
|
71
|
+
# `.connect_or_create`, and `nil` for anything derived from another
|
|
72
|
+
# Proxy (e.g. `xl.Worksheets`) — attach-vs-create isn't a meaningful
|
|
73
|
+
# question for those.
|
|
74
|
+
def ole_created?
|
|
75
|
+
@created
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
def ole_release
|
|
79
|
+
@client.call('release', {handle: @ole_handle})
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def ole_const_load
|
|
83
|
+
check_live!
|
|
84
|
+
@client.call('const_load', {handle: @ole_handle})
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def marshal_dump
|
|
88
|
+
raise NotSerializableError, 'WineOLE::Proxy references are connection-scoped and cannot be persisted'
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# Deliberately bare and public, unlike every other meta-method here
|
|
92
|
+
# (which are `ole_`-prefixed to avoid shadowing a same-named remote COM
|
|
93
|
+
# member): an explicit escape hatch for the rare case a COM object
|
|
94
|
+
# really does define e.g. an `ole_handle` member, matching real Ruby
|
|
95
|
+
# WIN32OLE's own choice to keep `invoke` public and unprefixed.
|
|
96
|
+
def invoke(name, args, named)
|
|
97
|
+
check_live!
|
|
98
|
+
params = {
|
|
99
|
+
handle: @ole_handle,
|
|
100
|
+
name: name,
|
|
101
|
+
args: args.map { |a| encode(a) },
|
|
102
|
+
named: named.transform_values { |v| encode(v) },
|
|
103
|
+
}
|
|
104
|
+
decode(@client.call('invoke', params))
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def check_live!
|
|
110
|
+
return if @ole_session_id == @client.object_id
|
|
111
|
+
|
|
112
|
+
raise StaleReferenceError, 'this reference belongs to a previous connection'
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def encode(value)
|
|
116
|
+
case value
|
|
117
|
+
when Proxy
|
|
118
|
+
# The argument's own liveness is not enough: a Proxy belonging to a
|
|
119
|
+
# *different* Client is live from its own point of view, yet its
|
|
120
|
+
# handle id means nothing in this connection's handle table (or,
|
|
121
|
+
# worse, means something unrelated). Check it against the receiver's
|
|
122
|
+
# client, which is the connection the id is about to be sent over.
|
|
123
|
+
unless value.ole_session_id == @client.object_id
|
|
124
|
+
raise StaleReferenceError,
|
|
125
|
+
'this reference belongs to a different connection and cannot be ' \
|
|
126
|
+
'passed as an argument here'
|
|
127
|
+
end
|
|
128
|
+
{'$ole_ref' => value.ole_handle}
|
|
129
|
+
when Hash
|
|
130
|
+
value.transform_values { |v| encode(v) }
|
|
131
|
+
when Array
|
|
132
|
+
value.map { |v| encode(v) }
|
|
133
|
+
else
|
|
134
|
+
value
|
|
135
|
+
end
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def decode(value)
|
|
139
|
+
if value.is_a?(Hash) && value.key?('$ole_ref')
|
|
140
|
+
Proxy.wrap(@client, @client.object_id, value['$ole_ref'])
|
|
141
|
+
elsif value.is_a?(Hash) && value['$type'] == 'time'
|
|
142
|
+
Time.iso8601(value['iso8601'])
|
|
143
|
+
else
|
|
144
|
+
value
|
|
145
|
+
end
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
data/lib/wineole.rb
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
require_relative 'wineole/errors'
|
|
2
|
+
require_relative 'wineole/client'
|
|
3
|
+
require_relative 'wineole/proxy'
|
|
4
|
+
|
|
5
|
+
module WineOLE
|
|
6
|
+
@default_client = nil
|
|
7
|
+
@mutex = Mutex.new
|
|
8
|
+
|
|
9
|
+
# Opens a client the same way Client.open does, and also makes it the
|
|
10
|
+
# module's implicit default -- a subsequent WineOLE.create/.connect call
|
|
11
|
+
# uses this client rather than lazily creating a separate zero-config one.
|
|
12
|
+
# Calling .open again replaces the implicit default without closing
|
|
13
|
+
# whatever it previously pointed to -- the caller owns the returned
|
|
14
|
+
# client and is responsible for closing it if not relying on the
|
|
15
|
+
# implicit default's eventual GC-finalizer cleanup.
|
|
16
|
+
def self.open(...)
|
|
17
|
+
client = Client.open(...)
|
|
18
|
+
@mutex.synchronize { @default_client = client }
|
|
19
|
+
client
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# The lazily-initialized implicit default client used by .create/.connect
|
|
23
|
+
# when nothing was ever explicitly opened via WineOLE.open. Thread-safe:
|
|
24
|
+
# the fast path avoids locking once initialized; only the first caller(s)
|
|
25
|
+
# racing on a nil default contend for the lock, and only the actual
|
|
26
|
+
# winner calls Client.open -- everyone else sees it already set once they
|
|
27
|
+
# acquire the lock, and the `||=` means they never call Client.open again.
|
|
28
|
+
def self.default_client
|
|
29
|
+
return @default_client if @default_client
|
|
30
|
+
|
|
31
|
+
@mutex.synchronize { @default_client ||= Client.open }
|
|
32
|
+
end
|
|
33
|
+
private_class_method :default_client
|
|
34
|
+
|
|
35
|
+
def self.create(class_name)
|
|
36
|
+
default_client.create(class_name)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def self.connect(class_name)
|
|
40
|
+
default_client.connect(class_name)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def self.connect_or_create(class_name)
|
|
44
|
+
default_client.connect_or_create(class_name)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
# Closes the implicit default client, if one exists, and clears it so the
|
|
48
|
+
# next .create/.connect lazily opens a fresh one. Mainly for test hygiene
|
|
49
|
+
# (clearing state between test cases); also usable to release the
|
|
50
|
+
# implicit default early in a long-running process.
|
|
51
|
+
def self.close
|
|
52
|
+
@mutex.synchronize do
|
|
53
|
+
@default_client&.close
|
|
54
|
+
@default_client = nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
metadata
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: wineole
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- firelzrd
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies: []
|
|
12
|
+
description: |
|
|
13
|
+
A bridge (Rust, cross-compiled to run under Wine) plus a Ruby client
|
|
14
|
+
that lets a Linux Ruby process drive Win32OLE/COM automation of
|
|
15
|
+
Windows applications running under Wine, over a JSON Lines TCP
|
|
16
|
+
protocol, without requiring a Windows build of Ruby.
|
|
17
|
+
executables: []
|
|
18
|
+
extensions: []
|
|
19
|
+
extra_rdoc_files: []
|
|
20
|
+
files:
|
|
21
|
+
- LICENSE
|
|
22
|
+
- lib/wineole.rb
|
|
23
|
+
- lib/wineole/client.rb
|
|
24
|
+
- lib/wineole/errors.rb
|
|
25
|
+
- lib/wineole/proxy.rb
|
|
26
|
+
- wineole-bridge-dist/aarch64-pc-windows-gnullvm/wineole-bridge.exe
|
|
27
|
+
- wineole-bridge-dist/i686-pc-windows-gnu/wineole-bridge.exe
|
|
28
|
+
- wineole-bridge-dist/x86_64-pc-windows-gnu/wineole-bridge.exe
|
|
29
|
+
homepage: https://github.com/firelzrd/wineole
|
|
30
|
+
licenses:
|
|
31
|
+
- MIT
|
|
32
|
+
metadata:
|
|
33
|
+
source_code_uri: https://github.com/firelzrd/wineole
|
|
34
|
+
bug_tracker_uri: https://github.com/firelzrd/wineole/issues
|
|
35
|
+
rdoc_options: []
|
|
36
|
+
require_paths:
|
|
37
|
+
- lib
|
|
38
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
39
|
+
requirements:
|
|
40
|
+
- - ">="
|
|
41
|
+
- !ruby/object:Gem::Version
|
|
42
|
+
version: '3.0'
|
|
43
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
requirements: []
|
|
49
|
+
rubygems_version: 4.0.3
|
|
50
|
+
specification_version: 4
|
|
51
|
+
summary: Drive Win32OLE/COM automation of Wine-hosted Windows apps from Ruby
|
|
52
|
+
test_files: []
|