toolkami 0.1.2
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/README.md +34 -0
- data/lib/toolkami/assert.rb +8 -0
- data/lib/toolkami/client.rb +109 -0
- data/lib/toolkami/daemon.rb +131 -0
- data/lib/toolkami/generated.rb +19 -0
- data/lib/toolkami/install.rb +126 -0
- data/lib/toolkami/release.rb +63 -0
- data/lib/toolkami/v1/ping_pb.rb +20 -0
- data/lib/toolkami/v1/ping_services_pb.rb +25 -0
- data/lib/toolkami/version.rb +3 -0
- data/lib/toolkami.rb +9 -0
- data/toolkami.gemspec +29 -0
- metadata +88 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: c1ae1f752dd302a8ae60dad1caff7c12f0c474130d3f46cfec87ab6eaa4a70ed
|
|
4
|
+
data.tar.gz: 21c8360aeeb363b08ebe5383e0cea9d61706858a9fcc08bf0c6b2587ffbedeae
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 14de59f15537365e4f0e5723333c61adbe330be47353a03b27c9c384543565dfae5ebe16b4686b6124767112d0082b2721fb37c6b8dd0158ff5bc2f70a54c776
|
|
7
|
+
data.tar.gz: 4e88c77d9c663be3ac70416847ae7d428bde4b1a772fe3e2f937ada8f0ada33883bd280bae4ba7c15eaf54263ac5838ba7915375f5cd01c59c4241119eed4f5b
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Aperoc
|
|
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.
|
data/README.md
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# toolkami
|
|
2
|
+
|
|
3
|
+
Ruby gRPC client for the local `toolkami` daemon.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
gem install toolkami
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
The packaged gem downloads the matching Linux x64 daemon from the GitHub release for the gem version on first client start. Repository checkouts do not download a daemon; they use the local Cargo debug binary instead.
|
|
12
|
+
|
|
13
|
+
## Usage
|
|
14
|
+
|
|
15
|
+
```ruby
|
|
16
|
+
require "toolkami"
|
|
17
|
+
|
|
18
|
+
client = Toolkami::Client.open(tcp_address: "127.0.0.1:50061")
|
|
19
|
+
puts client.get_version.version
|
|
20
|
+
puts client.ping("ruby").message
|
|
21
|
+
client.close
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Development
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
bundle exec rake proto:generate
|
|
28
|
+
bundle exec rake test
|
|
29
|
+
bundle exec rake release:build
|
|
30
|
+
bundle exec rake integration:smoke_local
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
`release:verify` runs the pre-publish checks with the locally built gem and fails if the version is already on RubyGems.
|
|
34
|
+
`release:verify_published` runs the post-publish smoke test against RubyGems and fails until that version is visible there.
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
require_relative "assert"
|
|
2
|
+
require_relative "daemon"
|
|
3
|
+
require_relative "../toolkami/v1/ping_services_pb"
|
|
4
|
+
|
|
5
|
+
module Toolkami
|
|
6
|
+
class Client
|
|
7
|
+
def self.open(**options)
|
|
8
|
+
client = new(**options)
|
|
9
|
+
|
|
10
|
+
client.start
|
|
11
|
+
client
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(
|
|
15
|
+
tcp_address: nil,
|
|
16
|
+
startup_attempts_max: nil,
|
|
17
|
+
startup_delay_s: nil
|
|
18
|
+
)
|
|
19
|
+
@tcp_address = Daemon.resolve_tcp_address(tcp_address)
|
|
20
|
+
@startup_attempts_max = Daemon.resolve_startup_attempts_max(startup_attempts_max)
|
|
21
|
+
@startup_delay_s = Daemon.resolve_startup_delay_s(startup_delay_s)
|
|
22
|
+
@daemon_launch = nil
|
|
23
|
+
@stub = build_stub(@tcp_address)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def close
|
|
27
|
+
Daemon.terminate(@daemon_launch) unless @daemon_launch.nil?
|
|
28
|
+
@daemon_launch = nil
|
|
29
|
+
true
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def get_version
|
|
33
|
+
@stub.get_version(Toolkami::V1::GetVersionRequest.new)
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def ping(name)
|
|
37
|
+
Assert.that(name.is_a?(String), "name must be a String")
|
|
38
|
+
Assert.that(!name.empty?, "name must not be empty")
|
|
39
|
+
@stub.ping(Toolkami::V1::PingRequest.new(name: name))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def start
|
|
43
|
+
return self if ready?
|
|
44
|
+
|
|
45
|
+
daemon_bin_path = Daemon.resolve_bin_path
|
|
46
|
+
@daemon_launch = Daemon.launch(
|
|
47
|
+
daemon_bin_path: daemon_bin_path,
|
|
48
|
+
tcp_address: @tcp_address
|
|
49
|
+
)
|
|
50
|
+
@stub = build_stub(@tcp_address)
|
|
51
|
+
wait_for_ready
|
|
52
|
+
self
|
|
53
|
+
rescue StandardError => error
|
|
54
|
+
close
|
|
55
|
+
raise error
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
private
|
|
59
|
+
|
|
60
|
+
def build_stub(tcp_address)
|
|
61
|
+
stub = Toolkami::V1::ToolkamiService::Stub.new(
|
|
62
|
+
tcp_address,
|
|
63
|
+
:this_channel_is_insecure
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
Assert.that(!stub.nil?, "stub must be initialized")
|
|
67
|
+
Assert.that(tcp_address.include?(":"), "tcp_address must include a port")
|
|
68
|
+
stub
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def ready?
|
|
72
|
+
@stub = build_stub(@tcp_address)
|
|
73
|
+
get_version
|
|
74
|
+
true
|
|
75
|
+
rescue GRPC::BadStatus, Errno::ENOENT
|
|
76
|
+
false
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def wait_for_ready
|
|
80
|
+
attempt_index = 0
|
|
81
|
+
|
|
82
|
+
Assert.that(@startup_attempts_max.positive?, "startup_attempts_max must be positive")
|
|
83
|
+
Assert.that(@startup_delay_s.positive?, "startup_delay_s must be positive")
|
|
84
|
+
while attempt_index < @startup_attempts_max
|
|
85
|
+
return true if ready?
|
|
86
|
+
|
|
87
|
+
daemon_failure_raise
|
|
88
|
+
break if attempt_index + 1 >= @startup_attempts_max
|
|
89
|
+
|
|
90
|
+
sleep(@startup_delay_s)
|
|
91
|
+
attempt_index += 1
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
daemon_failure_raise(wait_timeout_s: @startup_delay_s)
|
|
95
|
+
raise "toolkami daemon did not become ready"
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
def daemon_failure_raise(wait_timeout_s: 0)
|
|
99
|
+
Assert.that(wait_timeout_s.is_a?(Numeric), "wait_timeout_s must be Numeric")
|
|
100
|
+
Assert.that(wait_timeout_s >= 0, "wait_timeout_s must be non-negative")
|
|
101
|
+
return true if @daemon_launch.nil?
|
|
102
|
+
|
|
103
|
+
daemon_failure = Daemon.failure(@daemon_launch, wait_timeout_s: wait_timeout_s)
|
|
104
|
+
return true if daemon_failure.nil?
|
|
105
|
+
|
|
106
|
+
raise daemon_failure
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
require_relative "assert"
|
|
2
|
+
require_relative "install"
|
|
3
|
+
require_relative "release"
|
|
4
|
+
|
|
5
|
+
module Toolkami
|
|
6
|
+
module Daemon
|
|
7
|
+
LaunchResult = Data.define(:daemon_pid, :daemon_stderr_read, :daemon_wait_thread)
|
|
8
|
+
|
|
9
|
+
STARTUP_ATTEMPTS_MAX_DEFAULT = 20
|
|
10
|
+
STARTUP_DELAY_S_DEFAULT = 0.1
|
|
11
|
+
STDERR_CHUNKS_MAX = 8
|
|
12
|
+
TCP_ADDRESS_DEFAULT = "127.0.0.1:50061"
|
|
13
|
+
|
|
14
|
+
def self.assert_bin_path(daemon_bin_path)
|
|
15
|
+
Assert.that(daemon_bin_path.is_a?(String), "daemon_bin_path must be a String")
|
|
16
|
+
Assert.that(!daemon_bin_path.empty?, "daemon_bin_path must not be empty")
|
|
17
|
+
Assert.that(File.executable?(daemon_bin_path), "daemon binary must be executable")
|
|
18
|
+
true
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.launch(daemon_bin_path:, tcp_address:)
|
|
22
|
+
assert_bin_path(daemon_bin_path)
|
|
23
|
+
daemon_stderr_read, daemon_stderr_write = IO.pipe
|
|
24
|
+
daemon_pid = Process.spawn(
|
|
25
|
+
{ "TOOLKAMI_TCP_ADDRESS" => tcp_address },
|
|
26
|
+
daemon_bin_path,
|
|
27
|
+
out: File::NULL,
|
|
28
|
+
err: daemon_stderr_write
|
|
29
|
+
)
|
|
30
|
+
daemon_stderr_write.close
|
|
31
|
+
|
|
32
|
+
Assert.that(daemon_pid.positive?, "daemon pid must be positive")
|
|
33
|
+
daemon_wait_thread = Process.detach(daemon_pid)
|
|
34
|
+
LaunchResult.new(daemon_pid, daemon_stderr_read, daemon_wait_thread)
|
|
35
|
+
rescue StandardError
|
|
36
|
+
daemon_stderr_read&.close unless daemon_stderr_read.nil? || daemon_stderr_read.closed?
|
|
37
|
+
daemon_stderr_write&.close unless daemon_stderr_write.nil? || daemon_stderr_write.closed?
|
|
38
|
+
raise
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def self.failure(daemon_launch, wait_timeout_s: 0)
|
|
42
|
+
Assert.that(!daemon_launch.nil?, "daemon_launch must not be nil")
|
|
43
|
+
Assert.that(wait_timeout_s.is_a?(Numeric), "wait_timeout_s must be Numeric")
|
|
44
|
+
Assert.that(wait_timeout_s >= 0, "wait_timeout_s must be non-negative")
|
|
45
|
+
return nil if daemon_launch.daemon_wait_thread.join(wait_timeout_s).nil?
|
|
46
|
+
|
|
47
|
+
daemon_status = daemon_launch.daemon_wait_thread.value
|
|
48
|
+
daemon_stderr = daemon_stderr_read(daemon_launch.daemon_stderr_read)
|
|
49
|
+
daemon_failure_message(daemon_status, daemon_stderr)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def self.terminate(daemon_launch)
|
|
53
|
+
Assert.that(!daemon_launch.nil?, "daemon_launch must not be nil")
|
|
54
|
+
unless daemon_launch.daemon_wait_thread.join(0)
|
|
55
|
+
Process.kill("TERM", daemon_launch.daemon_pid)
|
|
56
|
+
end
|
|
57
|
+
true
|
|
58
|
+
rescue Errno::ESRCH
|
|
59
|
+
true
|
|
60
|
+
ensure
|
|
61
|
+
daemon_launch.daemon_stderr_read.close unless daemon_launch.daemon_stderr_read.closed?
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
def self.resolve_bin_path
|
|
65
|
+
daemon_bin_path =
|
|
66
|
+
if Release.repository_checkout_detect
|
|
67
|
+
Release.daemon_bin_path_checkout_resolve
|
|
68
|
+
else
|
|
69
|
+
Install.daemon_bin_path_install
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
assert_bin_path(daemon_bin_path)
|
|
73
|
+
daemon_bin_path
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def self.resolve_tcp_address(tcp_address_override)
|
|
77
|
+
tcp_address = tcp_address_override || ENV["TOOLKAMI_TCP_ADDRESS"] || TCP_ADDRESS_DEFAULT
|
|
78
|
+
|
|
79
|
+
Assert.that(!tcp_address.to_s.empty?, "tcp_address must not be empty")
|
|
80
|
+
Assert.that(tcp_address.include?(":"), "tcp_address must include a port")
|
|
81
|
+
tcp_address
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def self.resolve_startup_attempts_max(startup_attempts_max)
|
|
85
|
+
attempts_max = startup_attempts_max || STARTUP_ATTEMPTS_MAX_DEFAULT
|
|
86
|
+
|
|
87
|
+
Assert.that(attempts_max.is_a?(Integer), "startup_attempts_max must be an Integer")
|
|
88
|
+
Assert.that(attempts_max.positive?, "startup_attempts_max must be positive")
|
|
89
|
+
attempts_max
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def self.resolve_startup_delay_s(startup_delay_s)
|
|
93
|
+
delay_s = startup_delay_s || STARTUP_DELAY_S_DEFAULT
|
|
94
|
+
|
|
95
|
+
Assert.that(delay_s.is_a?(Numeric), "startup_delay_s must be Numeric")
|
|
96
|
+
Assert.that(delay_s.positive?, "startup_delay_s must be positive")
|
|
97
|
+
delay_s
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def self.daemon_failure_message(daemon_status, daemon_stderr)
|
|
101
|
+
Assert.that(!daemon_status.nil?, "daemon_status must not be nil")
|
|
102
|
+
daemon_status_text =
|
|
103
|
+
if daemon_status.exitstatus.nil?
|
|
104
|
+
"signal #{daemon_status.termsig || 'unknown'}"
|
|
105
|
+
else
|
|
106
|
+
"code #{daemon_status.exitstatus}"
|
|
107
|
+
end
|
|
108
|
+
daemon_stderr_text = daemon_stderr.strip
|
|
109
|
+
daemon_message = "toolkami daemon exited before becoming ready (#{daemon_status_text})"
|
|
110
|
+
|
|
111
|
+
return daemon_message if daemon_stderr_text.empty?
|
|
112
|
+
|
|
113
|
+
"#{daemon_message} stderr: #{daemon_stderr_text}"
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
def self.daemon_stderr_read(daemon_stderr_read)
|
|
117
|
+
Assert.that(!daemon_stderr_read.nil?, "daemon_stderr_read must not be nil")
|
|
118
|
+
daemon_chunks = []
|
|
119
|
+
daemon_index = 0
|
|
120
|
+
|
|
121
|
+
until daemon_stderr_read.eof? || daemon_index >= STDERR_CHUNKS_MAX
|
|
122
|
+
daemon_chunks << daemon_stderr_read.readpartial(256)
|
|
123
|
+
daemon_index += 1
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
daemon_chunks.join
|
|
127
|
+
rescue EOFError
|
|
128
|
+
daemon_chunks.join
|
|
129
|
+
end
|
|
130
|
+
end
|
|
131
|
+
end
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
module Toolkami
|
|
2
|
+
module Generated
|
|
3
|
+
GENERATED_FILES = [
|
|
4
|
+
File.expand_path("v1/ping_pb.rb", __dir__),
|
|
5
|
+
File.expand_path("v1/ping_services_pb.rb", __dir__),
|
|
6
|
+
].freeze
|
|
7
|
+
|
|
8
|
+
def self.assert_ready
|
|
9
|
+
missing_paths = GENERATED_FILES.reject { |generated_path| File.file?(generated_path) }
|
|
10
|
+
|
|
11
|
+
return true if missing_paths.empty?
|
|
12
|
+
|
|
13
|
+
raise LoadError,
|
|
14
|
+
"generated Ruby gRPC files are missing; run `bundle exec rake proto:generate` in sdk/toolkami/ruby"
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
Toolkami::Generated.assert_ready
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
require "fileutils"
|
|
2
|
+
require "net/http"
|
|
3
|
+
require "rbconfig"
|
|
4
|
+
require "uri"
|
|
5
|
+
|
|
6
|
+
require_relative "assert"
|
|
7
|
+
require_relative "release"
|
|
8
|
+
|
|
9
|
+
module Toolkami
|
|
10
|
+
module Install
|
|
11
|
+
DOWNLOAD_OPEN_TIMEOUT_S = 15
|
|
12
|
+
DOWNLOAD_READ_TIMEOUT_S = 15
|
|
13
|
+
DOWNLOAD_REDIRECTS_MAX = 5
|
|
14
|
+
DOWNLOAD_SIZE_BYTES_MAX = 50 * 1024 * 1024
|
|
15
|
+
|
|
16
|
+
def self.daemon_bin_path_install
|
|
17
|
+
platform_assert
|
|
18
|
+
daemon_bin_path = Release.daemon_bin_path_packaged_resolve
|
|
19
|
+
return daemon_bin_path if daemon_bin_path_ready?(daemon_bin_path)
|
|
20
|
+
|
|
21
|
+
daemon_bytes = daemon_bytes_download
|
|
22
|
+
daemon_bin_file_write(daemon_bin_path, daemon_bytes)
|
|
23
|
+
daemon_bin_path_assert(daemon_bin_path)
|
|
24
|
+
daemon_bin_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.platform_assert
|
|
28
|
+
host_cpu = RbConfig::CONFIG["host_cpu"]
|
|
29
|
+
host_os = RbConfig::CONFIG["host_os"]
|
|
30
|
+
|
|
31
|
+
Assert.that(host_cpu.is_a?(String), "host_cpu must be a String")
|
|
32
|
+
Assert.that(host_os.is_a?(String), "host_os must be a String")
|
|
33
|
+
return true if host_os.include?("linux") && ["x86_64", "amd64"].include?(host_cpu)
|
|
34
|
+
|
|
35
|
+
raise "toolkami supports only linux x64 for automatic daemon installation"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.daemon_bytes_download
|
|
39
|
+
package_version = Release.package_version_resolve
|
|
40
|
+
release_artifact_url = Release.release_artifact_url_resolve(package_version)
|
|
41
|
+
response = http_response_fetch(URI(release_artifact_url))
|
|
42
|
+
daemon_bytes = response.body
|
|
43
|
+
|
|
44
|
+
Assert.that(daemon_bytes.is_a?(String), "daemon_bytes must be a String")
|
|
45
|
+
Assert.that(!daemon_bytes.empty?, "daemon_bytes must not be empty")
|
|
46
|
+
Assert.that(daemon_bytes.bytesize <= DOWNLOAD_SIZE_BYTES_MAX, "daemon_bytes must be bounded")
|
|
47
|
+
daemon_bytes
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def self.http_response_fetch(release_artifact_uri)
|
|
51
|
+
redirect_index = 0
|
|
52
|
+
current_uri = release_artifact_uri
|
|
53
|
+
|
|
54
|
+
Assert.that(current_uri.is_a?(URI::HTTPS), "release_artifact_uri must be HTTPS")
|
|
55
|
+
while redirect_index <= DOWNLOAD_REDIRECTS_MAX
|
|
56
|
+
response = http_response_request(current_uri)
|
|
57
|
+
return response if response.is_a?(Net::HTTPSuccess)
|
|
58
|
+
|
|
59
|
+
current_uri = http_response_redirect_uri(current_uri, response)
|
|
60
|
+
redirect_index += 1
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
raise "toolkami daemon download exceeded redirect limit"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def self.http_response_request(release_artifact_uri)
|
|
67
|
+
response = nil
|
|
68
|
+
|
|
69
|
+
Assert.that(release_artifact_uri.is_a?(URI::HTTP), "release_artifact_uri must be HTTP or HTTPS")
|
|
70
|
+
Assert.that(!release_artifact_uri.host.to_s.empty?, "release_artifact_uri host must not be empty")
|
|
71
|
+
Net::HTTP.start(
|
|
72
|
+
release_artifact_uri.host,
|
|
73
|
+
release_artifact_uri.port,
|
|
74
|
+
use_ssl: release_artifact_uri.scheme == "https",
|
|
75
|
+
open_timeout: DOWNLOAD_OPEN_TIMEOUT_S,
|
|
76
|
+
read_timeout: DOWNLOAD_READ_TIMEOUT_S
|
|
77
|
+
) do |http_client|
|
|
78
|
+
request = Net::HTTP::Get.new(release_artifact_uri)
|
|
79
|
+
response = http_client.request(request)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
Assert.that(!response.nil?, "response must be present")
|
|
83
|
+
Assert.that(response.code.to_i.positive?, "response code must be positive")
|
|
84
|
+
return response if response.is_a?(Net::HTTPSuccess) || response.is_a?(Net::HTTPRedirection)
|
|
85
|
+
|
|
86
|
+
raise "toolkami daemon download failed: #{response.code} #{response.message}"
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def self.http_response_redirect_uri(release_artifact_uri, response)
|
|
90
|
+
location_header = response["location"]
|
|
91
|
+
|
|
92
|
+
Assert.that(response.is_a?(Net::HTTPRedirection), "response must be a redirect")
|
|
93
|
+
Assert.that(location_header.is_a?(String), "redirect location must be a String")
|
|
94
|
+
redirect_uri = URI.join(release_artifact_uri.to_s, location_header)
|
|
95
|
+
Assert.that(redirect_uri.is_a?(URI::HTTP), "redirect_uri must be HTTP or HTTPS")
|
|
96
|
+
redirect_uri
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.daemon_bin_file_write(daemon_bin_path, daemon_bytes)
|
|
100
|
+
daemon_directory_path = File.dirname(daemon_bin_path)
|
|
101
|
+
daemon_temp_path = "#{daemon_bin_path}.tmp"
|
|
102
|
+
|
|
103
|
+
Assert.that(daemon_bin_path.end_with?("/toolkami"), "daemon_bin_path must end with /toolkami")
|
|
104
|
+
Assert.that(daemon_bytes.bytesize.positive?, "daemon_bytes must not be empty")
|
|
105
|
+
FileUtils.mkdir_p(daemon_directory_path)
|
|
106
|
+
File.binwrite(daemon_temp_path, daemon_bytes)
|
|
107
|
+
File.chmod(0o755, daemon_temp_path)
|
|
108
|
+
File.rename(daemon_temp_path, daemon_bin_path)
|
|
109
|
+
daemon_bin_path
|
|
110
|
+
ensure
|
|
111
|
+
FileUtils.rm_f(daemon_temp_path) unless daemon_temp_path.nil?
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
def self.daemon_bin_path_ready?(daemon_bin_path)
|
|
115
|
+
Assert.that(daemon_bin_path.is_a?(String), "daemon_bin_path must be a String")
|
|
116
|
+
Assert.that(!daemon_bin_path.empty?, "daemon_bin_path must not be empty")
|
|
117
|
+
File.file?(daemon_bin_path) && File.executable?(daemon_bin_path)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def self.daemon_bin_path_assert(daemon_bin_path)
|
|
121
|
+
Assert.that(File.file?(daemon_bin_path), "daemon binary must exist")
|
|
122
|
+
Assert.that(File.executable?(daemon_bin_path), "daemon binary must be executable")
|
|
123
|
+
true
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
end
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
require "uri"
|
|
2
|
+
|
|
3
|
+
require_relative "assert"
|
|
4
|
+
require_relative "version"
|
|
5
|
+
|
|
6
|
+
module Toolkami
|
|
7
|
+
module Release
|
|
8
|
+
RELEASE_ASSET_NAME = "toolkami-linux-x64"
|
|
9
|
+
RELEASE_BASE_URL = "https://github.com/aperoc/toolkami/releases"
|
|
10
|
+
|
|
11
|
+
def self.daemon_bin_path_checkout_resolve
|
|
12
|
+
daemon_bin_path = File.expand_path("../../../../../src/toolkami/target/debug/toolkami", __dir__)
|
|
13
|
+
|
|
14
|
+
Assert.that(daemon_bin_path.end_with?("/toolkami"), "checkout daemon path must end with /toolkami")
|
|
15
|
+
Assert.that(daemon_bin_path.include?("/src/toolkami/target/debug/"), "checkout daemon path must target cargo debug output")
|
|
16
|
+
daemon_bin_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def self.daemon_bin_path_packaged_resolve
|
|
20
|
+
daemon_bin_path = File.expand_path("../../bin/toolkami", __dir__)
|
|
21
|
+
|
|
22
|
+
Assert.that(daemon_bin_path.end_with?("/bin/toolkami"), "packaged daemon path must end with /bin/toolkami")
|
|
23
|
+
Assert.that(daemon_bin_path.include?("/sdk/toolkami/ruby/") || daemon_bin_path.include?("/gems/toolkami-"), "packaged daemon path must target sdk or gem install")
|
|
24
|
+
daemon_bin_path
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def self.package_root_path_resolve
|
|
28
|
+
package_root_path = File.expand_path("../..", __dir__)
|
|
29
|
+
|
|
30
|
+
Assert.that(File.absolute_path(package_root_path) == package_root_path, "package_root_path must be absolute")
|
|
31
|
+
Assert.that(package_root_path.include?("toolkami"), "package_root_path must include toolkami")
|
|
32
|
+
package_root_path
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def self.package_version_resolve
|
|
36
|
+
package_version = Toolkami::VERSION
|
|
37
|
+
|
|
38
|
+
Assert.that(package_version.is_a?(String), "package_version must be a String")
|
|
39
|
+
Assert.that(!package_version.empty?, "package_version must not be empty")
|
|
40
|
+
Assert.that(!package_version.include?(" "), "package_version must not contain spaces")
|
|
41
|
+
package_version
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def self.release_artifact_url_resolve(package_version)
|
|
45
|
+
release_artifact_url = URI.join(
|
|
46
|
+
"#{RELEASE_BASE_URL}/",
|
|
47
|
+
"download/v#{package_version}/#{RELEASE_ASSET_NAME}"
|
|
48
|
+
).to_s
|
|
49
|
+
|
|
50
|
+
Assert.that(package_version.is_a?(String), "package_version must be a String")
|
|
51
|
+
Assert.that(release_artifact_url.include?("/v#{package_version}/"), "release_artifact_url must include version")
|
|
52
|
+
release_artifact_url
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def self.repository_checkout_detect
|
|
56
|
+
cargo_toml_path = File.expand_path("../../../../../src/toolkami/Cargo.toml", __dir__)
|
|
57
|
+
|
|
58
|
+
Assert.that(cargo_toml_path.end_with?("/Cargo.toml"), "cargo_toml_path must end with /Cargo.toml")
|
|
59
|
+
Assert.that(cargo_toml_path.include?("/src/toolkami/"), "cargo_toml_path must point to the toolkami crate")
|
|
60
|
+
File.readable?(cargo_toml_path)
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
3
|
+
# source: toolkami/v1/ping.proto
|
|
4
|
+
|
|
5
|
+
require 'google/protobuf'
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
descriptor_data = "\n\x16toolkami/v1/ping.proto\x12\x0btoolkami.v1\"\x13\n\x11GetVersionRequest\":\n\x12GetVersionResponse\x12\x0f\n\x07version\x18\x01 \x01(\t\x12\x13\n\x0btcp_address\x18\x02 \x01(\t\"\x1b\n\x0bPingRequest\x12\x0c\n\x04name\x18\x01 \x01(\t\"\x1f\n\x0cPingResponse\x12\x0f\n\x07message\x18\x01 \x01(\t2\x9d\x01\n\x0fToolkamiService\x12M\n\nGetVersion\x12\x1e.toolkami.v1.GetVersionRequest\x1a\x1f.toolkami.v1.GetVersionResponse\x12;\n\x04Ping\x12\x18.toolkami.v1.PingRequest\x1a\x19.toolkami.v1.PingResponseb\x06proto3"
|
|
9
|
+
|
|
10
|
+
pool = ::Google::Protobuf::DescriptorPool.generated_pool
|
|
11
|
+
pool.add_serialized_file(descriptor_data)
|
|
12
|
+
|
|
13
|
+
module Toolkami
|
|
14
|
+
module V1
|
|
15
|
+
GetVersionRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.GetVersionRequest").msgclass
|
|
16
|
+
GetVersionResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.GetVersionResponse").msgclass
|
|
17
|
+
PingRequest = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.PingRequest").msgclass
|
|
18
|
+
PingResponse = ::Google::Protobuf::DescriptorPool.generated_pool.lookup("toolkami.v1.PingResponse").msgclass
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
|
2
|
+
# Source: toolkami/v1/ping.proto for package 'toolkami.v1'
|
|
3
|
+
|
|
4
|
+
require 'grpc'
|
|
5
|
+
require 'toolkami/v1/ping_pb'
|
|
6
|
+
|
|
7
|
+
module Toolkami
|
|
8
|
+
module V1
|
|
9
|
+
module ToolkamiService
|
|
10
|
+
class Service
|
|
11
|
+
|
|
12
|
+
include ::GRPC::GenericService
|
|
13
|
+
|
|
14
|
+
self.marshal_class_method = :encode
|
|
15
|
+
self.unmarshal_class_method = :decode
|
|
16
|
+
self.service_name = 'toolkami.v1.ToolkamiService'
|
|
17
|
+
|
|
18
|
+
rpc :GetVersion, ::Toolkami::V1::GetVersionRequest, ::Toolkami::V1::GetVersionResponse
|
|
19
|
+
rpc :Ping, ::Toolkami::V1::PingRequest, ::Toolkami::V1::PingResponse
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Stub = Service.rpc_stub_class
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
data/lib/toolkami.rb
ADDED
data/toolkami.gemspec
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
lib_path = File.expand_path("lib", __dir__)
|
|
2
|
+
$LOAD_PATH.unshift(lib_path) unless $LOAD_PATH.include?(lib_path)
|
|
3
|
+
require "toolkami/version"
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |spec|
|
|
6
|
+
spec.name = "toolkami"
|
|
7
|
+
spec.version = Toolkami::VERSION
|
|
8
|
+
spec.authors = ["Aperoc"]
|
|
9
|
+
spec.email = ["aperoc@users.noreply.github.com"]
|
|
10
|
+
spec.summary = "Toolkami Ruby SDK for the local Rust daemon"
|
|
11
|
+
spec.description = "Ruby gRPC SDK for the local Toolkami daemon with automatic packaged daemon installation."
|
|
12
|
+
spec.homepage = "https://github.com/aperoc/toolkami"
|
|
13
|
+
spec.license = "MIT"
|
|
14
|
+
spec.required_ruby_version = ">= 3.3.0"
|
|
15
|
+
spec.metadata["source_code_uri"] = "https://github.com/aperoc/toolkami"
|
|
16
|
+
spec.metadata["bug_tracker_uri"] = "https://github.com/aperoc/toolkami/issues"
|
|
17
|
+
spec.metadata["rubygems_mfa_required"] = "true"
|
|
18
|
+
spec.files = Dir.chdir(__dir__) do
|
|
19
|
+
Dir[
|
|
20
|
+
"LICENSE",
|
|
21
|
+
"README.md",
|
|
22
|
+
"lib/**/*.rb",
|
|
23
|
+
"toolkami.gemspec"
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
spec.require_paths = ["lib"]
|
|
27
|
+
spec.add_dependency "google-protobuf", "4.34.0"
|
|
28
|
+
spec.add_dependency "grpc", "1.78.1"
|
|
29
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: toolkami
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.2
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Aperoc
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-03-14 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: google-protobuf
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - '='
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: 4.34.0
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - '='
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: 4.34.0
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: grpc
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - '='
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: 1.78.1
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - '='
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: 1.78.1
|
|
41
|
+
description: Ruby gRPC SDK for the local Toolkami daemon with automatic packaged daemon
|
|
42
|
+
installation.
|
|
43
|
+
email:
|
|
44
|
+
- aperoc@users.noreply.github.com
|
|
45
|
+
executables: []
|
|
46
|
+
extensions: []
|
|
47
|
+
extra_rdoc_files: []
|
|
48
|
+
files:
|
|
49
|
+
- LICENSE
|
|
50
|
+
- README.md
|
|
51
|
+
- lib/toolkami.rb
|
|
52
|
+
- lib/toolkami/assert.rb
|
|
53
|
+
- lib/toolkami/client.rb
|
|
54
|
+
- lib/toolkami/daemon.rb
|
|
55
|
+
- lib/toolkami/generated.rb
|
|
56
|
+
- lib/toolkami/install.rb
|
|
57
|
+
- lib/toolkami/release.rb
|
|
58
|
+
- lib/toolkami/v1/ping_pb.rb
|
|
59
|
+
- lib/toolkami/v1/ping_services_pb.rb
|
|
60
|
+
- lib/toolkami/version.rb
|
|
61
|
+
- toolkami.gemspec
|
|
62
|
+
homepage: https://github.com/aperoc/toolkami
|
|
63
|
+
licenses:
|
|
64
|
+
- MIT
|
|
65
|
+
metadata:
|
|
66
|
+
source_code_uri: https://github.com/aperoc/toolkami
|
|
67
|
+
bug_tracker_uri: https://github.com/aperoc/toolkami/issues
|
|
68
|
+
rubygems_mfa_required: 'true'
|
|
69
|
+
post_install_message:
|
|
70
|
+
rdoc_options: []
|
|
71
|
+
require_paths:
|
|
72
|
+
- lib
|
|
73
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
74
|
+
requirements:
|
|
75
|
+
- - ">="
|
|
76
|
+
- !ruby/object:Gem::Version
|
|
77
|
+
version: 3.3.0
|
|
78
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
requirements: []
|
|
84
|
+
rubygems_version: 3.5.22
|
|
85
|
+
signing_key:
|
|
86
|
+
specification_version: 4
|
|
87
|
+
summary: Toolkami Ruby SDK for the local Rust daemon
|
|
88
|
+
test_files: []
|