Skip to content

Commit

Permalink
Bring back 1.0 features without automount
Browse files Browse the repository at this point in the history
We removed the code, so its easier for Ryan to develop, with no legacy
code to think or worry about. Let's bring it back for the final 2.0
release.

It carries a twist though, I'm disabling the default `/console` mount.
If the users still want the old functionally, they have to explicitly
enable it.
  • Loading branch information
gsamokovarov committed Aug 19, 2014
1 parent 0f8fa3b commit ef508a3
Show file tree
Hide file tree
Showing 34 changed files with 7,092 additions and 20 deletions.
1 change: 1 addition & 0 deletions app/assets/javascripts/web_console/application.js
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
//= require_tree .
172 changes: 172 additions & 0 deletions app/assets/javascripts/web_console/console_sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
//= require web-console

var AJAXTransport = (function(WebConsole) {

var inherits = WebConsole.inherits;
var EventEmitter = WebConsole.EventEmitter;

var FORM_MIME_TYPE = 'application/x-www-form-urlencoded; charset=utf-8';

var AJAXTransport = function(options) {
EventEmitter.call(this);
options || (options = {});

this.url = (typeof options.url === 'string') ? {
input: options.url,
pendingOutput: options.url,
configuration: options.url
} : options.url;

this.pendingInput = '';

this.initializeEventHandlers();
};

inherits(AJAXTransport, EventEmitter);

// Initializes the default event handlers.
AJAXTransport.prototype.initializeEventHandlers = function() {
this.on('input', this.sendInput);
this.on('configuration', this.sendConfiguration);
this.once('initialization', function(cols, rows) {
this.emit('configuration', cols, rows);
this.pollForPendingOutput();
});
};

// Shorthand for creating XHR requests.
AJAXTransport.prototype.createRequest = function(method, url, options) {
options || (options = {});

var request = new XMLHttpRequest;
request.open(method, url);

if (typeof options.form === 'object') {
var content = [], form = options.form;

for (var key in form) {
var value = form[key];
content.push(encodeURIComponent(key) + '=' + encodeURIComponent(value));
}

request.setRequestHeader('Content-Type', FORM_MIME_TYPE);
request.data = content.join('&');
}

return request;
};

AJAXTransport.prototype.pollForPendingOutput = function() {
var request = this.createRequest('GET', this.url.pendingOutput);

var self = this;
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
if (request.status === 200) {
self.emit('pendingOutput', request.responseText);
self.pollForPendingOutput();
} else {
self.emit('disconnect', request);
}
}
};

request.send(null);
};

// Send the input to the server.
//
// Each key press is encoded to an intermediate format, before it is sent to
// the server.
//
// WebConsole#keysPressed is an alias for WebConsole#sendInput.
AJAXTransport.prototype.sendInput = function(input) {
input || (input = '');

if (this.disconnected) return;
if (this.sendingInput) return this.pendingInput += input;

// Indicate that we are starting to send input.
this.sendingInput = true;

var request = this.createRequest('PUT', this.url.input, {
form: { input: this.pendingInput + input }
});

// Clear the pending input.
this.pendingInput = '';

var self = this;
request.onreadystatechange = function() {
if (request.readyState === XMLHttpRequest.DONE) {
self.sendingInput = false;
if (self.pendingInput) self.sendInput();
}
};

request.send(request.data);
};

// Send the terminal configuration to the server.
//
// Right now by configuration, we understand the terminal widht and terminal
// height.
//
// WebConsole#resized is an alias for WebConsole#sendconfiguration.
AJAXTransport.prototype.sendConfiguration = function(cols, rows) {
if (this.disconnected) return;

var request = this.createRequest('PUT', this.url.configuration, {
form: { width: cols, height: rows }
});

// Just send the configuration and don't care about any output.
request.send(request.data);
};

return AJAXTransport;

}).call(this, WebConsole);

window.addEventListener('load', function() {
var geometry = calculateFitScreenGeometry();
config.terminal.cols = geometry[0];
config.terminal.rows = geometry[1];

var terminal = window.terminal = new WebConsole.Terminal(config.terminal);

terminal.on('data', function(data) {
transport.emit('input', data);
});

var transport = new AJAXTransport(config.transport);

transport.on('pendingOutput', function(response) {
var json = JSON.parse(response);
if (json.output) terminal.write(json.output);
});

transport.on('disconnect', function() {
terminal.destroy();
});

transport.emit('initialization', terminal.cols, terminal.rows);

// Utilities
// ---------

function calculateFitScreenGeometry() {
// Currently, resizing term.js is broken. E.g. opening vi causes it to go
// back to 80x24 and fail with off-by-one error. Other stuff, like chip8
// are rendered incorrectly and so on.
//
// To work around it, create a temporary terminal, just so we can get the
// best dimensions for the screen.
var temporary = new WebConsole.Terminal;
try {
return temporary.fitScreen();
} finally {
temporary.destroy();
}
}
});
13 changes: 13 additions & 0 deletions app/assets/stylesheets/web_console/application.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
/*
* This is a manifest file that'll be compiled into application.css, which will include all the files
* listed below.
*
* Any CSS and SCSS file within this directory, lib/assets/stylesheets, vendor/assets/stylesheets,
* or vendor/assets/stylesheets of plugins, if any, can be referenced here using a relative path.
*
* You're free to add application-wide styles to this file and they'll appear at the top of the
* compiled file, but it's generally better to create a new file per style scope.
*
*= require_self
*= require_tree .
*/
6 changes: 6 additions & 0 deletions app/assets/stylesheets/web_console/console_sessions.css.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
<% WebConsole.config.style.instance_eval do %>
body { color: <%= colors.foreground %>; background: <%= colors.background %>; margin: 0; padding: 0; }

