+
+ [% i18n('Fork_ProgressBar', 'current asset') %]:
+ (
/
).
+ [% i18n('Fork_ProgressBar', 'time elapsed') %]:
+
+ [% i18n('Fork_ProgressBar', 'seconds') %].
+
+
+
+TEMPLATE
+
+my $stylesheet = <<'STYLESHEET';
+
+STYLESHEET
+
+#-------------------------------------------------------------------
+
+=head2 handler ( process )
+
+See WebGUI::Operation::Fork.
+
+=cut
+
+sub handler {
+ my $process = shift;
+ my $session = $process->session;
+ my $style = $session->style;
+ my $url = $session->url;
+ $style->setRawHeadTags($stylesheet);
+ $style->setScript($url->extras('underscore/underscore-min.js'));
+ WebGUI::Fork::ProgressBar::renderBar($process, $template);
+}
+
+1;
diff --git a/lib/WebGUI/Fork/Status.pm b/lib/WebGUI/Fork/Status.pm
new file mode 100644
index 000000000..06c2ebd3b
--- /dev/null
+++ b/lib/WebGUI/Fork/Status.pm
@@ -0,0 +1,84 @@
+package WebGUI::Fork::Status;
+
+use JSON;
+
+=head1 LEGAL
+
+ -------------------------------------------------------------------
+ WebGUI is Copyright 2001-2009 Plain Black Corporation.
+ -------------------------------------------------------------------
+ Please read the legal notices (docs/legal.txt) and the license
+ (docs/license.txt) that came with this distribution before using
+ this software.
+ -------------------------------------------------------------------
+ http://www.plainblack.com info@plainblack.com
+ -------------------------------------------------------------------
+
+=cut
+
+use strict;
+use warnings;
+
+=head1 NAME
+
+WebGUI::Fork::Status
+
+=head1 DESCRIPTION
+
+Returns a json response of the following form:
+
+ {
+ "finished" : true,
+ "elapsed" : 10,
+ "status" : "whatever is in the status field. Could be anything.",
+ "error" : "whatever is in the error field"
+ }
+
+Note that if your status is JSON, you'll have to decode that seperately, so
+something like:
+
+ decoded = JSON.parse(r.responseText);
+ status = JSON.parse(decoded.status);
+
+Finished is obviously true or false. Notably, it will be true in the error
+case: so to status.finished && !status.error means successful completion.
+Error will only be present if the process died for some reason.
+
+Status will always be present, mostly so you can see what the last status was
+before it died.
+
+Elapsed will be the number of seconds since the process started (or until the
+process finished, if it is finished).
+
+=head1 SUBROUTINES
+
+These subroutines are available from this package:
+
+=cut
+
+#-------------------------------------------------------------------
+
+=head2 handler ( process )
+
+See the synopsis for what kind of response this generates.
+
+=cut
+
+sub handler {
+ my $process = shift;
+ my $status = $process->getStatus;
+ my ( $finished, $startTime, $endTime, $error ) = $process->get( 'finished', 'startTime', 'endTime', 'error' );
+
+ $endTime = time() unless $finished;
+
+ my %status = (
+ status => $status,
+ elapsed => ( $endTime - $startTime ),
+ finished => ( $finished ? \1 : \0 ),
+ );
+ $status{error} = $error if $finished;
+ $process->session->http->setMimeType('text/plain');
+ JSON::encode_json( \%status );
+} ## end sub handler
+
+1;
diff --git a/lib/WebGUI/Operation.pm b/lib/WebGUI/Operation.pm
index 4c102fb75..7c9a9204b 100644
--- a/lib/WebGUI/Operation.pm
+++ b/lib/WebGUI/Operation.pm
@@ -76,6 +76,7 @@ Returns a hash reference containing operation and package names.
sub getOperations {
return {
+ 'fork' => 'Fork',
'killSession' => 'ActiveSessions',
'viewActiveSessions' => 'ActiveSessions',
diff --git a/lib/WebGUI/Operation/Fork.pm b/lib/WebGUI/Operation/Fork.pm
new file mode 100644
index 000000000..ceeadda54
--- /dev/null
+++ b/lib/WebGUI/Operation/Fork.pm
@@ -0,0 +1,74 @@
+package WebGUI::Operation::Fork;
+
+=head1 LEGAL
+
+ -------------------------------------------------------------------
+ WebGUI is Copyright 2001-2009 Plain Black Corporation.
+ -------------------------------------------------------------------
+ Please read the legal notices (docs/legal.txt) and the license
+ (docs/license.txt) that came with this distribution before using
+ this software.
+ -------------------------------------------------------------------
+ http://www.plainblack.com info@plainblack.com
+ -------------------------------------------------------------------
+
+=cut
+
+use strict;
+use warnings;
+
+use WebGUI::Fork;
+use WebGUI::Pluggable;
+
+=head1 NAME
+
+WebGUI::Operation::Fork
+
+=head1 DESCRIPTION
+
+URL dispatching for WebGUI::Fork monitoring
+
+=head1 SUBROUTINES
+
+These subroutines are available from this package:
+
+=cut
+
+#-------------------------------------------------------------------
+
+=head2 www_fork ( session )
+
+Dispatches to the proper module based on the module form parameter if op is
+fork. Returns insufficient privilege page if the user doesn't pass canView on
+the process before dispatching.
+
+=cut
+
+sub www_fork {
+ my $session = shift;
+ my $form = $session->form;
+ my $module = $form->get('module') || 'Status';
+ my $pid = $form->get('pid') || return undef;
+
+ my $process = WebGUI::Fork->new( $session, $pid );
+
+ return $session->privilege->insufficient unless $process->canView;
+
+ my $log = $session->log;
+
+ unless ($process) {
+ $log->error("Tried to get info for nonexistent process $pid");
+ return undef;
+ }
+
+ my $output = eval { WebGUI::Pluggable::run( "WebGUI::Fork::$module", 'handler', [$process] ); };
+
+ if ($@) {
+ $log->error($@);
+ return undef;
+ }
+
+ return $output;
+} ## end sub www_fork
+
+1;
diff --git a/lib/WebGUI/Operation/VersionTag.pm b/lib/WebGUI/Operation/VersionTag.pm
index 4f2caedc2..214b15af6 100644
--- a/lib/WebGUI/Operation/VersionTag.pm
+++ b/lib/WebGUI/Operation/VersionTag.pm
@@ -21,6 +21,9 @@ use WebGUI::International;
use WebGUI::VersionTag;
use WebGUI::HTMLForm;
use WebGUI::Paginator;
+use WebGUI::Fork;
+use Monkey::Patch;
+use JSON;
=head1 NAME
@@ -137,6 +140,50 @@ sub getVersionTagOptions {
return %tag;
}
+#----------------------------------------------------------------------------
+
+=head2 rollbackInFork ($process, $tagId)
+
+WebGUI::Fork method called by www_rollbackVersionTag
+
+=cut
+
+sub rollbackInFork {
+ my ( $process, $tagId ) = @_;
+ my $session = $process->session;
+ my $tag = WebGUI::VersionTag->new( $session, $tagId );
+ my %status = (
+ finished => 0,
+ total => $process->session->db->quickScalar( 'SELECT count(*) FROM assetData WHERE tagId = ?', [$tagId] ),
+ message => '',
+ );
+ my $update = sub {
+ $process->update( sub { JSON::encode_json( \%status ) } );
+ };
+ my $patch = Monkey::Patch::patch_class(
+ 'WebGUI::Asset',
+ 'purgeRevision',
+ sub {
+ my $purgeRevision = shift;
+ my $self = shift;
+ $self->$purgeRevision(@_);
+ $status{finished}++;
+ $update->();
+ }
+ );
+ $tag->rollback( {
+ outputSub => sub {
+ $status{message} = shift;
+ $update->();
+ }
+ }
+ );
+
+ # need to get at least one of these in for the degenerate case of no
+ # revisions in tag
+ $update->();
+} ## end sub rollbackInFork
+
#-------------------------------------------------------------------
=head2 www_approveVersionTag ( session )
@@ -853,16 +900,27 @@ sub www_rollbackVersionTag {
return $session->privilege->adminOnly() unless canView($session);
my $tagId = $session->form->process("tagId");
return $session->privilege->vitalComponent() if ($tagId eq "pbversion0000000000001");
- my $pb = WebGUI::ProgressBar->new($session);
- my $i18n = WebGUI::International->new($session, 'VersionTag');
- $pb->start($i18n->get('rollback version tag'), $session->url->extras('adminConsole/versionTags.gif'));
- if ($tagId) {
- my $tag = WebGUI::VersionTag->new($session, $tagId);
- $tag->rollback({ outputSub => sub { $pb->update(@_) }, }) if defined $tag;
- }
+
+ my $process = WebGUI::Fork->start(
+ $session, 'WebGUI::Operation::VersionTag', 'rollbackInFork', $tagId
+ );
+
+ my $i18n = WebGUI::International->new($session, 'VersionTag');
my $method = $session->form->process("proceed");
$method = $method eq "manageCommittedVersions" ? $method : 'manageVersions';
- $pb->finish(WebGUI::Asset->getDefault($session)->getUrl('op='.$method));
+ my $redir = WebGUI::Asset->getDefault($session)->getUrl("op=$method");
+ $session->http->setRedirect(
+ $session->url->page(
+ $process->contentPairs(
+ 'ProgressBar', {
+ icon => 'versions',
+ title => $i18n->get('rollback version tag'),
+ proceed => $redir,
+ }
+ )
+ )
+ );
+ return 'redirect';
}
diff --git a/lib/WebGUI/ProgressTree.pm b/lib/WebGUI/ProgressTree.pm
new file mode 100644
index 000000000..079907ba5
--- /dev/null
+++ b/lib/WebGUI/ProgressTree.pm
@@ -0,0 +1,172 @@
+package WebGUI::ProgressTree;
+
+use warnings;
+use strict;
+
+=head1 NAME
+
+WebGUI::ProgressTree
+
+=head1 DESCRIPTION
+
+Helper functions for maintaining a JSON represtentation of the progress of an
+operation that modifies a tree of assets. See WebGUI::Fork::ProgressTree for a
+status page that renders this.
+
+=head1 SYNOPSIS
+
+ my $tree = WebGUI::ProgressTree->new($session, \@assetIds);
+ $tree->success($assetId);
+ $tree->failure($assetId, $reason);
+ $tree->note($assetId, 'something about this one...');
+
+=head1 LEGAL
+
+ -------------------------------------------------------------------
+ WebGUI is Copyright 2001-2009 Plain Black Corporation.
+ -------------------------------------------------------------------
+ Please read the legal notices (docs/legal.txt) and the license
+ (docs/license.txt) that came with this distribution before using
+ this software.
+ -------------------------------------------------------------------
+ http://www.plainblack.com info@plainblack.com
+ -------------------------------------------------------------------
+
+=head1 METHODS
+
+=cut
+
+#-------------------------------------------------------------------
+
+=head2 new ($session, $assetIds)
+
+Constructs new tree object for tracking the progress of $assetIds.
+
+=cut
+
+sub new {
+ my ( $class, $session, $assetIds ) = @_;
+ my $db = $session->db;
+ my $dbh = $db->dbh;
+ my $set = join( ',', map { $dbh->quote($_) } @$assetIds );
+ my $sql = qq{
+ SELECT a.assetId, a.parentId, d.url
+ FROM asset a INNER JOIN assetData d ON a.assetId = d.assetId
+ WHERE a.assetId IN ($set)
+ ORDER BY a.lineage ASC, d.revisionDate DESC
+ };
+ my $sth = $db->read($sql);
+ my ( %flat, @roots );
+
+ while ( my $asset = $sth->hashRef ) {
+ my ( $id, $parentId ) = delete @{$asset}{ 'assetId', 'parentId' };
+
+ # We'll get back multiple rows for each asset, but the first one is
+ # the latest. Skip the others.
+ next if $flat{$id};
+ $flat{$id} = $asset;
+ if ( my $parent = $flat{$parentId} ) {
+ push( @{ $parent->{children} }, $asset );
+ }
+ else {
+ push( @roots, $asset );
+ }
+ }
+ my $self = {
+ session => $session,
+ tree => \@roots,
+ flat => \%flat,
+ };
+ bless $self, $class;
+} ## end sub new
+
+#-------------------------------------------------------------------
+
+=head2 success ($assetId)
+
+Whatever we were doing to $assetId succeeded. Woohoo!
+
+=cut
+
+sub success {
+ my ( $self, $assetId ) = @_;
+ $self->{flat}->{$assetId}->{success} = 1;
+}
+
+#-------------------------------------------------------------------
+
+=head2 failure ($assetId, $reason)
+
+Whatever we were doing to $assetId didn't work for $reason. Aww.
+
+=cut
+
+sub failure {
+ my ( $self, $assetId, $reason ) = @_;
+ $self->{flat}->{$assetId}->{failure} = $reason;
+}
+
+#-------------------------------------------------------------------
+
+=head2 note ($assetId, $note)
+
+Add some extra text. WebGUI::Fork::ProgressTree displays these as paragraphs
+under the node for this asset.
+
+=cut
+
+sub note {
+ my ( $self, $assetId, $note ) = @_;
+ push( @{ $self->{flat}->{$assetId}->{notes} }, $note );
+}
+
+#-------------------------------------------------------------------
+
+=head2 focus ($assetId)
+
+Make a note that this is the asset that we are currently doing something with.
+
+=cut
+
+sub focus {
+ my ( $self, $assetId ) = @_;
+ if ( my $last = delete $self->{last} ) {
+ delete $last->{focus};
+ }
+ if ($assetId) {
+ my $focus = $self->{last} = $self->{flat}->{$assetId};
+ $focus->{focus} = 1;
+ }
+}
+
+#-------------------------------------------------------------------
+
+=head2 tree
+
+A hashy representation of the status of this tree of assets.
+
+=cut
+
+sub tree { $_[0]->{tree} }
+
+#-------------------------------------------------------------------
+
+=head2 json
+
+$self->tree encoded as json.
+
+=cut
+
+sub json { JSON::encode_json( $_[0]->tree ) }
+
+#-------------------------------------------------------------------
+
+=head2 session
+
+The WebGUI::Session this progress tree is associated with.
+
+=cut
+
+sub session { $_[0]->{session} }
+
+1;
diff --git a/lib/WebGUI/VersionTag.pm b/lib/WebGUI/VersionTag.pm
index cc5b8afb5..eba44462a 100644
--- a/lib/WebGUI/VersionTag.pm
+++ b/lib/WebGUI/VersionTag.pm
@@ -37,6 +37,23 @@ These methods are available from this class:
=cut
+#-------------------------------------------------------------------
+
+=head2 autoCommitUrl ( $base )
+
+Returns the url autoCommitWorkingIfEnabled would redirect to if it were going
+to.
+
+=cut
+
+sub autoCommitUrl {
+ my $self = shift;
+ my $session = $self->session;
+ my $url = $session->url;
+ my $base = shift || $url->page;
+ my $id = $self->getId;
+ return $url->append($base, "op=commitVersionTag;tagId=$id");
+}
#-------------------------------------------------------------------
@@ -75,25 +92,13 @@ sub autoCommitWorkingIfEnabled {
return undef
unless $versionTag;
- #Auto commit is no longer determined from autoRequestCommit
-
- # auto commit assets
- # save and commit button and site wide auto commit work the same
- # Do not auto commit if tag is system wide tag or tag belongs to someone else
- if (
- $options->{override}
- || ( $class->getVersionTagMode($session) eq q{autoCommit}
- && ! $versionTag->get(q{isSiteWide})
- && $versionTag->get(q{createdBy}) eq $session->user()->userId()
- )
- ) {
+ if ($options->{override} || $versionTag->canAutoCommit) {
if ($session->setting->get("skipCommitComments") || !$options->{allowComments}) {
$versionTag->requestCommit;
return 'commit';
}
else {
- my $url = $options->{returnUrl} || $session->url->page;
- $url = $session->url->append($url, "op=commitVersionTag;tagId=" . $versionTag->getId);
+ my $url = $versionTag->autoCommitUrl($options->{returnUrl});
$session->http->setRedirect($url);
return 'redirect';
}
@@ -103,6 +108,24 @@ sub autoCommitWorkingIfEnabled {
#-------------------------------------------------------------------
+=head2 canAutoCommit
+
+Returns true if we would autocommit this tag without an override.
+
+=cut
+
+sub canAutoCommit {
+ my $self = shift;
+ my $session = $self->session;
+ my $class = ref $self;
+ my $mode = $class->getVersionTagMode($session);
+ return $mode eq 'autoCommit'
+ && !$self->get('isSiteWide')
+ && $self->get('createdBy') eq $session->user->userId;
+}
+
+#-------------------------------------------------------------------
+
=head2 clearWorking ( )
Makes it so this tag is no longer the working tag for any user.
diff --git a/lib/WebGUI/Workflow/Activity/RemoveOldForks.pm b/lib/WebGUI/Workflow/Activity/RemoveOldForks.pm
new file mode 100644
index 000000000..209d29341
--- /dev/null
+++ b/lib/WebGUI/Workflow/Activity/RemoveOldForks.pm
@@ -0,0 +1,82 @@
+package WebGUI::Workflow::Activity::RemoveOldForks;
+
+=head1 LEGAL
+
+ -------------------------------------------------------------------
+ WebGUI is Copyright 2001-2009 Plain Black Corporation.
+ -------------------------------------------------------------------
+ Please read the legal notices (docs/legal.txt) and the license
+ (docs/license.txt) that came with this distribution before using
+ this software.
+ -------------------------------------------------------------------
+ http://www.plainblack.com info@plainblack.com
+ -------------------------------------------------------------------
+
+=cut
+
+use warnings;
+use strict;
+
+use base 'WebGUI::Workflow::Activity';
+
+use WebGUI::International;
+use WebGUI::Fork;
+
+=head1 NAME
+
+WebGUI::Workflow::Activity::RemoveOldForks
+
+=head1 DESCRIPTION
+
+Remove forks that are older than a configurable threshold.
+
+=head1 METHODS
+
+These methods are available from this class:
+
+=cut
+
+#-------------------------------------------------------------------
+
+=head2 definition ( session, definition )
+
+See WebGUI::Workflow::Activity::definition() for details.
+
+=cut
+
+sub definition {
+ my ( $class, $session, $definition ) = @_;
+ my $i18n = WebGUI::International->new( $session, 'Workflow_Activity_RemoveOldForks' );
+ my %def = (
+ name => $i18n->get('activityName'),
+ properties => {
+ interval => {
+ fieldType => 'interval',
+ label => $i18n->get('interval'),
+ defaultValue => 60 * 60 * 24 * 7,
+ hoverHelp => $i18n->get('interval help')
+ }
+ }
+ );
+ push @$definition, \%def;
+ return $class->SUPER::definition( $session, $definition );
+} ## end sub definition
+
+#-------------------------------------------------------------------
+
+=head2 execute ( [ object ] )
+
+See WebGUI::Workflow::Activity::execute() for details.
+
+=cut
+
+sub execute {
+ my $self = shift;
+ my $db = $self->session->db;
+ my $tbl = $db->dbh->quote_identifier( WebGUI::Fork->tableName );
+ my $time = time - $self->get('interval');
+ $db->write( "DELETE FROM $tbl WHERE endTime <= ?", [$time] );
+ return $self->COMPLETE;
+}
+
+1;
diff --git a/lib/WebGUI/i18n/English/Fork_ProgressBar.pm b/lib/WebGUI/i18n/English/Fork_ProgressBar.pm
new file mode 100644
index 000000000..8556eb06e
--- /dev/null
+++ b/lib/WebGUI/i18n/English/Fork_ProgressBar.pm
@@ -0,0 +1,22 @@
+package WebGUI::i18n::English::Fork_ProgressBar;
+
+use strict;
+
+our $I18N = {
+ 'time elapsed' => {
+ message => 'Time Elapsed',
+ lastUpdated => 1286466369,
+ context => 'Used as a label to indicate how many seconds have gone by since the forked process started running',
+ },
+ 'seconds' => {
+ message => 'seconds',
+ lastUpdated => 1286466433,
+ },
+ 'current asset' => {
+ message => 'Current Asset',
+ lastUpdated => 1286466701,
+ context => 'Used as a label to indicate which asset is in "focus"',
+ },
+};
+
+1;
diff --git a/lib/WebGUI/i18n/English/Workflow_Activity_RemoveOldForks.pm b/lib/WebGUI/i18n/English/Workflow_Activity_RemoveOldForks.pm
new file mode 100644
index 000000000..c93ebe4d2
--- /dev/null
+++ b/lib/WebGUI/i18n/English/Workflow_Activity_RemoveOldForks.pm
@@ -0,0 +1,20 @@
+package WebGUI::i18n::English::Workflow_Activity_RemoveOldForks;
+
+use strict;
+
+our $I18N = {
+ 'interval help' => {
+ message => 'How long do we wait after process completion before deleting it?',
+ lastUpdated => 1285358250,
+ },
+ 'interval' => {
+ message => q|Interval|,
+ lastUpdated => 1285358250,
+ },
+ 'activityName' => {
+ message => q|Remove Old Forks|,
+ lastUpdated => 1285358250,
+ },
+};
+
+1;
diff --git a/sbin/preload.perl b/sbin/preload.perl
index 4e54f6820..0adbef505 100644
--- a/sbin/preload.perl
+++ b/sbin/preload.perl
@@ -17,19 +17,6 @@ unshift @INC, grep {
}
} readLines($webguiRoot."/sbin/preload.custom");
-#----------------------------------------
-# Logger
-#----------------------------------------
-require Log::Log4perl;
-Log::Log4perl->init( $webguiRoot."/etc/log.conf" );
-
-#----------------------------------------
-# Database connectivity.
-#----------------------------------------
-#require Apache::DBI; # Uncomment if you want to enable connection pooling. Not recommended on servers with many sites, or those using db slaves.
-require DBI;
-DBI->install_driver("mysql"); # Change to match your database driver.
-
#----------------------------------------
# WebGUI modules.
#----------------------------------------
@@ -48,6 +35,29 @@ WebGUI::Pluggable::findAndLoad( "WebGUI",
}
);
+#----------------------------------------
+# Preload all site configs.
+#----------------------------------------
+WebGUI::Config->loadAllConfigs($webguiRoot);
+
+#----------------------------------------
+# WebGUI::Fork initialization
+#----------------------------------------
+WebGUI::Fork->init();
+
+#----------------------------------------
+# Logger
+#----------------------------------------
+require Log::Log4perl;
+Log::Log4perl->init( $webguiRoot."/etc/log.conf" );
+
+#----------------------------------------
+# Database connectivity.
+#----------------------------------------
+#require Apache::DBI; # Uncomment if you want to enable connection pooling. Not recommended on servers with many sites, or those using db slaves.
+require DBI;
+DBI->install_driver("mysql"); # Change to match your database driver.
+
require APR::Request::Apache2;
require Apache2::Cookie;
require Apache2::ServerUtil;
@@ -64,12 +74,6 @@ $| = 1;
print "\nStarting WebGUI ".$WebGUI::VERSION."\n";
-#----------------------------------------
-# Preload all site configs.
-#----------------------------------------
-WebGUI::Config->loadAllConfigs($webguiRoot);
-
-
# reads lines from a file into an array, trimming white space and ignoring commented lines
sub readLines {
my $file = shift;
diff --git a/t/Asset/AssetClipboard.t b/t/Asset/AssetClipboard.t
index 8eb2bbb1c..526425e50 100644
--- a/t/Asset/AssetClipboard.t
+++ b/t/Asset/AssetClipboard.t
@@ -20,6 +20,7 @@ use WebGUI::Session;
use WebGUI::Utility;
use WebGUI::Asset;
use WebGUI::VersionTag;
+use Test::MockObject;
use Test::More; # increment this value for each test you create
plan tests => 29;
@@ -148,12 +149,20 @@ sub copied {
return undef;
}
-my @methods = qw(Single Children Descendants);
+my $process = Test::MockObject->new->mock(update => sub {});
+my @methods = (
+ # single duplicate doesn't fork, so we can just test the www method to
+ # make sure it gets it right
+ sub { shift->www_copy },
+ sub { shift->duplicateBranch(1, 'clipboard') },
+ sub { shift->duplicateBranch(0, 'clipboard') },
+);
+my @prefixes = qw(single children descendants);
for my $i (0..2) {
- my $meth = "_wwwCopy$methods[$i]";
+ my $meth = $methods[$i];
$root->$meth();
my $clip = copied();
- is_tree_of_folders($clip, $i+1, $meth);
+ is_tree_of_folders($clip, $i+1, @prefixes[$i]);
$clip->purge;
}
diff --git a/t/Fork.t b/t/Fork.t
new file mode 100644
index 000000000..8e9d51909
--- /dev/null
+++ b/t/Fork.t
@@ -0,0 +1,106 @@
+# vim:syntax=perl
+#-------------------------------------------------------------------
+# WebGUI is Copyright 2001-2009 Plain Black Corporation.
+#-------------------------------------------------------------------
+# Please read the legal notices (docs/legal.txt) and the license
+# (docs/license.txt) that came with this distribution before using
+# this software.
+#------------------------------------------------------------------
+# http://www.plainblack.com info@plainblack.com
+#------------------------------------------------------------------
+
+# WebGUI::Fork tests
+
+use strict;
+use warnings;
+
+use FindBin;
+use lib "$FindBin::Bin/lib";
+use lib "$FindBin::Bin/../lib";
+
+use Test::More;
+use Test::Deep;
+use Data::Dumper;
+use JSON;
+
+use WebGUI::Test;
+use WebGUI::Session;
+use WebGUI::Fork;
+
+my $class = 'WebGUI::Fork';
+my $testClass = 'WebGUI::Test::Fork';
+my $pipe = $class->init();
+my $session = WebGUI::Test->session;
+
+# test simplest (non-forking) case
+
+my $process = $class->create($session);
+my $request = $process->request( $testClass, 'simple', ['data'] );
+
+cmp_bag(
+ [ keys %$request ],
+ [qw(webguiRoot configFile sessionId id module subname data)],
+ 'request hash has the right keys'
+);
+
+my $now = time;
+
+$class->runRequest($request);
+ok $process->isFinished, 'finished';
+my $error = $process->getError;
+ok( !$error, 'no errors' ) or diag " Expected nothing, got: $error\n";
+$process->setWait(0);
+is $process->getStatus, 'data', 'proper status';
+my $started = $process->startTime;
+ok( ( $started >= $now ), 'sane startTime' );
+ok( ( $process->endTime >= $started ), 'sane endTime' );
+
+$process->delete;
+
+note "Testing error case\n";
+$process = $class->create($session);
+$request = $process->request( $testClass, 'error', ['error'] );
+$class->runRequest($request);
+ok $process->isFinished, 'finished';
+is $process->getError, "error\n", 'has error code';
+$process->setWait(0);
+my $status = $process->getStatus;
+ok( !$status, 'no discernable status' ) or diag $status;
+ok( ( $process->endTime >= $started ), 'sane endTime' );
+
+my $forkCount = 0;
+my $forkAndExec = $class->can('forkAndExec');
+my $replace = sub {
+ my $self = shift;
+ $forkCount++;
+ $self->$forkAndExec(@_);
+};
+
+{
+ no strict 'refs';
+ no warnings 'redefine';
+ *{ $class . '::forkAndExec' } = $replace;
+}
+
+sub backgroundTest {
+ note "$_[0]\n";
+ $process = $class->start( $session, $testClass, 'complex', ['data'] );
+ my $sleeping;
+ while ( !$process->isFinished && $sleeping++ < 10 ) {
+ sleep 1;
+ }
+ ok $process->isFinished, 'finished';
+ is $process->getStatus, 'baz', 'correct status'
+ or diag $process->getError . "\n";
+
+ $process->delete;
+}
+backgroundTest('talk to background');
+is $forkCount, 0, 'we did not fork';
+close $pipe;
+backgroundTest('On-demand fork');
+is $forkCount, 1, 'we did fork';
+
+done_testing;
+
+#vim:ft=perl
diff --git a/t/lib/WebGUI/Test/Fork.pm b/t/lib/WebGUI/Test/Fork.pm
new file mode 100644
index 000000000..ff86fbfed
--- /dev/null
+++ b/t/lib/WebGUI/Test/Fork.pm
@@ -0,0 +1,20 @@
+package WebGUI::Test::Fork;
+
+sub simple {
+ my ( $self, $arr ) = @_;
+ $self->update( $arr->[0] );
+}
+
+sub error {
+ my ( $self, $arr ) = @_;
+ die "$arr->[0]\n";
+}
+
+sub complex {
+ my $self = shift;
+ $self->update( sub {'foo'} );
+ $self->update( sub {'bar'} );
+ $self->update( sub {'baz'} );
+}
+
+1;
diff --git a/www/extras/Fork/ProgressBar.css b/www/extras/Fork/ProgressBar.css
new file mode 100644
index 000000000..d00e142b6
--- /dev/null
+++ b/www/extras/Fork/ProgressBar.css
@@ -0,0 +1,20 @@
+.webgui-fork-pb {
+ border: thin solid black;
+ position: relative;
+ line-height: 20pt;
+ height: 20pt;
+}
+
+.webgui-fork-pb .webgui-fork-pb-bar {
+ background-color: lime;
+ height: 100%;
+}
+
+.webgui-fork-pb .webgui-fork-pb-caption {
+ position: absolute;
+ top: 0;
+ left: 0;
+ width: 100%;
+ text-align: center;
+ font-size: 18pt;
+}
diff --git a/www/extras/Fork/ProgressBar.js b/www/extras/Fork/ProgressBar.js
new file mode 100644
index 000000000..3b6647950
--- /dev/null
+++ b/www/extras/Fork/ProgressBar.js
@@ -0,0 +1,30 @@
+/*global YAHOO, WebGUI, document */
+/* Dependencies: yahoo, dom */
+(function () {
+ var dom = YAHOO.util.Dom,
+ ns = YAHOO.namespace('WebGUI.Fork'),
+ cls = ns.ProgressBar = function () {},
+ proto = cls.prototype;
+
+ proto.render = function (node) {
+ var bar, cap;
+ if (!node.tagName) {
+ node = document.getElementById(node);
+ }
+ dom.addClass(node, 'webgui-fork-pb');
+ bar = document.createElement('div');
+ cap = document.createElement('div');
+ dom.addClass(bar, 'webgui-fork-pb-bar');
+ dom.addClass(cap, 'webgui-fork-pb-caption');
+ node.appendChild(bar);
+ node.appendChild(cap);
+ this.domNode = node;
+ this.bar = bar;
+ this.caption = cap;
+ };
+ proto.update = function (done, total) {
+ var pct = (total > 0 ? Math.floor((done/total)*100) : 100) + '%';
+ this.caption.innerHTML = pct;
+ this.bar.style.width = pct;
+ };
+}());
diff --git a/www/extras/Fork/poll.js b/www/extras/Fork/poll.js
new file mode 100644
index 000000000..5bb1a7761
--- /dev/null
+++ b/www/extras/Fork/poll.js
@@ -0,0 +1,42 @@
+/*global YAHOO, setTimeout */
+/* Dependencies: yahoo, connection_core, json */
+
+(function () {
+ var ns = YAHOO.namespace('WebGUI.Fork'), JSON = YAHOO.lang.JSON;
+
+ ns.poll = function(args) {
+ function fetch() {
+ var first = true;
+ YAHOO.util.Connect.asyncRequest('GET', args.url, {
+ success: function (o) {
+ var data, e;
+ if (o.status != 200) {
+ args.error("Server returned bad response");
+ return;
+ }
+ data = JSON.parse(o.responseText);
+ e = data.error;
+ if (e) {
+ args.error(e);
+ return;
+ }
+ args.draw(data);
+ if (args.first && first) {
+ first = false;
+ args.first();
+ }
+ if (data.finished) {
+ args.finish();
+ }
+ else {
+ setTimeout(fetch, args.interval || 1000);
+ }
+ },
+ failure: function (o) {
+ args.error("Could not communicate with server");
+ }
+ });
+ }
+ fetch();
+ };
+}());
diff --git a/www/extras/Fork/redirect.js b/www/extras/Fork/redirect.js
new file mode 100644
index 000000000..bf4c061e3
--- /dev/null
+++ b/www/extras/Fork/redirect.js
@@ -0,0 +1,16 @@
+/*global YAHOO, setTimeout, window */
+/* Dependencies: yahoo */
+
+(function () {
+ var ns = YAHOO.namespace('WebGUI.Fork');
+ ns.redirect = function (redir, after) {
+ if (!redir) {
+ return;
+ }
+ setTimeout(function() {
+ // The idea here is to only allow local redirects
+ var loc = window.location;
+ loc.href = loc.protocol + '//' + loc.host + redir;
+ }, after || 1000);
+ };
+}());