# frozen_string_literal: truerequire'shellwords'require'socket'require'timeout'require'tmpdir'require'rbconfig'require'opal/os'require'net/http'require'webrick'moduleOpalmoduleCliRunnersclassSafariEXECUTION_TIMEOUT=600# secondsDEFAULT_SAFARI_DRIVER_HOST='localhost'DEFAULT_SAFARI_DRIVER_PORT=9444# in addition safari_driver_port + 1 is used for the http serverdefself.call(data)runner=new(data)runner.runenddefinitialize(data)argv=data[:argv]ifargv&&argv.any?warn"warning: ARGV is not supported by the Safari runner #{argv.inspect}"endoptions=data[:options]@output=options.fetch(:output,$stdout)@builder=data[:builder].callendattr_reader:output,:exit_status,:builderdefrunmktmpdirdo|dir|with_http_server(dir)do|http_port,server_thread|with_safari_driverdoprepare_files_in(dir)# Safaridriver commands are very limitied, for supported commands see:# https://developer.apple.com/documentation/webkit/macos_webdriver_commands_for_safari_12_and_laterNet::HTTP.start(safari_driver_host,safari_driver_port)do|con|con.read_timeout=EXECUTION_TIMEOUTres=con.post('/session',{capabilities: {browserName: 'Safari'}}.to_json,'Content-Type'=>'application/json')session_id=JSON.parse(res.body).dig('value','sessionId')ifsession_idsession_path="/session/#{session_id}"con.post("#{session_path}/url",{url: "http://#{safari_driver_host}:#{http_port}/index.html"}.to_json,'Content-Type'=>'application/json')server_thread.join(EXECUTION_TIMEOUT)elseSTDERR.puts"Could not create session: #{res.body}"endend0endendendendprivatedefprepare_files_in(dir)# The safaridriver is very limited in capabilities, basically it can trigger visiting sites# and interact a bit with the page. So this runner starts its own server, overwrites the# console log, warn, error functions of the browser and triggers a request after execution# to exit. Certain exceptions cannot be caught that way and everything may fail in between,# thats why execution is timed out after EXECUTION_TIMEOUT (10 minutes).# As a side effect, console messages may arrive out of order and timing anything may be inaccurate.builder.build_str<<~RUBY,'(exit)',no_export: true
%x{
var req = new XMLHttpRequest();
req.open("GET", '/exit');
req.send();
}
RUBYjs=builder.to_smap=builder.source_map.to_jsonext=builder.output_extensionmodule_type=' type="module"'ifbuilder.esm?File.binwrite("#{dir}/index.#{ext}",js)File.binwrite("#{dir}/index.map",map)File.binwrite("#{dir}/index.html",<<~HTML)
<html><head>
<meta charset='utf-8'>
<link rel="icon" href="data:;base64,=">
</head><body>
<script>
var orig_log = console.log;
var orig_err = console.error;
var orig_warn = console.warn;
function send_log_request(args) {
var req = new XMLHttpRequest();
req.open("POST", '/log');
req.setRequestHeader("Content-Type", "application/json");
req.send(JSON.stringify(args));
}
console.log = function() {
orig_log.apply(null, arguments);
send_log_request(arguments);
}
console.error = function() {
orig_err.apply(null, arguments);
send_log_request(arguments);
}
console.warn = function() {
orig_warn.apply(null, arguments);
send_log_request(arguments);
}
</script>
<script src='./index.#{ext}'#{module_type}></script>
</body></html>
HTML# <script src='./index.#{ext}'#{module_type}></script>enddefsafari_driver_hostENV['SAFARI_DRIVER_HOST']||DEFAULT_SAFARI_DRIVER_HOSTenddefsafari_driver_portENV['SAFARI_DRIVER_PORT']||DEFAULT_SAFARI_DRIVER_PORTenddefwith_http_server(dir)port=safari_driver_port.to_i+1server_thread=Thread.newdoserver=WEBrick::HTTPServer.new(Port:port,DocumentRoot:dir,Logger:WEBrick::Log.new('/dev/null'),AccessLog:[])server.mount_proc('/log')do|req,res|ifreq.bodypar=JSON.parse(req.body)par.each_valuedo|value|printvalue.to_sendendres.header['Content-Type']='text/plain'res.body=''endserver.mount_proc('/exit')doserver_thread.killendserver.startendyieldport,server_threadrescueexit(1)ensureserver_thread.killifserver_threadenddefwith_safari_driverifsafari_driver_running?yieldelserun_safari_driver{yield}endenddefrun_safari_driverraise'Safari driver can be started only on localhost'ifsafari_driver_host!=DEFAULT_SAFARI_DRIVER_HOSTstarted=falsesafari_driver_cmd=%{/usr/bin/safaridriver \
-p #{safari_driver_port}\#{ENV['SAFARI_DRIVER_OPTS']}}safari_driver_pid=Process.spawn(safari_driver_cmd,in: OS.dev_null,out: OS.dev_null,err: OS.dev_null)Timeout.timeout(30)doloopdobreakifsafari_driver_running?sleep0.5endendstarted=trueyieldrescueTimeout::Error=>eputsstarted?'Execution timed out':'Failed to start Safari driver'raiseeensureProcess.kill('HUP',safari_driver_pid)ifsafari_driver_pidenddefsafari_driver_running?puts"Connecting to #{safari_driver_host}:#{safari_driver_port}..."TCPSocket.new(safari_driver_host,safari_driver_port).closetruerescueErrno::ECONNREFUSED,Errno::EADDRNOTAVAILfalseenddefmktmpdir(&block)Dir.mktmpdir('safari-opal-',&block)endendendend