$stdout.sync = true require 'tempfile' require 'securerandom' require 'greenletters' require "net/http" require "uri" require "open3" require 'therubyracer' module BooJS def self.verify str #Create tmp file tmp = Tempfile.new(SecureRandom.hex) path = tmp.path tmp.close! File.open path, "w" do |f| f.puts %{ phantom.onError = function(msg, trace) { console.log("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); console.log("PhantomJS Error"); console.log("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); console.log(msg); trace.forEach(function(t) { console.log(t.file + ': line ' + t.line ); }) console.log("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); phantom.exit(1); } } f.puts str f.puts "phantom.exit(0)" end begin #Do we have a syntax error? #Open a pipe and check if syntax_error @syntax_error = false Timeout.timeout(8) do Open3.popen3 "#{$phantomjs_path} #{path}" do |_, out, err, _| begin loop do rr, ww = select([out, err], []); e = rr[0] #PhantomJS wrote to stderr r = e.readline if r =~ /SyntaxError/ @syntax_error = true raise "syntax_error" #Break out end end ensure Process.kill :INT, p.pid end end end rescue => e end #PhantomJS does not support syntax checking, so we use V8 to do that return 0 unless @syntax_error begin ctx = V8::Context.new ctx.load path rescue V8::Error => e matches = e.message.match(/.*? at (?.*?):(?.*?):.*?$/) dump_lines matches[:path], matches[:line].to_i, 10 system("node #{path}") #The node tool has a lot more info (ignore if they don't have it) end return 1 end #Restart request class PipeRestart < Exception end #Optionally, accept code to inject and a command to run #If the command is nil, this is not executed as a oneshot def self.pipe(str=nil, cmd=nil, will_timeout=false, output_phantomjs_pid=false) js = %{ //Clear localStorage var system = require('system'); function __spec_ping(str) { system.stdout.writeLine("pong"+str) } phantom.onError = function(msg, trace) { system.stderr.writeLine("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); system.stderr.writeLine("PhantomJS Error"); system.stderr.writeLine("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); system.stderr.writeLine(msg); trace.forEach(function(t) { system.stderr.writeLine(t.file + ': line ' + t.line ); }) system.stderr.writeLine("%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%"); phantom.exit(1); } //@maboiteaspam (https://github.com/ariya/phantomjs/issues/11961) console.error = function(msg) { system.stderr.writeLine(msg) }; function booPing() { console.log("pong"); } } #Any code the user wanted js += "\n#{str}" if str #Repl if cmd js += "\n#{cmd}" js += "\nphantom.exit(1)" unless will_timeout else #You can not use $stdin here, PhantomJS blocks on readLine js += PhantomAsyncInput.code #js += "\nwhile (true) { var line = system.stdin.readLine(); eval(line); }" end #Create file to load tmp = Tempfile.new(SecureRandom.hex) tmp.puts js tmp.close @input_sender_r, @input_sender_w = IO.pipe #Set to true on restarts, for knowing whether to output $__RESTART_OK__ when the restart finishes pipe_restart = false #Phantom JS Process begin p = IO.popen("#{$phantomjs_path} #{tmp.path}") #If it's not a pipe restart, then clear local storage unless pipe_restart @input_sender_w.puts "localStorage.clear();" end #We are going to do a loop back (ready to rcv) to wait for the JS to respond before outputing the spec PID #So that in the specs, we have a reliable way of testing the process execution as ending this process #before the child process ends, will termiante the spawned process (PhantomJS) by default but not if it's #kept alive for a while (which we handle via sending it an INT in the ensure) if output_phantomjs_pid and not pipe_restart #Dont re-emit the PID on pipe restarts @input_sender_w.puts "booPing();" res = p.readline puts p.pid end loop do rr, _ = select([p, STDIN]); e = rr[0] #PhantomJS has written something if e == p res = e.readline if res =~ /STDIN_PORT/ port = res.split(" ")[1].to_i unless @in_port start_server(@input_sender_r) end @in_port = port #Tell stdout that we did restart if pipe_restart pipe_restart = false puts "$__RESTART_OK__" $stdout.flush end else puts res $stdout.flush end end #User has written to this program if e == STDIN res = e.readline if res == "$__RESTART__\n" #You must wait several seconds for fsync sleep 2 pipe_restart = true raise PipeRestart else @input_sender_w.puts res end end end rescue PipeRestart Process.kill :INT, p.pid retry ensure Process.kill :INT, p.pid end tmp.unlink exit ret rescue EOFError => e exit 1 end def self.start_server pipe #Server that manages inputs Thread.new do begin loop do rr, __ = IO.select([@input_sender_r], []); e = rr[0] uri = URI.parse("http://localhost:#{@in_port}") Net::HTTP.post_form(uri, {:str => e.readline}) end rescue Exception => e $stderr.puts "Input server exception: #{e.inspect}" end end end #Show the lines of somethingl ike a source file will #show around center_num where it has n_expand lines around #the center_num. If you pass msg, it will output the #information next to the arrow # ----------------------------------------------- # my_source.js # ----------------------------------------------- # 2| pretend_to_do_something(); # 3| console.log("hello world"); # 4| console.log("hello world again"]; <-------- [optional msg] # 5| var x = 4; # 6| var y = 3; # ----------------------------------------------- def self.dump_lines fn, center_num, n_expand, msg=nil f = File.open(fn) range = (center_num-n_expand)..(center_num+n_expand) $stderr.puts "------------------------------------------------------------" $stderr.puts "#{File.basename(fn)}:#{center_num}" $stderr.puts "------------------------------------------------------------" f.each_line.with_index do |line, index| line.strip! if range.include? index if index == center_num $stderr.puts "#{index}| #{line} <------ " + (msg ? "[#{msg}]" : "") else $stderr.puts "#{index}| #{line}" end end end $stderr.puts "------------------------------------------------------------" end end #You can not use stdin readLine from PhantomJS because it blocks. This is a workaround where #commands are queued via the built-in server and then evaluated asynchronously via setInterval. #This method is used because exceptions that occur inside the calling context of the server response #cause the server to crash without notifying the application via phantom.onError module PhantomAsyncInput def self.code %{ __async_stdin_queue = [] __webserver = require('webserver'); __server = __webserver.create(); __port = Math.floor((Math.random() * 3000) + 3000); console.log("STDIN_PORT " + __port) __service = __server.listen(__port, function(req, res) { //Reply var str = req.post.str __async_stdin_queue.unshift(str) res.write("ok"); res.close(); }); function __async_stdin_dequeue() { //Nothing to process if (__async_stdin_queue.length == 0) { return; } var str = __async_stdin_queue.pop() eval.call(window, str); } setInterval(__async_stdin_dequeue, 50); } end end