mimic-browser 0.1.0 → 0.1.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0b3b8e7dc2f8a30d2cf5fd0b1c126817088119e09ecf2fa2645560ea5794f84
4
- data.tar.gz: 79b4692fee3d5445366cb121e9242a600941424ac2ef012b825d67883963bbcb
3
+ metadata.gz: 37f584d860c127be652e6c4979dca16dae2dda07263f3a306b9324416071a272
4
+ data.tar.gz: 2787d67161bac57dd660881b2250ef446dd703117e4591dc2942978132e47a6b
5
5
  SHA512:
6
- metadata.gz: 7cd0e4de5cc9a2955804d29c4b8eecac61df63bd2ea05692cd832bea3604fc5a27c127aabe169fd0dde0f88d5abaf3bf76552b93874a6adc8fe1a51f98a929f0
7
- data.tar.gz: 497504d0545e5c64f13c2f7786f9ab92bd9f1c22b34cdfeb81a6af1d02e8cb3797488e80ab85c8db5de24502f978c40e6534e63910c25a88dda790181e16567b
6
+ metadata.gz: f9ca3db0cbac5f553e9bdb9d138006cdfe366b17152ca2ace3a577b9d80f743260c3d338aecc87985e9efa45eec5e9ca43d7cb8b67193c52e85d5a505eab0af4
7
+ data.tar.gz: 1bf217c272574b736d016c0a34487e84734d938b70bc0d0be779b1d571f42f5c4bba390725c48613147a11c9d1b254fd89530e9cec61dd1ae1ae3db827634e71
data/README.md CHANGED
@@ -3,18 +3,14 @@
3
3
  Package `mimic-browser` requires Ruby 3.2+. Ferrum is optional; require
4
4
  `mimic_sdk/ferrum` to select it. Core import is inert.
5
5
 
6
- Install from GitHub with the native gem tools. The RubyGems package is not
7
- published yet:
6
+ Install the SDK from RubyGems and the optional Ferrum client:
8
7
 
9
8
  ```sh
10
- git clone https://github.com/mimic-browser/sdk.git
11
- cd sdk/ruby
12
- gem build mimic-browser.gemspec
13
- gem install ./mimic-browser-0.1.0.gem --no-document
9
+ gem install mimic-browser --no-document
14
10
  gem install ferrum --version 0.18.0 --no-document
15
11
  ```
16
12
 
17
- The bundled v0.2.3 runtime includes the CSS and navigation lifecycle support
13
+ The bundled v0.2.4 runtime includes the CSS and navigation lifecycle support
18
14
  required by Ferrum. The first launch downloads and verifies that exact runtime;
19
15
  later launches reuse the persistent cache. No executable path is required.
20
16
 
@@ -39,8 +35,10 @@ both the browser endpoint and `/devtools/page/*` on the same origin.
39
35
 
40
36
  `new_context(profile:, media:, resource_policy:, proxy:)` applies Mimic settings
41
37
  before returning the native context. A managed profile uses the runtime's
42
- configure-context bridge. `for_page(page)` creates a page extension session on
43
- the owned raw connection. Framework session IDs are never copied across sockets.
38
+ configure-context bridge. `for_page(page)` reuses one page capability handle on
39
+ the owned raw connection. Call `handle.close` to detach it early; the native Page
40
+ remains open. Closing the Page or integration invalidates the handle and removes
41
+ its cache entry. Framework session IDs are never copied across sockets.
44
42
 
45
43
  `media:` also accepts a callable. It receives
46
44
  `ContextSetup(browser_context_id:, mimic:)` after native Context creation and
@@ -64,7 +62,10 @@ the adapter's ownership mutex; a failing callback disposes its new Context.
64
62
  Media configuration alone keeps ordinary CDP emulation available; only an
65
63
  explicit environment profile or proxy selects managed Context configuration.
66
64
 
67
- Typed capabilities use snake_case methods and generated models. `UNSET` omits a
65
+ Runtime and adapter methods expose explicit keyword parameters. Generated models
66
+ also expose named constructors, for example
67
+ `Generated::MediaConfiguration.new(devices: [])`, and documented field/result
68
+ types for editor completion. Typed capabilities use snake_case methods. `UNSET` omits a
68
69
  value, while `nil` remains JSON null. Raw wire names retain their exact spelling:
69
70
 
70
71
  ```ruby
@@ -79,9 +80,9 @@ dispatcher serves generated and experimental calls. `ProtocolError` retains
79
80
  after dispatch does not imply rollback and does not cause automatic retries.
80
81
 
81
82
  `RuntimeManager.new(runtime_version:, lock_file:, executable_path:, runtime_dir:,
82
- allow_download:, archive_path:, startup_timeout:, cancelled:)` follows the shared
83
+ allow_download:, archive_path:, startup_timeout:, lock_timeout:, cancelled:)` follows the shared
83
84
  runtime contract. It provides `resolve_lock`, `install`, `launch`, `list`,
