lib/opal/cli_runners/firefox_cdp_interface.rb

Summary

Maintainability
A
1 hr
Test Coverage
# 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)