mimic-browser 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: e0b3b8e7dc2f8a30d2cf5fd0b1c126817088119e09ecf2fa2645560ea5794f84
4
+ data.tar.gz: 79b4692fee3d5445366cb121e9242a600941424ac2ef012b825d67883963bbcb
5
+ SHA512:
6
+ metadata.gz: 7cd0e4de5cc9a2955804d29c4b8eecac61df63bd2ea05692cd832bea3604fc5a27c127aabe169fd0dde0f88d5abaf3bf76552b93874a6adc8fe1a51f98a929f0
7
+ data.tar.gz: 497504d0545e5c64f13c2f7786f9ab92bd9f1c22b34cdfeb81a6af1d02e8cb3797488e80ab85c8db5de24502f978c40e6534e63910c25a88dda790181e16567b
data/LICENSE ADDED
@@ -0,0 +1,57 @@
1
+ # The Prosperity Public License 3.0.0
2
+
3
+ Contributor: moreveal
4
+
5
+ Source Code: https://github.com/moreveal/mimic
6
+
7
+ ## Purpose
8
+
9
+ This license allows you to use and share this software for noncommercial purposes for free and to try this software for commercial purposes for thirty days.
10
+
11
+ ## Agreement
12
+
13
+ In order to receive this license, you have to agree to its rules. Those rules are both obligations under that agreement and conditions to your license. Don't do anything with this software that triggers a rule you can't or won't follow.
14
+
15
+ ## Notices
16
+
17
+ Make sure everyone who gets a copy of any part of this software from you, with or without changes, also gets the text of this license and the contributor and source code lines above.
18
+
19
+ ## Commercial Trial
20
+
21
+ Limit your use of this software for commercial purposes to a thirty-day trial period. If you use this software for work, your company gets one trial period for all personnel, not one trial per person.
22
+
23
+ ## Contributions Back
24
+
25
+ Developing feedback, changes, or additions that you contribute back to the contributor on the terms of a standardized public software license such as [the Blue Oak Model License 1.0.0](https://blueoakcouncil.org/license/1.0.0), [the Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.html), [the MIT license](https://spdx.org/licenses/MIT.html), or [the two-clause BSD license](https://spdx.org/licenses/BSD-2-Clause.html) doesn't count as use for a commercial purpose.
26
+
27
+ ## Personal Uses
28
+
29
+ Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, doesn't count as use for a commercial purpose.
30
+
31
+ ## Noncommercial Organizations
32
+
33
+ Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution doesn't count as use for a commercial purpose regardless of the source of funding or obligations resulting from the funding.
34
+
35
+ ## Defense
36
+
37
+ Don't make any legal claim against anyone accusing this software, with or without changes, alone or with other technology, of infringing any patent.
38
+
39
+ ## Copyright
40
+
41
+ The contributor licenses you to do everything with this software that would otherwise infringe their copyright in it.
42
+
43
+ ## Patent
44
+
45
+ The contributor licenses you to do everything with this software that would otherwise infringe any patents they can license or become able to license.
46
+
47
+ ## Reliability
48
+
49
+ The contributor can't revoke this license.
50
+
51
+ ## Excuse
52
+
53
+ You're excused for unknowingly breaking [Notices](#notices) if you take all practical steps to comply within thirty days of learning you broke the rule.
54
+
55
+ ## No Liability
56
+
57
+ ***As far as the law allows, this software comes as is, without any warranty or condition, and the contributor won't be liable to anyone for any damages related to this software or this license, under any kind of legal claim.***
data/README.md ADDED
@@ -0,0 +1,96 @@
1
+ # Mimic for Ruby
2
+
3
+ Package `mimic-browser` requires Ruby 3.2+. Ferrum is optional; require
4
+ `mimic_sdk/ferrum` to select it. Core import is inert.
5
+
6
+ Install from GitHub with the native gem tools. The RubyGems package is not
7
+ published yet:
8
+
9
+ ```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
14
+ gem install ferrum --version 0.18.0 --no-document
15
+ ```
16
+
17
+ The bundled v0.2.3 runtime includes the CSS and navigation lifecycle support
18
+ required by Ferrum. The first launch downloads and verifies that exact runtime;
19
+ later launches reuse the persistent cache. No executable path is required.
20
+
21
+ ```ruby
22
+ require 'mimic_sdk/ferrum'
23
+
24
+ MimicSDK::Ferrum.launch do |session|
25
+ context = session.new_context
26
+ page = context.create_page
27
+ page.go_to('https://example.com')
28
+ puts page.title
29
+ puts session.mimic.get_version.version
30
+ end
31
+ ```
32
+
33
+ The returned browser, context and page are actual Ferrum objects. `connect`
34
+ attaches without installing or starting a runtime; closing it preserves the
35
+ external process and other clients. `launch` owns startup and bounded cleanup.
36
+ All launches are headless and bind an OS-assigned loopback port.
37
+ Ferrum uses a dedicated WebSocket for each Page. A reverse proxy must forward
38
+ both the browser endpoint and `/devtools/page/*` on the same origin.
39
+
40
+ `new_context(profile:, media:, resource_policy:, proxy:)` applies Mimic settings
41
+ 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.
44
+
45
+ `media:` also accepts a callable. It receives
46
+ `ContextSetup(browser_context_id:, mimic:)` after native Context creation and
47
+ before configuration or any user Page. Discover sources in that Context:
48
+
49
+ ```ruby
50
+ context = session.new_context(media: lambda do |setup|
51
+ sources = setup.mimic.get_media_sources({ 'browserContextId' => setup.browser_context_id }).sources
52
+ microphone = sources.find { |source| source.kind == 'audioinput' }
53
+ raise 'No microphone source available' unless microphone
54
+ { 'devices' => [{
55
+ 'key' => 'voice', 'kind' => 'audioinput', 'label' => 'Studio Microphone',
56
+ 'source' => { 'sourceId' => microphone.source_id }
57
+ }] }
58
+ end)
59
+ ```
60
+
61
+ Private source IDs select capture backends independently of public labels, keys,
62
+ groups and camera modes. Discovery does not open capture. Callbacks run outside
63
+ the adapter's ownership mutex; a failing callback disposes its new Context.
64
+ Media configuration alone keeps ordinary CDP emulation available; only an
65
+ explicit environment profile or proxy selects managed Context configuration.
66
+
67
+ Typed capabilities use snake_case methods and generated models. `UNSET` omits a
68
+ value, while `nil` remains JSON null. Raw wire names retain their exact spelling:
69
+
70
+ ```ruby
71
+ session.mimic.experimental.newContributorCommand({ 'enabled' => true })
72
+ session.mimic.experimental.call('newContributorCommand', { 'enabled' => true })
73
+ ```
74
+
75
+ Inspection is inert and explicit `call` handles reserved names. The same
76
+ dispatcher serves generated and experimental calls. `ProtocolError` retains
77
+ `code`, `message`, and `data`; missing error data differs from explicit null.
78
+ `timeout:` and `cancelled: -> { ... }` are supported by raw calls. Cancellation
79
+ after dispatch does not imply rollback and does not cause automatic retries.
80
+
81
+ `RuntimeManager.new(runtime_version:, lock_file:, executable_path:, runtime_dir:,
82
+ allow_download:, archive_path:, startup_timeout:, cancelled:)` follows the shared
83
+ 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
+ executables are hash-verified; all languages reuse the same OS cache and leases.
86
+ Use `mimic-sdk install --offline --archive /path/to/official.tar.gz`, `list`,
87
+ `verify`, or `lock` for deliberate cache operations.
88
+
89
+ Run `test/sdk_test.rb`, `test/transport_test.rb`, and `test/ferrum_test.rb` inside
90
+ Linux/WSL. Real fixture tests cover reverse-order concurrent replies, event order,
91
+ omission/null, structured errors, cancellation, disconnect, archive integrity,
92
+ native automation and owned/attached cleanup. Exact runtime/framework evidence
93
+ belongs in the repository's qualification receipts.
94
+ `test/media_test.rb` requires `MIMIC_MEDIA_FIXTURE` pointing to the explicit
95
+ synthetic-provider fixture. It verifies native capture and public identity
96
+ without opening physical devices.
data/bin/mimic-sdk ADDED
@@ -0,0 +1,29 @@
1
+ #!/usr/bin/env ruby
2
+ require 'optparse'
3
+ require 'mimic_sdk'
4
+
5
+ command = ARGV.shift
6
+ options = {}
7
+ parser = OptionParser.new do |opts|
8
+ opts.banner = 'mimic-sdk install|list|verify|prune|lock [options]'
9
+ opts.on('--version VERSION') { |value| options[:runtime_version] = value }
10
+ opts.on('--lock FILE') { |value| options[:lock_file] = value }
11
+ opts.on('--runtime-dir DIR') { |value| options[:runtime_dir] = value }
12
+ opts.on('--archive FILE') { |value| options[:archive_path] = value }
13
+ opts.on('--offline') { options[:allow_download] = false }
14
+ opts.on('--directory DIR') { |value| options[:directory] = value }
15
+ end
16
+ parser.parse!
17
+ manager = MimicSDK::RuntimeManager.new(**options)
18
+ result = case command
19
+ when 'install' then manager.install
20
+ when 'list' then manager.list
21
+ when 'verify', 'prune'
22
+ abort('--directory is required') unless options[:directory]
23
+ installation = manager.verify(options[:directory])
24
+ manager.prune(installation) if command == 'prune'
25
+ installation
26
+ when 'lock' then manager.resolve_lock
27
+ else abort(parser.to_s)
28
+ end
29
+ puts JSON.pretty_generate(result)
@@ -0,0 +1,126 @@
1
+ require 'mimic_sdk'
2
+
3
+ begin
4
+ require 'ferrum'
5
+ rescue LoadError => error
6
+ raise LoadError, "Install the selected adapter dependency with `gem install ferrum`: #{error.message}"
7
+ end
8
+
9
+ module MimicSDK
10
+ module Ferrum
11
+ ContextSetup = Struct.new(:browser_context_id, :mimic, keyword_init: true)
12
+
13
+ class Session
14
+ attr_reader :browser, :mimic, :runtime
15
+
16
+ def initialize(endpoint, runtime: nil, timeout: 30)
17
+ @runtime = runtime
18
+ @contexts = []
19
+ @mutex = Mutex.new
20
+ @transport = runtime ? runtime.transport : Transport.new(endpoint, timeout: timeout)
21
+ @mimic = Client.new(@transport)
22
+ version = @mimic.get_version
23
+ raise RuntimeError, 'Endpoint is not Mimic' if version.version.to_s.empty? || version.chrome_version.to_s.empty?
24
+ # Ferrum resolves target creation before its flattened session may be
25
+ # attached. A dedicated target socket binds each Page immediately and
26
+ # avoids caching a SessionClient with a missing session ID.
27
+ @browser = ::Ferrum::Browser.new(
28
+ ws_url: @transport.url, timeout: timeout, process_timeout: timeout, flatten: false
29
+ )
30
+ rescue StandardError
31
+ close
32
+ raise
33
+ end
34
+
35
+ # Returns Ferrum::Context, not an SDK automation wrapper. All configuration
36
+ # is complete before a user Page can be created through the returned object.
37
+ def new_context(profile: UNSET, media: nil, resource_policy: nil, proxy: nil)
38
+ context = @mutex.synchronize do
39
+ raise RuntimeError, 'Integration session closed' if @closed
40
+ created = browser.contexts.create
41
+ @contexts << created
42
+ created
43
+ end
44
+ begin
45
+ factory = media.respond_to?(:call)
46
+ # User callbacks run without the ownership mutex and may await their
47
+ # own work, create another Context or close this Session.
48
+ media = media.call(ContextSetup.new(browser_context_id: context.id, mimic: mimic)) if factory
49
+ if factory && !media.is_a?(Hash) && !media.is_a?(Generated::MediaConfiguration)
50
+ raise ArgumentError, 'Media factory must return a media configuration'
51
+ end
52
+ @mutex.synchronize { raise RuntimeError, 'Integration session closed' if @closed }
53
+ if !profile.equal?(UNSET) || proxy
54
+ params = { 'browserContextId' => context.id }
55
+ params['profile'] = Generated.to_wire(profile) unless profile.equal?(UNSET)
56
+ params['proxy'] = Generated.to_wire(proxy) if proxy
57
+ params['media'] = Generated.to_wire(media) if media
58
+ params['resourcePolicy'] = Generated.to_wire(resource_policy) if resource_policy
59
+ mimic.configure_context(params)
60
+ else
61
+ mimic.set_media_profile(Generated.to_wire(media).merge('browserContextId' => context.id)) if media
62
+ mimic.update_resource_policy({ 'browserContextId' => context.id, 'policy' => Generated.to_wire(resource_policy) }) if resource_policy
63
+ end
64
+ context
65
+ rescue StandardError
66
+ begin
67
+ context.dispose
68
+ @mutex.synchronize { @contexts.delete(context) }
69
+ rescue StandardError
70
+ # Preserve the configuration error; close retains ownership and
71
+ # retries disposal if cleanup could not be confirmed.
72
+ end
73
+ raise
74
+ end
75
+ end
76
+
77
+ def for_page(page)
78
+ 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'))
81
+ end
82
+
83
+ def close
84
+ return unless @mutex
85
+ @mutex.synchronize do
86
+ return if @closed
87
+ @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
96
+ end
97
+ end
98
+ nil
99
+ end
100
+ end
101
+
102
+ def self.launch(**options)
103
+ process = RuntimeManager.new(**options).launch
104
+ session = Session.new(process.endpoint, runtime: process)
105
+ return session unless block_given?
106
+ begin
107
+ yield session
108
+ ensure
109
+ session.close
110
+ end
111
+ rescue StandardError
112
+ process&.close
113
+ raise
114
+ end
115
+
116
+ def self.connect(endpoint, **options)
117
+ session = Session.new(endpoint, **options)
118
+ return session unless block_given?
119
+ begin
120
+ yield session
121
+ ensure
122
+ session.close
123
+ end
124
+ end
125
+ end
126
+ end