84
- `verify` and explicit `prune`. The packaged pin is v0.2.3. Exact archives and
85
+ `verify` and explicit `prune`. The packaged pin is v0.2.4. Exact archives and
85
86
  executables are hash-verified; all languages reuse the same OS cache and leases.
86
87
  Use `mimic-sdk install --offline --archive /path/to/official.tar.gz`, `list`,
87
88
  `verify`, or `lock` for deliberate cache operations.
@@ -8,15 +8,30 @@ end
8
8
 
9
9
  module MimicSDK
10
10
  module Ferrum
11
+ # Identity and typed capabilities for the context being configured.
12
+ # @!attribute [rw] browser_context_id
13
+ # @return [String]
14
+ # @!attribute [rw] mimic
15
+ # @return [MimicSDK::Client]
11
16
  ContextSetup = Struct.new(:browser_context_id, :mimic, keyword_init: true)
12
17
 
13
18
  class Session
14
- attr_reader :browser, :mimic, :runtime
19
+ # @return [::Ferrum::Browser]
20
+ attr_reader :browser
21
+ # @return [MimicSDK::Client]
22
+ attr_reader :mimic
23
+ # @return [MimicSDK::RuntimeProcess, nil] nil when attached to an external runtime
24
+ attr_reader :runtime
15
25
 
26
+ # @param endpoint [String] HTTP or browser WebSocket endpoint
27
+ # @param runtime [MimicSDK::RuntimeProcess, nil]
28
+ # @param timeout [Numeric] native client timeout in seconds
16
29
  def initialize(endpoint, runtime: nil, timeout: 30)
17
30
  @runtime = runtime
18
31
  @contexts = []
32
+ @page_clients = {}
19
33
  @mutex = Mutex.new
34
+ @close_changed = ConditionVariable.new
20
35
  @transport = runtime ? runtime.transport : Transport.new(endpoint, timeout: timeout)
21
36
  @mimic = Client.new(@transport)
22
37
  version = @mimic.get_version
@@ -27,6 +42,23 @@ module MimicSDK
27
42
  @browser = ::Ferrum::Browser.new(
28
43
  ws_url: @transport.url, timeout: timeout, process_timeout: timeout, flatten: false
29
44
  )
45
+ events, @unsubscribe = @transport.subscribe
46
+ @attachment_events = Thread.new do
47
+ while (event = events.pop)
48
+ next unless event['method'] == 'Target.detachedFromTarget'
49
+ @mutex.synchronize do
50
+ @page_clients.delete_if do |_target, client|
51
+ detached = client.session_id == event.dig('params', 'sessionId')
52
+ detached && client.invalidate
53
+ end
54
+ end
55
+ end
56
+ ensure
57
+ @mutex.synchronize do
58
+ # A pending explicit detach remains owned until its reply/failure.
59
+ @page_clients.delete_if { |_target, client| client.invalidate }
60
+ end
61
+ end
30
62
  rescue StandardError
31
63
  close
32
64
  raise
@@ -34,6 +66,12 @@ module MimicSDK
34
66
 
35
67
  # Returns Ferrum::Context, not an SDK automation wrapper. All configuration
36
68
  # is complete before a user Page can be created through the returned object.
69
+ # @param profile [String, MimicSDK::Generated::GenerateProfileSelection, Hash]
70
+ # @param media [MimicSDK::Generated::MediaConfiguration, Hash, Proc, nil]
71
+ # A Proc receives ContextSetup and returns MediaConfiguration or its wire Hash.
72
+ # @param resource_policy [MimicSDK::Generated::ResourcePolicy, Hash, nil]
73
+ # @param proxy [MimicSDK::Generated::Proxy, Hash, nil]
74
+ # @return [::Ferrum::Context]
37
75
  def new_context(profile: UNSET, media: nil, resource_policy: nil, proxy: nil)
38
76
  context = @mutex.synchronize do
39
77
  raise RuntimeError, 'Integration session closed' if @closed
@@ -74,34 +112,102 @@ module MimicSDK
74
112
  end
75
113
  end
76
114
 
115
+ # @param page [::Ferrum::Page]
116
+ # @return [MimicSDK::PageClient] cached until detached; close releases only this attachment
77
117
  def for_page(page)
78
118
  raise ArgumentError, 'Expected an actual Ferrum::Page' unless page.is_a?(::Ferrum::Page)
79
- session = @transport.call_raw('Target.attachToTarget', { 'targetId' => page.target_id, 'flatten' => true })
80
- Client.new(@transport, session_id: session.fetch('sessionId'))
119
+ loop do
120
+ client = @mutex.synchronize do
121
+ raise RuntimeError, 'Integration session closed' if @closed
122
+ @page_clients[page.target_id] ||= begin
123
+ session = @transport.call_raw('Target.attachToTarget', { 'targetId' => page.target_id, 'flatten' => true })
124
+ PageClient.new(@transport, session_id: session.fetch('sessionId'), detach: method(:detach_page_client))
125
+ end
126
+ end
127
+ return client unless client.closed?
128
+ # Never replace a closing handle before its cleanup finishes. Waiting
129
+ # outside the Session mutex allows detach to release its ownership.
130
+ client.close
131
+ end
132
+ end
133
+
134
+ def detach_page_client(client)
135
+ @transport.call_raw('Target.detachFromTarget', { 'sessionId' => client.session_id })
136
+ rescue ProtocolError => error
137
+ # The target can close between local invalidation and the detach command.
138
+ raise unless error.code == -32000 && error.message == 'No session with given id'
139
+ ensure
140
+ @mutex.synchronize { @page_clients.delete_if { |_target, owned| owned.equal?(client) } }
81
141
  end
