Browse files

added WebSocket support, happy 1000th commit!

  • Loading branch information...
1 parent 87b708d commit a1a7060e35a6a0965938f00a1971047bfed26b5a Sebastian Riedel committed Jan 24, 2010
View
3 Changes
@@ -7,7 +7,9 @@ This file documents the revision history for Perl extension Mojo.
MOJO_RELOAD=1.
- Changed the testing framework to always run real world tests with
daemon and TCP connections.
+ - Started working on the Mojolicious book.
- Started adding reference documentation. (marcus)
+ - Added WebSocket support.
- Added IPv6 support.
- Added SSL/TLS support.
- Added IDNA support.
@@ -48,6 +50,7 @@ This file documents the revision history for Perl extension Mojo.
- Added buffer size limits to the message parser.
- Added child_status method to Mojo::Server::Daemon::Prefork. (und3f)
- Added header_condition plugin to Mojolicious. (xantus)
+ - Added finish method to Mojolicious::Controller.
- Made all Mojolicious after_* plugin hooks run in reverse order.
- Made param decoding more defensive and allow malformed data to pass
through for debugging.
View
32 lib/Mojo.pm
@@ -13,6 +13,7 @@ use Mojo::Commands;
use Mojo::Home;
use Mojo::Log;
use Mojo::Transaction::Single;
+use Mojo::Transaction::WebSocket;
__PACKAGE__->attr(
build_tx_cb => sub {
@@ -26,6 +27,28 @@ __PACKAGE__->attr(
__PACKAGE__->attr(client => sub { Mojo::Client->new });
__PACKAGE__->attr(home => sub { Mojo::Home->new });
__PACKAGE__->attr(log => sub { Mojo::Log->new });
+__PACKAGE__->attr(
+ websocket_handshake_cb => sub {
+ sub {
+ my ($self, $tx) = @_;
+
+ # Handshake response
+ my $res = $tx->res;
+ $res->code(101);
+ $res->message('Web Socket Protocol Handshake');
+ $res->headers->upgrade('WebSocket');
+ $res->headers->connection('Upgrade');
+ my $scheme =
+ $tx->req->url->to_abs->scheme eq 'https' ? 'wss' : 'ws';
+ $res->headers->websocket_location(
+ $tx->req->url->to_abs->scheme($scheme)->to_string);
+ $res->headers->websocket_origin($tx->req->headers->origin);
+
+ # WenSocket transaction
+ return Mojo::Transaction::WebSocket->new(req => $tx->req);
+ }
+ }
+);
# Oh, so they have internet on computers now!
our $VERSION = '0.999915';
@@ -149,6 +172,15 @@ which stringifies to the actual path.
The logging layer of your application, by default a L<Mojo::Log> object.
+=head2 C<websocket_handshake_cb>
+
+ my $cb = $mojo->websocket_handshake_cb;
+ $mojo = $mojo->websocket_handshake_cb(sub { ... });
+
+The websocket handshake callback, by default it builds a
+L<Mojo::Transaction::WebSocket> object and handles the response for the
+handshake request.
+
=head1 METHODS
L<Mojo> inherits all methods from L<Mojo::Base> and implements the following
View
68 lib/Mojo/Client.pm
@@ -8,7 +8,6 @@ use warnings;
use base 'Mojo::Base';
use bytes;
-use IO::Socket::INET;
use Mojo::ByteStream 'b';
use Mojo::Content::MultiPart;
use Mojo::Content::Single;
@@ -18,8 +17,7 @@ use Mojo::Parameters;
use Mojo::Server::Daemon;
use Mojo::Transaction::Pipeline;
use Mojo::Transaction::Single;
-use Scalar::Util qw/isweak weaken/;
-use Socket;
+use Scalar::Util 'weaken';
__PACKAGE__->attr([qw/app default_cb tls_ca_file tls_verify_cb/]);
__PACKAGE__->attr([qw/continue_timeout max_keep_alive_connections/] => 5);
@@ -54,27 +52,6 @@ sub DESTROY {
sub delete { shift->_build_tx('DELETE', @_) }
-sub generate_port {
- my $self = shift;
-
- # Ports
- my $port = 1 . int(rand 10) . int(rand 10) . int(rand 10) . int(rand 10);
- while ($port++ < 30000) {
-
- # Try port
- return $port
- if IO::Socket::INET->new(
- Listen => 5,
- LocalAddr => '127.0.0.1',
- LocalPort => $port,
- Proto => 'tcp'
- );
- }
-
- # Nothing
- return;
-}
-
sub get { shift->_build_tx('GET', @_) }
sub head { shift->_build_tx('HEAD', @_) }
sub post { shift->_build_tx('POST', @_) }
@@ -300,11 +277,11 @@ sub _drop {
$self->ioloop->not_writing($id);
# Deposit
- my $info = $tx->client_info;
- my $host = $info->{host};
- my $port = $info->{port};
- my $scheme = $info->{scheme};
- $self->_deposit("$scheme:$host:$port", $id) if $tx->keep_alive;
+ my $info = $tx->client_info;
+ my $address = $info->{address};
+ my $port = $info->{port};
+ my $scheme = $info->{scheme};
+ $self->_deposit("$scheme:$address:$port", $id) if $tx->keep_alive;
}
# Connection close
@@ -461,13 +438,15 @@ sub _prepare_server {
# Server
my $server = Mojo::Server::Daemon->new;
- my $port = $self->generate_port;
+ my $port = $self->ioloop->generate_port;
die "Couldn't find a free TCP port for testing.\n" unless $port;
$self->_port($port);
$server->listen("http://*:$port");
ref $self->app
? $server->app($self->app)
: $server->app_class($self->app);
+ $server->app->client->app($server->app);
+ $server->lock_file($server->lock_file . '.test');
$server->prepare_lock_file;
$server->prepare_ioloop;
}
@@ -480,24 +459,25 @@ sub _queue {
my @active = $tx->is_pipeline ? @{$tx->active} : $tx;
for my $active (@active) {
my $url = $active->req->url->to_abs;
+ next if $url->host;
$url->scheme('http');
$url->host('localhost');
$url->port($self->_port);
$active->req->url($url);
}
$tx->client_info(
- {scheme => 'http', host => 'localhost', port => $self->_port})
- if $tx->is_pipeline;
+ {scheme => 'http', address => 'localhost', port => $self->_port})
+ if $tx->is_pipeline && !$tx->client_info->{address};
}
# Cookies from the jar
$self->_fetch_cookies($tx);
# Info
- my $info = $tx->client_info;
- my $host = $info->{host};
- my $port = $info->{port};
- my $scheme = $info->{scheme};
+ my $info = $tx->client_info;
+ my $address = $info->{address};
+ my $port = $info->{port};
+ my $scheme = $info->{scheme};
# Weaken
weaken $self;
@@ -512,7 +492,7 @@ sub _queue {
# Cached connection
my $id;
- if ($id = $self->_withdraw("$scheme:$host:$port")) {
+ if ($id = $self->_withdraw("$scheme:$address:$port")) {
# Writing
$self->ioloop->writing($id);
@@ -532,10 +512,10 @@ sub _queue {
# Connect
$id = $self->ioloop->connect(
- cb => $connected,
- host => $host,
- port => $port,
- tls => $scheme eq 'https' ? 1 : 0,
+ cb => $connected,
+ address => $address,
+ port => $port,
+ tls => $scheme eq 'https' ? 1 : 0,
tls_ca_file => $self->tls_ca_file || $ENV{MOJO_CA_FILE},
tls_verify_cb => $self->tls_verify_cb
);
@@ -814,12 +794,6 @@ As usual, you can pass any of the attributes above to the constructor.
Send a HTTP C<DELETE> request.
-=head2 C<generate_port>
-
- my $port = $client->generate_port;
-
-Find a free TCP port.
-
=head2 C<get>
$client = $client->get('http://kraih.com' => sub {...});
View
67 lib/Mojo/HelloWorld.pm
@@ -28,11 +28,14 @@ sub handler {
my ($self, $tx) = @_;
# Default to 200
- $tx->res->code(200);
+ $tx->res->code(200) unless $tx->is_websocket;
# Dispatch to diagnostics functions
return $self->_diag($tx) if $tx->req->url->path =~ m|^/diag|;
+ # WebSocket?
+ return if $tx->is_websocket;
+
# Hello world!
$tx->res->headers->content_type('text/plain');
$tx->res->body('Congratulations, your Mojo is working!');
@@ -43,12 +46,13 @@ sub _diag {
# Dispatch
my $path = $tx->req->url->path;
- $self->_chunked_params($tx) if $path =~ m|^/diag/chunked_params|;
- $self->_dump_env($tx) if $path =~ m|^/diag/dump_env|;
- $self->_dump_params($tx) if $path =~ m|^/diag/dump_params|;
- $self->_dump_tx($tx) if $path =~ m|^/diag/dump_tx|;
- $self->_dump_url($tx) if $path =~ m|^/diag/dump_url|;
- $self->_proxy($tx) if $path =~ m|^/diag/proxy|;
+ $self->_chunked_params($tx) if $path =~ m|^/diag/chunked_params|;
+ $self->_dump_env($tx) if $path =~ m|^/diag/dump_env|;
+ $self->_dump_params($tx) if $path =~ m|^/diag/dump_params|;
+ $self->_dump_tx($tx) if $path =~ m|^/diag/dump_tx|;
+ $self->_dump_url($tx) if $path =~ m|^/diag/dump_url|;
+ $self->_proxy($tx) if $path =~ m|^/diag/proxy|;
+ return $self->_websocket($tx) if $path =~ m|^/diag/websocket|;
# Defaults
$tx->res->headers->content_type('text/plain')
@@ -66,7 +70,8 @@ sub _diag {
<a href="/diag/dump_params">Dump Request Parameters</a><br />
<a href="/diag/dump_tx">Dump Transaction</a><br />
<a href="/diag/dump_url">Dump Request URL</a><br />
- <a href="/diag/proxy">Proxy</a>
+ <a href="/diag/proxy">Proxy</a><br />
+ <a href="/diag/websocket">WebSocket</a>
</body>
</html>
EOF
@@ -147,7 +152,7 @@ sub _proxy {
}
# Form
- my $url = $tx->req->url->clone;
+ my $url = $tx->req->url->to_abs;
$url->path('/diag/proxy');
$tx->res->headers->content_type('text/html');
$tx->res->body(<<"EOF");
@@ -163,6 +168,50 @@ sub _proxy {
EOF
}
+sub _websocket {
+ my ($self, $tx) = @_;
+
+ # WebSocket request
+ if ($tx->is_websocket) {
+ return $tx->receive_message(
+ sub {
+ my ($tx, $message) = @_;
+ return unless $message eq 'test 123';
+ $tx->send_message("Congratulations, your Mojo is working!");
+ $tx->send_message("With WebSocket support!");
+ }
+ );
+ }
+
+ # WebSocket example
+ my $url = $tx->req->url->to_abs;
+ $url->scheme('ws');
+ $url->path('/diag/websocket');
+ $tx->res->headers->content_type('text/html');
+ $tx->res->body(<<"EOF");
+<!doctype html><html>
+ <head>
+ <title>Mojo Diagnostics</title>
+ <script language="javascript">
+ ws = new WebSocket("$url");
+ function wsmessage(event) {
+ data = event.data;
+ alert(data);
+ }
+ function wsopen(event) {
+ ws.send("test 123");
+ }
+ ws.onmessage = wsmessage;
+ ws.onopen = wsopen;
+ </script>
+ </head>
+ <body>
+ Testing WebSocket support, you should see two popups if it works.
+ </body>
+</html>
+EOF
+}
+
1;
__END__
View
132 lib/Mojo/IOLoop.pm
@@ -77,7 +77,7 @@ sub connect {
# Options (TLS handshake only works blocking)
my %options = (
Blocking => $args->{tls} ? 1 : 0,
- PeerAddr => $args->{host},
+ PeerAddr => $args->{address},
PeerPort => $args->{port} || ($args->{tls} ? 443 : 80),
Proto => 'tcp',
Type => SOCK_STREAM
@@ -100,23 +100,25 @@ sub connect {
# Non blocking
$socket->blocking(0);
- # Timeout
- my $id = $self->timer(
- after => $self->connect_timeout,
- cb => sub {
- shift->_error("$socket", 'Connect timeout.');
- }
- );
-
# Add connection
$self->_connections->{$socket} = {
- buffer => Mojo::Buffer->new,
- connect_cb => $args->{cb},
- connect_timer => $id,
- connecting => 1,
- socket => $socket
+ buffer => Mojo::Buffer->new,
+ connect_cb => $args->{cb},
+ connecting => 1,
+ socket => $socket
};
+ # Timeout
+ my $id = $self->timer(
+ "$socket" => (
+ after => $self->connect_timeout,
+ cb => sub {
+ shift->_error("$socket", 'Connect timeout.');
+ }
+ )
+ );
+ $self->_connections->{$socket}->{connect_timer} = $id;
+
# File descriptor
my $fd = fileno($socket);
$self->_fds->{$fd} = "$socket";
@@ -139,6 +141,16 @@ sub drop {
# Drop timer?
if ($self->_timers->{$id}) {
+ my $c = $self->_timers->{$id}->{connection};
+
+ # Cleanup connection
+ my @timers;
+ for my $timer (@{$self->_connections->{$c}->{timers}}) {
+ next if $timer eq $id;
+ push @timers, $timer;
+ }
+ $self->_connections->{$c}->{timers} = \@timers;
+
delete $self->_timers->{$id};
return $self;
}
@@ -152,14 +164,18 @@ sub drop {
# Not listening
return $self unless $self->_listening;
- # Make sure lock is free
+ # Not listening anymore
$self->_listening(0);
- $self->unlock_cb->($self);
}
# Socket
if (my $socket = $c->{socket}) {
+ # Cleanup timers
+ if (my $timers = $c->{timers}) {
+ for my $timer (@$timers) { $self->drop($timer) }
+ }
+
# Remove file descriptor
my $fd = fileno($socket);
delete $self->_fds->{$fd};
@@ -196,6 +212,27 @@ sub finish {
return $self;
}
+sub generate_port {
+ my $self = shift;
+
+ # Ports
+ my $port = 1 . int(rand 10) . int(rand 10) . int(rand 10) . int(rand 10);
+ while ($port++ < 30000) {
+
+ # Try port
+ return $port
+ if IO::Socket::INET->new(
+ Listen => 5,
+ LocalAddr => '127.0.0.1',
+ LocalPort => $port,
+ Proto => 'tcp'
+ );
+ }
+
+ # Nothing
+ return;
+}
+
sub hup_cb { shift->_add_event('hup', @_) }
# Fat Tony is a cancer on this fair city!
@@ -332,16 +369,27 @@ sub stop { shift->_running(0) }
sub timer {
my $self = shift;
+ my $id = shift;
# Arguments
my $args = ref $_[0] ? $_[0] : {@_};
# Started
$args->{started} = time;
+ # Connection
+ $args->{connection} = $id;
+
+ # Connection doesn't exist
+ return unless $self->_connections->{$id};
+
# Add timer
$self->_timers->{"$args"} = $args;
+ # Bind timer to connection
+ my $timers = $self->_connections->{$id}->{timers} ||= [];
+ push @{$timers}, "$args";
+
return "$args";
}
@@ -388,22 +436,24 @@ sub _accept {
# Accept
my $socket = $listen->accept or return;
- # Timeout
- my $id = $self->timer(
- after => $self->accept_timeout,
- cb => sub {
- shift->_error("$socket", 'Accept timeout.');
- }
- );
-
# Add connection
$self->_connections->{$socket} = {
- accept_timer => $id,
- accepting => 1,
- buffer => Mojo::Buffer->new,
- socket => $socket
+ accepting => 1,
+ buffer => Mojo::Buffer->new,
+ socket => $socket
};
+ # Timeout
+ my $id = $self->timer(
+ "$socket" => (
+ after => $self->accept_timeout,
+ cb => sub {
+ shift->_error("$socket", 'Accept timeout.');
+ }
+ )
+ );
+ $self->_connections->{$socket}->{accept_timer} = $id;
+
# File descriptor
my $fd = fileno($socket);
$self->_fds->{$fd} = "$socket";
@@ -731,7 +781,7 @@ sub _timer {
# Callback
if ((my $cb = $t->{cb}) && $run) {
- $self->$cb("$t");
+ $self->$cb("$t", $t->{connection});
$t->{last} = time;
}
@@ -826,7 +876,7 @@ Mojo::IOLoop - IO Loop
);
# Connect to port 3000 with TLS activated
- my $id = $loop->connect(host => 'localhost', port => 3000, tls => 1);
+ my $id = $loop->connect(address => 'localhost', port => 3000, tls => 1);
# Loop starts writing
$loop->writing($id);
@@ -851,15 +901,15 @@ Mojo::IOLoop - IO Loop
});
# Add a timer
- $loop->timer(after => 5, cb => sub {
- my $self = shift;
- $self->drop($id);
- });
+ $loop->timer($id => (after => 5, cb => sub {
+ my ($self, $tid, $cid) = @_;
+ $self->drop($cid);
+ }));
# Add another timer
- $loop->timer(interval => 3, cb => sub {
+ $loop->timer($id => (interval => 3, cb => sub {
print "Timer is running again!\n";
- });
+ }));
# Start and stop loop
$loop->start;
@@ -987,6 +1037,12 @@ Callback to be invoked if an error event happens on the connection.
Drop a connection gracefully by allowing it to finish writing all data in
it's write buffer.
+=head2 C<generate_port>
+
+ my $port = $loop->generate_port;
+
+Find a free TCP port.
+
=head2 C<hup_cb>
$loop = $loop->hup_cb($id => sub { ... });
@@ -1060,8 +1116,8 @@ Stop the loop immediately.
=head2 C<timer>
- my $id = $loop->timer(after => 5, cb => sub {...});
- my $id = $loop->timer({interval => 5, cb => sub {...}});
+ my $id = $loop->timer($id => (after => 5, cb => sub {...}));
+ my $id = $loop->timer($id => {interval => 5, cb => sub {...}}));
Create a new timer, invoking the callback afer a given amount of seconds.
View
15 lib/Mojo/Server.pm
@@ -68,6 +68,14 @@ __PACKAGE__->attr(
}
);
__PACKAGE__->attr(reload => sub { $ENV{MOJO_RELOAD} || 0 });
+__PACKAGE__->attr(
+ websocket_handshake_cb => sub {
+ sub {
+ my $self = shift;
+ return $self->app->websocket_handshake_cb->($self->app, @_);
+ }
+ }
+);
# Are you saying you're never going to eat any animal again? What about bacon?
# No.
@@ -154,6 +162,13 @@ L<Mojo::Server> implements the following attributes.
my $reload = $server->reload;
$server = $server->reload(1);
+=head2 C<websocket_handshake_cb>
+
+ my $handshake = $server->websocket_handshake_cb;
+ $server = $server->websocket_handshake_cb(sub {
+ my ($self, $tx) = @_;
+ });
+
=head1 METHODS
L<Mojo::Server> inherits all methods from L<Mojo::Base> and implements the
View
88 lib/Mojo/Server/Daemon.pm
@@ -14,7 +14,8 @@ use File::Spec;
use IO::File;
use Mojo::IOLoop;
use Mojo::Transaction::Pipeline;
-use Scalar::Util qw/isweak weaken/;
+use Mojo::Transaction::WebSocket;
+use Scalar::Util 'weaken';
use Sys::Hostname 'hostname';
__PACKAGE__->attr([qw/group listen listen_queue_size user/]);
@@ -214,7 +215,7 @@ sub _create_pipeline {
if ($p->is_finished) {
# Close connection
- if (!$conn->{pipeline}->keep_alive) {
+ if (!$conn->{pipeline}->keep_alive && !$conn->{websocket}) {
$self->_drop($id);
$self->ioloop->finish($id);
}
@@ -246,9 +247,7 @@ sub _create_pipeline {
# Handler callback
$tx->handler_cb(
sub {
-
- # Weaken
- weaken $tx unless isweak $tx;
+ my $tx = shift;
# Handler
$self->handler_cb->($self, $tx);
@@ -258,15 +257,45 @@ sub _create_pipeline {
# Continue handler callback
$tx->continue_handler_cb(
sub {
-
- # Weaken
- weaken $tx;
+ my $tx = shift;
# Continue handler
$self->continue_handler_cb->($self, $tx);
}
);
+ # Upgrade callback
+ $tx->upgrade_cb(
+ sub {
+ my $tx = shift;
+
+ # WebSocket?
+ return unless $tx->req->headers->upgrade =~ /WebSocket/i;
+
+ # WebSocket handshake handler
+ my $ws = $conn->{websocket} =
+ $self->websocket_handshake_cb->($self, $tx);
+
+ # State change callback
+ $ws->state_cb(
+ sub {
+ my $ws = shift;
+
+ # Finish
+ if ($ws->is_finished) {
+ $self->_drop($id);
+ $self->ioloop->finish($id);
+ }
+
+ # Writing?
+ $ws->server_is_writing
+ ? $self->ioloop->writing($id)
+ : $self->ioloop->not_writing($id);
+ }
+ );
+ }
+ );
+
return $tx;
}
);
@@ -365,26 +394,36 @@ sub _listen {
sub _read {
my ($self, $loop, $id, $chunk) = @_;
- # Pipeline
- my $p = $self->_connections->{$id}->{pipeline}
- ||= $self->_create_pipeline($id);
+ # Pipeline?
+ my $tx = $self->_connections->{$id}->{pipeline};
+
+ # WebSocket
+ $tx ||= $self->_connections->{$id}->{websocket};
+
+ # Create pipeline
+ $tx = $self->_connections->{$id}->{pipeline} =
+ $self->_create_pipeline($id)
+ unless $tx;
# Read
- $p->server_read($chunk);
+ $tx->server_read($chunk);
+
+ # WebSocket?
+ return if $tx->is_websocket;
# State machine
- $p->server_spin;
+ $tx->server_spin;
# Add transactions to the pipe for leftovers
- if (my $leftovers = $p->server_leftovers) {
+ if (my $leftovers = $tx->server_leftovers) {
# Read leftovers
- $p->server_read($leftovers);
+ $tx->server_read($leftovers);
}
# Last keep alive request?
- $p->server_tx->res->headers->connection('Close')
- if $p->server_tx
+ $tx->server_tx->res->headers->connection('Close')
+ if $tx->server_tx
&& $self->_connections->{$id}->{requests}
>= $self->max_keep_alive_requests;
}
@@ -393,13 +432,22 @@ sub _write {
my ($self, $loop, $id) = @_;
# Pipeline
- return unless my $p = $self->_connections->{$id}->{pipeline};
+ my $tx = $self->_connections->{$id}->{pipeline};
+
+ # WebSocket
+ $tx ||= $self->_connections->{$id}->{websocket};
+
+ # No transaction
+ return unless $tx;
# Get chunk
- my $chunk = $p->server_get_chunk;
+ my $chunk = $tx->server_get_chunk;
+
+ # WebSocket?
+ return $chunk if $tx->is_websocket;
# State machine
- $p->server_spin;
+ $tx->server_spin;
return $chunk;
}
View
6 lib/Mojo/Transaction.pm
@@ -40,6 +40,8 @@ sub is_paused { shift->is_state('paused') }
sub is_pipeline { return shift->isa('Mojo::Transaction::Pipeline') ? 1 : 0 }
+sub is_websocket { return shift->isa('Mojo::Transaction::WebSocket') ? 1 : 0 }
+
sub pause {
my $self = shift;
@@ -203,6 +205,10 @@ implements the following new ones.
my $is_pipeline = $tx->is_pipeline;
+=head2 C<is_websocket>
+
+ my $is_websocket = $tx->is_websocket;
+
=head2 C<pause>
$tx = $tx->pause;
View
2 lib/Mojo/Transaction/Pipeline.pm
@@ -442,7 +442,7 @@ and implements the following new ones.
=head2 C<client_info>
my $info = $p->client_info;
- $p = $p->client_info({host => 'localhost'});
+ $p = $p->client_info({address => 'localhost'});
=head2 C<client_is_writing>
View
16 lib/Mojo/Transaction/Single.pm
@@ -9,8 +9,9 @@ use base 'Mojo::Transaction';
use Mojo::Message::Request;
use Mojo::Message::Response;
+use Mojo::Transaction::WebSocket;
-__PACKAGE__->attr([qw/continued handler_cb continue_handler_cb/]);
+__PACKAGE__->attr([qw/continue_handler_cb continued handler_cb upgrade_cb/]);
__PACKAGE__->attr(req => sub { Mojo::Message::Request->new });
__PACKAGE__->attr(res => sub { Mojo::Message::Response->new });
@@ -102,7 +103,7 @@ sub client_info {
$scheme ||= 'http';
$port ||= $scheme eq 'https' ? 443 : 80;
- return {host => $host, port => $port, scheme => $scheme};
+ return {address => $host, port => $port, scheme => $scheme};
}
sub client_leftovers {
@@ -360,8 +361,12 @@ sub server_read {
# Writing
$self->state('write');
+ # Upgrade callback
+ my $ws;
+ $ws = $self->upgrade_cb->($self) if $self->req->headers->upgrade;
+
# Handler callback
- $self->handler_cb->($self);
+ $self->handler_cb->($ws ? ($ws, $self) : $self);
}
return $self;
@@ -532,6 +537,11 @@ L<Mojo::Transaction> and implements the following new ones.
my $res = $tx->res;
$tx = $tx->res(Mojo::Message::Response->new);
+=head2 C<upgrade_cb>
+
+ my $cb = $tx->upgrade_cb;
+ $tx = $tx->upgrade_cb(sub {...});
+
=head1 METHODS
L<Mojo::Transaction::Single> inherits all methods from L<Mojo::Transaction>
View
169 lib/Mojo/Transaction/WebSocket.pm
@@ -0,0 +1,169 @@
+# Copyright (C) 2008-2010, Sebastian Riedel.
+
+package Mojo::Transaction::WebSocket;
+
+use strict;
+use warnings;
+
+# I'm not calling you a liar but...
+# I can't think of a way to finish that sentence.
+use base 'Mojo::Transaction';
+
+use Mojo::Buffer;
+use Mojo::ByteStream 'b';
+use Mojo::Message::Request;
+
+__PACKAGE__->attr([qw/read_buffer write_buffer/] => sub { Mojo::Buffer->new }
+);
+__PACKAGE__->attr(
+ receive_message => sub {
+ sub { }
+ }
+);
+__PACKAGE__->attr(req => sub { Mojo::Message::Request->new });
+
+__PACKAGE__->attr(_finished => 0);
+
+sub finish {
+ my $self = shift;
+
+ # Still writing
+ return $self->_finished(1) if $self->write_buffer->size;
+
+ # Finished
+ $self->state('done');
+}
+
+sub send_message {
+ my ($self, $message) = @_;
+
+ # Encode
+ $message = b($message)->encode('UTF-8')->to_string;
+
+ # Add to buffer with framing
+ $self->write_buffer->add_chunk("\x00$message\xff");
+
+ # Writing
+ $self->state('write');
+}
+
+sub server_get_chunk {
+ my $self = shift;
+
+ # Not writing anymore
+ unless ($self->write_buffer->size) {
+ $self->_finished ? $self->state('done') : $self->state('read');
+ }
+
+ # Empty buffer
+ return $self->write_buffer->empty;
+}
+
+# Being eaten by crocodile is just like going to sleep... in a giant blender.
+sub server_read {
+ my ($self, $chunk) = @_;
+
+ # Add chunk
+ my $buffer = $self->read_buffer;
+ $buffer->add_chunk($chunk);
+
+ # Full frames
+ while ((my $i = $buffer->contains("\xff")) >= 0) {
+
+ # Frame
+ my $message = $buffer->remove($i + 1);
+
+ # Remove framing
+ $message =~ s/^[\x00]//;
+ $message =~ s/[\xff]$//;
+
+ # Callback
+ $self->receive_message->(
+ $self, b($message)->decode('UTF-8')->to_string
+ );
+ }
+}
+
+1;
+__END__
+
+=head1 NAME
+
+Mojo::Transaction::WebSocket - WebSocket Transaction Container
+
+=head1 SYNOPSIS
+
+ use Mojo::Transaction::WebSocket;
+
+=head1 DESCRIPTION
+
+L<Mojo::Transaction::WebSocket> is a container for WebSocket transactions.
+
+=head1 ATTRIBUTES
+
+L<Mojo::Transaction::WebSocket> inherits all attributes from
+L<Mojo::Transaction> and implements the following new ones.
+
+=head2 C<read_buffer>
+
+ my $buffer = $ws->read_buffer;
+ $ws = $ws->read_buffer(Mojo::Buffer->new);
+
+Buffer for incoming data.
+
+=head2 C<receive_message>
+
+ my $cb = $ws->receive_message;
+ $ws = $ws->receive_message(sub {...});
+
+The callback that receives decoded messages one by one.
+
+ $ws->receive_message(sub {
+ my ($self, $message) = @_;
+ });
+
+=head2 C<req>
+
+ my $req = $ws->req;
+ $ws = $ws->req(Mojo::Message::Request->new);
+
+The original handshake request.
+
+=head2 C<write_buffer>
+
+ my $buffer = $ws->write_buffer;
+ $ws = $ws->write_buffer(Mojo::Buffer->new);
+
+Buffer for outgoing data.
+
+=head1 METHODS
+
+L<Mojo::Transaction::WebSocket> inherits all methods from
+L<Mojo::Transaction> and implements the following new ones.
+
+=head2 C<finish>
+
+ $ws->finish;
+
+Finish the WebSocket connection gracefully.
+
+=head2 C<send_message>
+
+ $ws->send_message('Hi there!');
+
+Send a message over the WebSocket, encoding and framing will be handled
+transparently.
+
+=head2 C<server_get_chunk>
+
+ my $chunk = $ws->server_get_chunk;
+
+Raw WebSocket data to write, only used by servers.
+
+=head2 C<server_read>
+
+ $ws->server_read($data);
+
+Read raw WebSocket data, only used by servers.
+
+=cut
View
5 lib/Mojolicious.pm
@@ -52,7 +52,7 @@ sub new {
$self->static->root($self->home->rel_dir('public'));
# Hide own controller methods
- $self->routes->hide(qw/client helper param pause redirect_to/);
+ $self->routes->hide(qw/client finish helper param pause redirect_to/);
$self->routes->hide(qw/render_exception render_json render_inner/);
$self->routes->hide(qw/render_not_found render_partial render_static/);
$self->routes->hide(qw/render_text resume url_for/);
@@ -113,7 +113,8 @@ sub dispatch {
elsif ($e) { $c->render_not_found }
# Hook
- $self->plugins->run_hook_reverse(after_dispatch => $c);
+ $self->plugins->run_hook_reverse(after_dispatch => $c)
+ unless $c->tx->is_paused;
}
# Bite my shiny metal ass!
View
19 lib/Mojolicious/Book.pod
@@ -7,8 +7,21 @@ Mojolicious::Book - The Definitive Guide To Mojolicious
=head1 STATUS
This book is currently being written, to get status updates and/or read early
-drafts you can visit L<http://labs.kraih.com>,
-L<http://twitter.com/kraih> and L<http://github.com/kraih/mojo/> or join the
-official IRC channel C<#mojo> on C<irc.perl.org>.
+drafts you can visit L<http://labs.kraih.com>, L<http://twitter.com/kraih>
+and L<http://github.com/kraih/mojo/> or join the official IRC channel
+C<#mojo> on C<irc.perl.org>.
+
+=head1 CHAPTERS
+
+New chapters will be added one by one, so keep a close eye on this.
+
+=over 4
+
+=item L<Mojolicious::Book::CodingGuidelines>
+
+Coding guidelines and mission statement.
+A must read for developers and contributors!
+
+=back
=cut
View
81 lib/Mojolicious/Book/CodingGuidelines.pod
@@ -0,0 +1,81 @@
+# Copyright (C) 2008-2010, Sebastian Riedel.
+
+=head1 NAME
+
+Mojolicious::Book::CodingGuidelines - Coding Guidelines
+
+=head1 OVERVIEW
+
+This document describes the coding guidelines that are the foundations
+of L<Mojo> and L<Mojolicious> development.
+
+Please don't send patches unless you agree with them.
+
+=head1 MISSION STATEMENT
+
+L<Mojo> is a runtime environment for Perl web frameworks.
+It provides all the basic tools and helpers needed to write simple web
+applications and higher level web frameworks such as L<Mojolicious> and
+L<Mojolicious::Lite>.
+
+All components should be reusable in other projects and in a UNIXish way
+only loosely coupled.
+
+Especially for people new to Perl it should be as easy as possible to
+install Mojo and get started.
+Writing web applications can be one of the most fun ways to learn a language!
+
+For developers of other web frameworks it should be possible to reuse all the
+infrastructure and just consider the higher levels of the L<Mojo>
+distribution an example application.
+
+=head1 RULES
+
+=over 4
+
+Keep it simple, no magic unless absolutely necessary.
+
+Code should be written with a Perl6 port in mind.
+
+No refactoring unless a very important feature absolutely depends on it.
+
+It's not a feature without a test.
+
+A feature is only needed when the majority of the userbase benefits from it.
+
+Features may not be changed without being deprecated for at least one major
+release.
+
+Deprecating a feature should be avoided at all costs.
+
+Only add prereqs if absolutely necessary.
+
+Domain specific languages should be avoided in favor of Perl'ish solutions.
+
+No inline POD.
+
+Documentation belongs to the book, module POD is just an API reference.
+
+Lines should not be longer than 78 characters and we indent with 4
+whitespaces.
+
+Code should be run through L<Perl::Tidy> with the included C<.perltidyrc>.
+
+No spaghetti code.
+
+Code should be organized in blocks and those blocks should be commented.
+
+Comments should be funny if possible.
+
+Every file should contain at least one quote from C<The Simpsons> or
+C<Futurama>.
+
+No names outside of the CREDITS section of Mojo.pm.
+
+No Elitism.
+
+Peace!
+
+=back
+
+=cut
View
21 lib/Mojolicious/Controller.pm
@@ -16,6 +16,19 @@ require Carp;
# but then you get to the end and a gorilla starts throwing barrels at you.
sub client { shift->app->client }
+sub finish {
+ my $self = shift;
+
+ # Resume
+ $self->resume;
+
+ # Render
+ $self->app->routes->render($self);
+
+ # Hook
+ $self->app->plugins->run_hook_reverse(after_dispatch => $self);
+}
+
sub helper {
my $self = shift;
@@ -219,6 +232,14 @@ ones.
A L<Mojo::Client> prepared for the current environment.
+=head2 C<finish>
+
+ $c->finish;
+
+Similar to C<resume> but will also trigger automatic rendering and the
+C<after_dispatch> plugin hook, which would normally get disabled once a
+request gets paused.
+
=head2 C<helper>
$c->helper('foo');
View
10 lib/Test/Mojo/Server.pm
@@ -10,8 +10,8 @@ use base 'Mojo::Base';
use File::Spec;
use FindBin;
use IO::Socket::INET;
-use Mojo::Client;
use Mojo::Command;
+use Mojo::IOLoop;
use Mojo::Home;
require Test::More;
@@ -20,9 +20,9 @@ use constant DEBUG => $ENV{MOJO_SERVER_DEBUG} || 0;
__PACKAGE__->attr([qw/command pid/]);
__PACKAGE__->attr(executable => 'mojo');
-__PACKAGE__->attr(home => sub { Mojo::Home->new });
-__PACKAGE__->attr(port => sub { Mojo::Client->new->generate_port });
-__PACKAGE__->attr(timeout => 5);
+__PACKAGE__->attr(home => sub { Mojo::Home->new });
+__PACKAGE__->attr(port => sub { Mojo::IOLoop->singleton->generate_port });
+__PACKAGE__->attr(timeout => 5);
__PACKAGE__->attr('_server');
@@ -42,7 +42,7 @@ sub generate_port_ok {
local $Test::Builder::Level = $Test::Builder::Level + 1;
- my $port = Mojo::Client->new->generate_port;
+ my $port = Mojo::IOLoop->singleton->generate_port;
if ($port) {
Test::More::ok(1, $desc);
return $port;
View
2 t/mojolicious/lite_app.t
@@ -174,8 +174,8 @@ get '/subrequest' => sub {
$self->client->post(
'/template' => sub {
my ($client, $tx) = @_;
- $self->resume;
$self->render_text($tx->res->body);
+ $self->finish;
}
)->process;
};

0 comments on commit a1a7060

Please sign in to comment.