bidi2pdf 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.
Files changed (40) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.rspec +3 -0
  4. data/.rubocop.yml +50 -0
  5. data/.ruby-gemset +1 -0
  6. data/.ruby-version +1 -0
  7. data/CHANGELOG.md +5 -0
  8. data/LICENSE.txt +21 -0
  9. data/README.md +119 -0
  10. data/Rakefile +22 -0
  11. data/docker/Dockerfile +35 -0
  12. data/docker/docker-compose.yml +1 -0
  13. data/exe/bidi2pdf +7 -0
  14. data/lib/bidi2pdf/bidi/add_headers_interceptor.rb +42 -0
  15. data/lib/bidi2pdf/bidi/auth_interceptor.rb +67 -0
  16. data/lib/bidi2pdf/bidi/browser.rb +15 -0
  17. data/lib/bidi2pdf/bidi/browser_tab.rb +180 -0
  18. data/lib/bidi2pdf/bidi/client.rb +224 -0
  19. data/lib/bidi2pdf/bidi/event_manager.rb +84 -0
  20. data/lib/bidi2pdf/bidi/network_event.rb +54 -0
  21. data/lib/bidi2pdf/bidi/network_events.rb +82 -0
  22. data/lib/bidi2pdf/bidi/print_parameters_validator.rb +114 -0
  23. data/lib/bidi2pdf/bidi/session.rb +135 -0
  24. data/lib/bidi2pdf/bidi/user_context.rb +75 -0
  25. data/lib/bidi2pdf/bidi/web_socket_dispatcher.rb +70 -0
  26. data/lib/bidi2pdf/chromedriver_manager.rb +160 -0
  27. data/lib/bidi2pdf/cli.rb +118 -0
  28. data/lib/bidi2pdf/launcher.rb +46 -0
  29. data/lib/bidi2pdf/session_runner.rb +123 -0
  30. data/lib/bidi2pdf/utils.rb +15 -0
  31. data/lib/bidi2pdf/version.rb +5 -0
  32. data/lib/bidi2pdf.rb +25 -0
  33. data/sig/bidi2pdf/chrome/chromedriver_downloader.rbs +11 -0
  34. data/sig/bidi2pdf/chrome/downloader_helper.rbs +9 -0
  35. data/sig/bidi2pdf/chrome/finder.rbs +27 -0
  36. data/sig/bidi2pdf/chrome/platform.rbs +13 -0
  37. data/sig/bidi2pdf/chrome/version_resolver.rbs +19 -0
  38. data/sig/bidi2pdf.rbs +4 -0
  39. data/tmp/.keep +0 -0
  40. metadata +327 -0
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "browser_tab"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class UserContext
8
+ attr_reader :client
9
+
10
+ def initialize(client)
11
+ @client = client
12
+ @context_id = nil
13
+ end
14
+
15
+ def context_id
16
+ @context_id ||= begin
17
+ res = client.send_cmd_and_wait("browser.createUserContext", {}) do |response|
18
+ response["result"]["userContext"]
19
+ end
20
+
21
+ Bidi2pdf.logger.debug "User context created: #{res.inspect}"
22
+
23
+ res
24
+ end
25
+ end
26
+
27
+ def set_cookie(
28
+ name:,
29
+ value:,
30
+ domain:,
31
+ source_origin:,
32
+ path: "/",
33
+ secure: true,
34
+ http_only: false,
35
+ same_site: "strict",
36
+ ttl: 30
37
+ )
38
+ expiry = Time.now.to_i + ttl
39
+ client.send_cmd_and_wait("storage.setCookie", {
40
+ cookie: {
41
+ name: name,
42
+ value: {
43
+ type: "string",
44
+ value: value
45
+ },
46
+ domain: domain,
47
+ path: path,
48
+ secure: secure,
49
+ httpOnly: http_only,
50
+ sameSite: same_site,
51
+ expiry: expiry
52
+ },
53
+ partition: {
54
+ type: "storageKey",
55
+ userContext: context_id,
56
+ sourceOrigin: source_origin
57
+ }
58
+ }) do |response|
59
+ Bidi2pdf.logger.debug "Cookie set: #{response.inspect}"
60
+ end
61
+ end
62
+
63
+ def create_browser_window
64
+ client.send_cmd_and_wait("browsingContext.create", {
65
+ type: "window",
66
+ userContext: context_id
67
+ }) do |response|
68
+ browsing_context_id = response["result"]["context"]
69
+
70
+ BrowserTab.new(client, browsing_context_id, context_id)
71
+ end
72
+ end
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,70 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "event_manager"
4
+
5
+ module Bidi2pdf
6
+ module Bidi
7
+ class WebSocketDispatcher
8
+ attr_reader :socket_events, :session_events
9
+
10
+ def initialize(socket)
11
+ @socket = socket
12
+ @socket_events = EventManager.new("socket-event")
13
+ @session_events = EventManager.new("session-event")
14
+ end
15
+
16
+ def start_listening
17
+ Bidi2pdf.logger.debug "Registering WebSocket event listeners"
18
+
19
+ setup_connection_lifecycle_handlers
20
+ setup_message_handler
21
+ end
22
+
23
+ # Add listeners
24
+
25
+ def on_message(&block) = socket_events.on(:message, &block)
26
+
27
+ def on_event(name, &block) = session_events.on(name, &block)
28
+
29
+ def on_open(&block) = socket_events.on(:open, &block)
30
+
31
+ def on_close(&block) = socket_events.on(:close, &block)
32
+
33
+ def on_error(&block) = socket_events.on(:error, &block)
34
+
35
+ def remove_message_listener(block) = socket_events.off(:message, block)
36
+
37
+ def remove_event_listener(name, block) = session_events.off(name, block)
38
+
39
+ def remove_open_listener(block) = socket_events.off(:open, block)
40
+
41
+ def remove_close_listener(block) = socket_events.off(:close, block)
42
+
43
+ def remove_error_listener(block) = socket_events.off(:error, block)
44
+
45
+ def setup_message_handler
46
+ that = self
47
+
48
+ @socket.on(:message) do |msg|
49
+ data = JSON.parse(msg.data)
50
+ method = data["method"]
51
+
52
+ if method
53
+ Bidi2pdf.logger.debug "Dispatching session event: #{method}"
54
+ that.session_events.dispatch(method, data)
55
+ else
56
+ Bidi2pdf.logger.debug "Dispatching socket message"
57
+ that.socket_events.dispatch(:message, data)
58
+ end
59
+ end
60
+ end
61
+
62
+ def setup_connection_lifecycle_handlers
63
+ that = self
64
+ @socket.on(:open) { |e| that.socket_events.dispatch(:open, e) }
65
+ @socket.on(:close) { |e| that.socket_events.dispatch(:close, e) }
66
+ @socket.on(:error) { |e| that.socket_events.dispatch(:error, e) }
67
+ end
68
+ end
69
+ end
70
+ end
@@ -0,0 +1,160 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "chromedriver/binary"
4
+
5
+ module Bidi2pdf
6
+ class ChromedriverManager
7
+ attr_reader :port, :pid, :session
8
+
9
+ def initialize(port: 0, headless: true)
10
+ @port = port
11
+ @headless = headless
12
+ @session = nil
13
+ end
14
+
15
+ def start
16
+ return @pid if @pid
17
+
18
+ update_chromedriver
19
+ cmd = build_cmd
20
+ Bidi2pdf.logger.info "Starting Chromedriver with command: #{cmd}"
21
+
22
+ r, w = IO.pipe
23
+ @pid = Process.spawn(cmd, out: w, err: w)
24
+ w.close # close writer in parent
25
+
26
+ parse_port_from_output(r)
27
+
28
+ Bidi2pdf.logger.info "Started Chromedriver on port #{@port}, PID #{@pid}"
29
+ wait_until_chromedriver_ready
30
+
31
+ at_exit { stop }
32
+
33
+ @session = Bidi::Session.new(@port, headless: @headless)
34
+
35
+ @pid
36
+ end
37
+
38
+ def stop(timeout: 5)
39
+ return unless @pid
40
+
41
+ close_session
42
+
43
+ term_chromedriver
44
+
45
+ return unless process_alive?
46
+
47
+ kill_chromedriver timeout: timeout
48
+ ensure
49
+ @pid = nil
50
+ end
51
+
52
+ private
53
+
54
+ def close_session
55
+ Bidi2pdf.logger.info "Closing session"
56
+
57
+ @session.close
58
+ @session = nil
59
+ end
60
+
61
+ def term_chromedriver
62
+ Bidi2pdf.logger.info "Stopping Chromedriver (PID #{@pid})"
63
+
64
+ Process.kill("TERM", @pid)
65
+ rescue Errno::ESRCH
66
+ Bidi2pdf.logger.debug "Process already gone"
67
+ @pid = nil
68
+ end
69
+
70
+ def kill_chromedriver(timeout: 5)
71
+ start_time = Time.now
72
+
73
+ while Time.now - start_time < timeout
74
+ return @pid = nil unless process_alive?
75
+
76
+ sleep 0.1
77
+ end
78
+
79
+ Bidi2pdf.logger.warn "ChromeDriver did not terminate gracefully — force killing PID #{@pid}"
80
+
81
+ begin
82
+ Process.kill("KILL", @pid)
83
+ rescue Errno::ESRCH
84
+ Bidi2pdf.logger.debug "Process already gone"
85
+ end
86
+ end
87
+
88
+ def build_cmd
89
+ bin = Chromedriver::Binary::ChromedriverDownloader.driver_path
90
+ user_data_dir = File.join(Dir.tmpdir, "bidi2pdf", "user_data")
91
+
92
+ cmd = [bin]
93
+ cmd << "--port=#{@port}" unless @port.zero?
94
+ cmd << "--headless" if @headless
95
+ cmd << "--user-data-dir "
96
+ cmd << user_data_dir
97
+ cmd.join(" ")
98
+ end
99
+
100
+ def update_chromedriver
101
+ Chromedriver::Binary::ChromedriverDownloader.update
102
+ end
103
+
104
+ # rubocop: disable Metrics/AbcSize
105
+ def parse_port_from_output(io, timeout: 5)
106
+ Thread.new do
107
+ io.each_line do |line|
108
+ Bidi2pdf.logger.debug line.chomp
109
+
110
+ next unless line =~ /ChromeDriver was started successfully on port (\d+)/
111
+
112
+ Bidi2pdf.logger.debug "Found port: #{::Regexp.last_match(1).to_i} setup port: #{@port}"
113
+
114
+ @port = ::Regexp.last_match(1).to_i if @port.nil? || @port.zero?
115
+
116
+ break
117
+ end
118
+ rescue IOError
119
+ # reader closed
120
+ ensure
121
+ io.close unless io.closed?
122
+ end.join(timeout)
123
+
124
+ raise "Chromedriver did not report a usable port in #{timeout}s" if @port.nil?
125
+ end
126
+
127
+ # rubocop: enable Metrics/AbcSize
128
+
129
+ def process_alive?
130
+ return false unless @pid
131
+
132
+ begin
133
+ Process.waitpid(@pid, Process::WNOHANG)
134
+ true
135
+ rescue Errno::ESRCH, Errno::EPERM, Errno::ECHILD
136
+ Bidi2pdf.logger.debug "Process already gone"
137
+ false
138
+ end
139
+ end
140
+
141
+ def wait_until_chromedriver_ready(timeout: 5)
142
+ uri = URI("http://127.0.0.1:#{@port}/status")
143
+ deadline = Time.now + timeout
144
+
145
+ until Time.now > deadline
146
+ begin
147
+ response = Net::HTTP.get_response(uri)
148
+ json = JSON.parse(response.body)
149
+ return true if json["value"] && json["value"]["ready"]
150
+ rescue Errno::ECONNREFUSED, Errno::EHOSTUNREACH, JSON::ParserError
151
+ # Just retry
152
+ end
153
+
154
+ sleep 0.1
155
+ end
156
+
157
+ raise "ChromeDriver did not become ready within #{timeout} seconds"
158
+ end
159
+ end
160
+ end
@@ -0,0 +1,118 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "thor"
4
+
5
+ module Bidi2pdf
6
+ class CLI < Thor
7
+ desc "render", "Render a URL to PDF using Chrome BiDi"
8
+ long_desc <<~USAGE, wrap: false
9
+ Example:
10
+
11
+ $ bidi2pdf render \\
12
+ --url http://localhost:3000/report \\
13
+ --output report.pdf \\
14
+ --cookie session=abc123 \\
15
+ --header X-API-KEY=topsecret \\
16
+ --auth admin:admin \\
17
+ --headless \\
18
+ --port 0 \\
19
+ --wait_window_loaded \\
20
+ --wait_network_idle \\
21
+ --log-level debug
22
+
23
+ This command will render the given URL to PDF using Chrome via BiDi protocol,
24
+ optionally passing cookies, headers, and basic authentication.
25
+
26
+ Set --port to 0 for a random ChromeDriver port.
27
+ USAGE
28
+
29
+ option :url, required: true, desc: "The URL to render"
30
+ option :output, default: "output.pdf", desc: "Filename for the output PDF"
31
+ option :cookie, type: :array, default: [], banner: "name=value", desc: "One or more cookies"
32
+ option :header, type: :array, default: [], banner: "name=value", desc: "One or more custom headers"
33
+ option :auth, type: :string, banner: "user:pass", desc: "Basic auth credentials"
34
+ option :headless, type: :boolean, default: true, desc: "Run Chrome in headless mode"
35
+ option :port, type: :numeric, default: 0, desc: "Port to run ChromeDriver on (0 = auto)"
36
+ option :wait_window_loaded,
37
+ type: :boolean,
38
+ default: false,
39
+ desc: "Wait for the window to be fully loaded (windoow.loaded set by your script)"
40
+ option :wait_network_idle, type: :boolean, default: false, desc: "Wait for network to be idle"
41
+ option :default_timeout, type: :numeric, default: 60, desc: "Default timeout for commands"
42
+ option :log_level,
43
+ type: :string,
44
+ default: "info", enum: %w[debug info warn error fatal unknown], desc: "Set log level"
45
+
46
+ def render
47
+ configure
48
+
49
+ Bidi2pdf.logger.info "Rendering: #{options[:url]} -> #{options[:output]}"
50
+
51
+ launcher.launch
52
+ end
53
+
54
+ private
55
+
56
+ # rubocop:disable Metrics/AbcSize
57
+ def launcher
58
+ # rubocop:disable Layout/BeginEndAlignment
59
+ @launcher ||= begin
60
+ username, password = parse_auth(options[:auth]) if options[:auth]
61
+
62
+ Bidi2pdf::Launcher.new(
63
+ url: options[:url],
64
+ output: options[:output],
65
+ cookies: parse_key_values(options[:cookie]),
66
+ headers: parse_key_values(options[:header]),
67
+ auth: { username: username, password: password },
68
+ port: options[:port],
69
+ headless: options[:headless],
70
+ wait_window_loaded: options[:wait_window_loaded],
71
+ wait_network_idle: options[:wait_network_idle]
72
+ )
73
+ end
74
+ # rubocop:enable Layout/BeginEndAlignment
75
+ end
76
+
77
+ # rubocop:enable Metrics/AbcSize
78
+
79
+ def configure
80
+ Bidi2pdf.configure do |config|
81
+ config.logger.level = log_level
82
+ config.default_timeout = options[:default_timeout]
83
+
84
+ Chromedriver::Binary.configure do |c|
85
+ c.logger.level = log_level
86
+ end
87
+ end
88
+ end
89
+
90
+ def log_level
91
+ case options[:log_level]
92
+ when "debug" then Logger::DEBUG
93
+ when "warn" then Logger::WARN
94
+ when "error" then Logger::ERROR
95
+ when "fatal" then Logger::FATAL
96
+ when "unknown" then Logger::UNKNOWN
97
+ else
98
+ Logger::INFO
99
+ end
100
+ end
101
+
102
+ def parse_key_values(pairs)
103
+ pairs.to_h do |pair|
104
+ k, v = pair.split("=", 2)
105
+ raise ArgumentError, "Invalid format for pair: #{pair}" unless k && v
106
+
107
+ [k.strip, v.strip]
108
+ end
109
+ end
110
+
111
+ def parse_auth(auth_string)
112
+ user, pass = auth_string.split(":", 2)
113
+ raise ArgumentError, "Auth must be in 'user:pass' format" unless user && pass
114
+
115
+ [user, pass]
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "chromedriver_manager"
4
+ require_relative "session_runner"
5
+ require_relative "bidi/session"
6
+
7
+ module Bidi2pdf
8
+ class Launcher
9
+ def initialize(url:, output:, cookies:, headers:, auth:, headless: true, port: 0, wait_window_loaded: false,
10
+ wait_network_idle: false, print_options: {})
11
+ @url = url
12
+ @port = port
13
+ @headless = headless
14
+ @output = output
15
+ @cookies = cookies
16
+ @headers = headers
17
+ @auth = auth
18
+ @manager = nil
19
+ @wait_window_loaded = wait_window_loaded
20
+ @wait_network_idle = wait_network_idle
21
+ @print_options = print_options || {}
22
+ end
23
+
24
+ def launch
25
+ @manager = ChromedriverManager.new(port: @port, headless: @headless)
26
+ @manager.start
27
+
28
+ runner = SessionRunner.new(
29
+ session: @manager.session,
30
+ url: @url,
31
+ output: @output,
32
+ cookies: @cookies,
33
+ headers: @headers,
34
+ auth: @auth,
35
+ wait_window_loaded: @wait_window_loaded,
36
+ wait_network_idle: @wait_network_idle,
37
+ print_options: @print_options
38
+ )
39
+ runner.run
40
+ end
41
+
42
+ def stop
43
+ @manager.stop
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,123 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ class SessionRunner
5
+ def initialize(session:, url:, output:, cookies: {}, headers: {}, auth: {}, wait_window_loaded: false,
6
+ wait_network_idle: false, print_options: {})
7
+ @session = session
8
+ @url = url
9
+ @output = output
10
+ @cookies = cookies || {}
11
+ @headers = headers || {}
12
+ @auth = auth || {}
13
+ @wait_window_loaded = wait_window_loaded
14
+ @wait_network_idle = wait_network_idle
15
+ @print_options = print_options || {}
16
+ end
17
+
18
+ def run
19
+ @session.start
20
+ @session.client.on_close { Bidi2pdf.logger.info "WebSocket closed" }
21
+
22
+ setup_browser
23
+ run_flow
24
+ end
25
+
26
+ private
27
+
28
+ def setup_browser
29
+ browser = @session.browser
30
+ user_context = browser.create_user_context
31
+
32
+ add_cookies(user_context)
33
+
34
+ window = user_context.create_browser_window
35
+ tab = window.create_browser_tab
36
+
37
+ @window = window
38
+ @tab = tab
39
+
40
+ add_headers
41
+ add_basic_auth
42
+ end
43
+
44
+ def add_cookies(user_context)
45
+ @cookies.each do |name, value|
46
+ user_context.set_cookie(
47
+ name: name,
48
+ value: value,
49
+ domain: domain,
50
+ source_origin: source_origin
51
+ )
52
+ end
53
+ end
54
+
55
+ def add_headers
56
+ @headers.each do |name, value|
57
+ @tab.add_headers(
58
+ url_patterns: url_patterns,
59
+ headers: [{ name: name, value: value }]
60
+ )
61
+ end
62
+ end
63
+
64
+ def add_basic_auth
65
+ return unless @auth[:username] && @auth[:password]
66
+
67
+ @tab.basic_auth(
68
+ url_patterns: url_patterns,
69
+ username: @auth[:username],
70
+ password: @auth[:password]
71
+ )
72
+ end
73
+
74
+ def run_flow
75
+ @session.status
76
+ @session.user_contexts
77
+
78
+ @tab.open_page(@url)
79
+
80
+ if @wait_network_idle
81
+ Bidi2pdf.logger.info "Waiting for network idle"
82
+ @tab.wait_until_all_finished
83
+ end
84
+
85
+ if @wait_window_loaded
86
+ Bidi2pdf.logger.info "Waiting for window to be loaded"
87
+ @tab.execute_script <<-EOF_SCRIPT
88
+ new Promise(resolve => { const check = () => window.loaded ? resolve('done') : setTimeout(check, 100); check(); });
89
+ EOF_SCRIPT
90
+ end
91
+
92
+ @tab.print(@output, print_options: @print_options)
93
+ ensure
94
+ @tab.close
95
+ @window.close
96
+ end
97
+
98
+ def uri
99
+ @uri ||= URI(@url)
100
+ end
101
+
102
+ def domain
103
+ uri.host
104
+ end
105
+
106
+ def source_origin
107
+ origin = "#{uri.scheme}://#{uri.host}"
108
+ origin += ":#{uri.port}" unless [80, 443].include?(uri.port)
109
+ origin
110
+ end
111
+
112
+ def url_patterns
113
+ [
114
+ {
115
+ type: "pattern",
116
+ protocol: uri.scheme,
117
+ hostname: uri.host,
118
+ port: uri.port.to_s
119
+ }
120
+ ]
121
+ end
122
+ end
123
+ end
@@ -0,0 +1,15 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ module Utils
5
+ def timed(operation_name)
6
+ start_time = Time.now
7
+ result = yield
8
+ elapsed = Time.now - start_time
9
+ Bidi2pdf.logger.debug "#{operation_name} completed in #{elapsed.round(3)}s"
10
+ result
11
+ end
12
+
13
+ module_function :timed
14
+ end
15
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Bidi2pdf
4
+ VERSION = "0.1.0"
5
+ end
data/lib/bidi2pdf.rb ADDED
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "bidi2pdf/utils"
4
+ require_relative "bidi2pdf/launcher"
5
+ require_relative "bidi2pdf/bidi/session"
6
+
7
+ require "logger"
8
+
9
+ module Bidi2pdf
10
+ class Error < StandardError; end
11
+
12
+ @logger = Logger.new($stdout)
13
+ @logger.level = Logger::DEBUG
14
+
15
+ @default_timeout = 60
16
+
17
+ class << self
18
+ attr_accessor :logger, :default_timeout
19
+
20
+ # Allow configuration through a block
21
+ def configure
22
+ yield self if block_given?
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,11 @@
1
+ module Bidi2pdf
2
+ module Chrome
3
+ class ChromedriverDownloader
4
+ def self.install_dir: -> String
5
+
6
+ def self.target: -> String
7
+
8
+ def self.update: -> String
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,9 @@
1
+ module Bidi2pdf
2
+ module Chrome
3
+ module DownloaderHelper
4
+ def download_file: (String url, String destination) -> nil
5
+
6
+ def extract_zip: (String zip_file, String destination) -> nil
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,27 @@
1
+ module Bidi2pdf
2
+ module Chrome
3
+ class Finder
4
+ def self.location: -> String
5
+
6
+ def self.version: -> String
7
+
8
+ def self.win_location: -> String
9
+
10
+ def self.mac_location: -> String
11
+
12
+ def self.linux_location: -> String
13
+
14
+ def self.win_version: (String location) -> String
15
+
16
+ def self.linux_version: (String location) -> String
17
+
18
+ def self.mac_version: (String location) -> String
19
+
20
+ def self.call: (String process, Array[String] arg) -> String
21
+
22
+ private
23
+
24
+ def self.find_in_paths: -> String
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,13 @@
1
+ module Bidi2pdf
2
+ module Chrome
3
+ module Platform
4
+ def platform: -> String
5
+
6
+ def platform_id: -> String
7
+
8
+ def driver_filename: -> String
9
+
10
+ def file_name: -> String
11
+ end
12
+ end
13
+ end