142
+ private :detach_page_client
82
143
 
83
144
  def close
84
145
  return unless @mutex
85
- @mutex.synchronize do
86
- return if @closed
146
+ contexts, clients = @mutex.synchronize do
147
+ @close_changed.wait(@mutex) while @closing
148
+ if @closed
149
+ raise @close_error if @close_error
150
+ return
151
+ end
87
152
  @closed = true
88
- begin
89
- @contexts.each(&:dispose)
90
- ensure
91
- begin
92
- @browser&.quit # Remote Ferrum owns no process; quit only disconnects.
93
- ensure
94
- @runtime ? @runtime.close : @transport&.close
95
- end
153
+ @closing = true
154
+ owned = [@contexts, @page_clients.values]
155
+ @contexts = []
156
+ @page_clients = {}
157
+ owned
158
+ end
159
+ failure = nil
160
+ cleanup = lambda do |&operation|
161
+ operation.call
162
+ rescue StandardError => error
163
+ failure ||= error
164
+ end
165
+ begin
166
+ clients.each { |client| cleanup.call { client.close } }
167
+ contexts.each { |context| cleanup.call { context.dispose } }
168
+ cleanup.call { @browser&.quit } # A remote Ferrum browser owns no process.
169
+ cleanup.call { @runtime ? @runtime.close : @transport&.close }
170
+ ensure
171
+ clients.each(&:invalidate)
172
+ cleanup.call { @unsubscribe&.call }
173
+ cleanup.call { @attachment_events&.join unless @attachment_events == Thread.current }
174
+ @mutex.synchronize do
175
+ @close_error = failure
176
+ @closing = false
177
+ @close_changed.broadcast
96
178
  end
97
179
  end
180
+ raise failure if failure
98
181
  nil
99
182
  end
100
183
  end
101
184
 
102
- def self.launch(**options)
103
- process = RuntimeManager.new(**options).launch
104
- session = Session.new(process.endpoint, runtime: process)
185
+ # @param runtime_version [String, nil]
186
+ # @param lock_file [String, nil]
187
+ # @param executable_path [String, nil]
188
+ # @param runtime_dir [String, nil]
189
+ # @param archive_path [String, nil]
190
+ # @param allow_download [Boolean] omitted to use the environment default
191
+ # @param startup_timeout [Numeric] seconds
192
+ # @param lock_timeout [Numeric] seconds
193
+ # @param cancelled [Proc, nil] returns true to cancel
194
+ # @param timeout [Numeric] native client timeout in seconds
195
+ # @yield [session] closes the session after the block
196
+ # @yieldparam session [MimicSDK::Ferrum::Session]
197
+ # @return [MimicSDK::Ferrum::Session] when called without a block;
198
+ # the block form returns the block's result instead
199
+ def self.launch(runtime_version: nil, lock_file: nil, executable_path: nil,
200
+ runtime_dir: nil, archive_path: nil, allow_download: UNSET,
201
+ startup_timeout: 30, lock_timeout: 120, cancelled: nil,
202
+ timeout: 30)
203
+ process = RuntimeManager.new(
204
+ runtime_version: runtime_version, lock_file: lock_file,
205
+ executable_path: executable_path, runtime_dir: runtime_dir,
206
+ archive_path: archive_path, allow_download: allow_download,
207
+ startup_timeout: startup_timeout, lock_timeout: lock_timeout,
208
+ cancelled: cancelled
209
+ ).launch
210
+ session = Session.new(process.endpoint, runtime: process, timeout: timeout)
105
211
  return session unless block_given?
106
212
  begin
107
213
  yield session
@@ -113,8 +219,14 @@ module MimicSDK
113
219
  raise
114
220
  end
115
221
 
116
- def self.connect(endpoint, **options)
117
- session = Session.new(endpoint, **options)
222
+ # @param endpoint [String] HTTP or browser WebSocket endpoint
223
+ # @param timeout [Numeric] native client timeout in seconds
224
+ # @yield [session] closes the session after the block
225
+ # @yieldparam session [MimicSDK::Ferrum::Session]
226
+ # @return [MimicSDK::Ferrum::Session] when called without a block;
227
+ # the block form returns the block's result instead
228
+ def self.connect(endpoint, timeout: 30)
229
+ session = Session.new(endpoint, timeout: timeout)
118
230
  return session unless block_given?
119
231
  begin
120
232
  yield session