Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bring back 1.0 features without automount #25

Merged
merged 1 commit into from
Aug 20, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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