Skip to content

Commit

Permalink
Merge pull request #25 from rails/web-terminal-is-back
Browse files Browse the repository at this point in the history
Bring back 1.0 features without automount
  • Loading branch information
gsamokovarov committed Aug 20, 2014
2 parents 82aa43d + ef508a3 commit 91c828d
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 91c828d

Please sign in to comment.