lib/opal/cli_runners/firefox_cdp_interface.rb
# backtick_javascript: true # frozen_string_literal: true # This script I converted into Opal, so that I don't have to write # buffer handling again. We have gets, Node has nothing close to it, # even async. # For CDP see docs/cdp_common.(md|json) require 'opal/platform' require 'nodejs/env' %x{ var CDP = require("chrome-remote-interface"); var fs = require("fs"); var http = require("http"); var dir = #{ARGV.last}; // var ext = #{ENV['OPAL_CDP_EXT']}; // not used at the moment var offset; // port offset for http server, depending on number of targets // even though its Firefox, "chrome-remote-interface" expects CHROME_* vars var options = { host: #{ENV['CHROME_HOST'] || 'localhost'}, port: parseInt(#{ENV['CHROME_PORT'] || '9333'}) // makes sure it doesn't accidentally connect to a lingering chrome }; // support functions function perror(error) { console.error(error); } var exiting = false; function shutdown(exit_code) { if (exiting) { return Promise.resolve(); } exiting = true; cdp_client.Target.closeTarget(target_id); // Promise doesn't get resolved server.close(); process.exit(exit_code); }; // simple HTTP server to deliver page, scripts to, and trigger commands from browser function not_found(res) { res.writeHead(404, { "Content-Type": "text/plain" }); res.end("NOT FOUND"); } function response_ok(res) { res.writeHead(200, { "Content-Type": "text/plain" }); res.end("OK"); } function handle_post(req, res, fun) { var data = ""; req.on('data', function(chunk) { data += chunk; }) req.on('end', function() { var obj = JSON.parse(data); fun.call(this, obj); }); } var server = http.createServer(function(req, res) { if (req.method === "GET") { var path = dir + '/' + req.url.slice(1); if (path.includes('..') || !fs.existsSync(path)) { not_found(res); } else { var content_type; if (path.endsWith(".html")) { content_type = "text/html" } else if (path.endsWith(".map")) { content_type = "application/json" } else { content_type = "application/javascript" } res.writeHead(200, { "Content-Type": content_type }); res.end(fs.readFileSync(path)); } } else if (req.method === "POST") { if (req.url === "/File.write") { // totally insecure on purpose handle_post(req, res, function(obj) { fs.writeFileSync(obj.filename, obj.data); response_ok(res); }); } else { not_found(res); } } else { not_found(res); } }); // actual CDP code CDP.List(options, async function(err, targets) { offset = targets ? targets.length + 1 : 1; const {webSocketDebuggerUrl} = await CDP.Version(options); return await CDP({target: webSocketDebuggerUrl}, function(browser_client) { server.listen({port: offset + options.port, host: options.host }); browser_client.Target.createTarget({url: "about:blank"}).then(function(target) { target_id = target; options.target = target_id.targetId; CDP(options, function(client) { cdp_client = client; var Log = client.Log, Page = client.Page, Runtime = client.Runtime; // enable used CDP domains Promise.all([ Log.enable(), Page.enable(), Runtime.enable() ]).then(function() { // receive and handle all kinds of log and console messages Log.entryAdded(function(entry) { process.stdout.write(entry.entry.level + ': ' + entry.entry.text + "\n"); }); Runtime.consoleAPICalled(function(entry) { var args = entry.args; var stack = null; var i, arg, frame, value; // output actual message for(i = 0; i < args.length; i++) { arg = args[i]; if (arg.type === "string") { value = arg.value; } else { value = JSON.stringify(arg); } process.stdout.write(value); } if (entry.stackTrace && entry.stackTrace.callFrames) { stack = entry.stackTrace.callFrames; } if (entry.type === "error" && stack) { // print full stack for errors process.stdout.write("\n"); for(i = 0; i < stack.length; i++) { frame = stack[i]; if (frame) { value = frame.url + ':' + frame.lineNumer + ':' + frame.columnNumber + '\n'; process.stdout.write(value); } } } }); Runtime.exceptionThrown(function(exception) { var ex = exception.exceptionDetails; var stack = ex.stackTrace.callFrames; var fr; perror(ex.url + ':' + ex.lineNumber + ':' + ex.columnNumber + ': ' + ex.text); for (var i = 0; i < stack.length; i++) { fr = stack[i]; perror(fr.url + ':' + fr.lineNumber + ':' + fr.columnNumber + ': in ' + fr.functionName); } return shutdown(1); }); Page.javascriptDialogOpening((dialog) => { #{ if `dialog.type` == 'prompt' message = gets&.chomp if message `Page.handleJavaScriptDialog({accept: true, promptText: #{message}})` else `Page.handleJavaScriptDialog({accept: false})` end elsif `dialog.type` == 'alert' && `dialog.message` == 'opalheadlessbrowserexit' # A special case of an alert with a magic string "opalheadlessbrowserexit". # This denotes that `Kernel#exit` has been called. We would have rather used # an exception here, but they don't bubble sometimes. %x{ Page.handleJavaScriptDialog({accept: true}); Runtime.evaluate({ expression: "window.OPAL_EXIT_CODE" }).then(function(output) { var exit_code = 0; if (typeof(output.result) !== "undefined" && output.result.type === "number") { exit_code = output.result.value; } return shutdown(exit_code); }); } end } }); Page.loadEventFired(() => { Runtime.evaluate({ expression: "window.OPAL_EXIT_CODE" }).then(function(output) { if (typeof(output.result) !== "undefined" && output.result.type === "number") { return shutdown(output.result.value); } else if (typeof(output.result) !== "undefined" && output.result.type === "string" && output.result.value === "noexit") { // do nothing, we have headless chrome support enabled and there are most probably async events awaiting } else { return shutdown(0); } }) }); Page.navigate({ url: "http://localhost:" + (offset + options.port).toString() + "/index.html" }) }); }); }); }); }); } # end of code (marker to help see if brackets match above)