.terminal { float: left; overflow: hidden; font: <%= font %>; }
.terminal-cursor { color: <%= colors.background %>; background: <%= colors.foreground %>; }
<% end %>
13 changes: 13 additions & 0 deletions app/controllers/web_console/application_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
module WebConsole
class ApplicationController < ActionController::Base
before_action :prevent_unauthorized_requests!

private

def prevent_unauthorized_requests!
unless request.remote_ip.in?(WebConsole.config.whitelisted_ips)
render nothing: true, status: :unauthorized
end
end
end
end
43 changes: 43 additions & 0 deletions app/controllers/web_console/console_sessions_controller.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
require_dependency 'web_console/application_controller'

module WebConsole
class ConsoleSessionsController < ApplicationController
rescue_from ConsoleSession::Unavailable do |exception|
render json: exception, status: :gone
end

rescue_from ConsoleSession::Invalid do |exception|
render json: exception, status: :unprocessable_entity
end

def index
@console_session = ConsoleSession.create
end

def input
@console_session = ConsoleSession.find(params[:id])
@console_session.send_input(console_session_params[:input])

render nothing: true
end

def configuration
@console_session = ConsoleSession.find(params[:id])
@console_session.configure(console_session_params)

render nothing: true
end

def pending_output
@console_session = ConsoleSession.find(params[:id])

render json: { output: @console_session.pending_output }
end

private

def console_session_params
params.permit(:id, :input, :width, :height)
end
end
end
4 changes: 4 additions & 0 deletions app/helpers/web_console/application_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module WebConsole
module ApplicationHelper
end
end
4 changes: 4 additions & 0 deletions app/helpers/web_console/console_session_helper.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
module WebConsole
module ConsoleSessionHelper
end
end
96 changes: 96 additions & 0 deletions app/models/web_console/console_session.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
module WebConsole
# Manage and persist (in memory) WebConsole::Slave instances.
class ConsoleSession
include ActiveModel::Model

# In-memory storage for the console sessions. Session preservation is
# troubled on servers with multiple workers and threads.
INMEMORY_STORAGE = {}

# Base error class for ConsoleSession specific exceptions.
#
# Provides #to_json implementation, so all subclasses are JSON
# serializable.
class Error < StandardError
def to_json(*)
{ error: to_s }.to_json
end
end

# Raised when trying to find a session that is no longer in the in-memory
# session storage or when the slave process exited.
Unavailable = Class.new(Error)

# Raised when an operation transition to an invalid state.
Invalid = Class.new(Error)

class << self
# Finds a session by its pid.
#
# Raises WebConsole::ConsoleSession::Expired if there is no such session.
def find(pid)
INMEMORY_STORAGE[pid.to_i] or raise Unavailable, 'Session unavailable'
end

# Creates an already persisted consolse session.
#
# Use this method if you need to persist a session, without providing it
# any input.
def create
new.persist
end
end

def initialize
@slave = WebConsole::Slave.new
end

# Explicitly persist the model in the in-memory storage.
def persist
INMEMORY_STORAGE[pid] = self
end

# Returns true if the current session is persisted in the in-memory storage.
def persisted?
self == INMEMORY_STORAGE[pid]
end

# Returns an Enumerable of all key attributes if any is set, regardless if
# the object is persisted or not.
def to_key
[pid] if persisted?
end

private

def delegate_and_call_slave_method(name, *args, &block)
# Cache the delegated method, so we don't have to hit #method_missing
# on every call.
define_singleton_method(name) do |*inner_args, &inner_block|
begin
@slave.public_send(name, *inner_args, &inner_block)
rescue ArgumentError => exc
raise Invalid, exc
rescue Slave::Closed => exc
raise Unavailable, exc
end
end

# Now call the method, since that's our most common use case. Delegate
# the method and than call it.
public_send(name, *args, &block)
end

def method_missing(name, *args, &block)
if @slave.respond_to?(name)
delegate_and_call_slave_method(name, *args, &block)
else
super
end
end

def respond_to_missing?(name, include_all = false)
@slave.respond_to?(name) or super
end
end
end
14 changes: 14 additions & 0 deletions app/views/layouts/web_console/application.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<title>WebConsole</title>
<%= stylesheet_link_tag "web_console/application", media: "all" %>
<%= javascript_include_tag "web_console/application" %>
<%= csrf_meta_tags %>
</head>
<body>

<%= yield %>

</body>
</html>
15 changes: 15 additions & 0 deletions app/views/web_console/console_sessions/index.html.erb
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<script>
var config = {
terminal: {
colors: <%= raw WebConsole.config.style.colors.to_json %>
},

transport: {
url: {
input: "<%= web_console.input_console_session_path(@console_session) %>",
pendingOutput: "<%= web_console.pending_output_console_session_path(@console_session) %>",
configuration: "<%= web_console.configuration_console_session_path(@console_session) %>"
}
}
};
</script>
Loading

0 comments on commit ef508a3

Please sign in to comment.