Skip to content

Commit

Permalink
Initial import
Browse files Browse the repository at this point in the history
git-svn-id: svn+ssh://rubyforge.org/var/svn/systemtimer@1 a5c7b79e-fcf6-406e-a944-d66f4bb6588e
  • Loading branch information
ph7 committed Feb 27, 2008
0 parents commit f1be074
Show file tree
Hide file tree
Showing 8 changed files with 503 additions and 0 deletions.
22 changes: 22 additions & 0 deletions README
@@ -0,0 +1,22 @@
Not useful yet. Work in progress.

install timer
-------------
` 1. Block SIG_ALRM + save old mask
` 2. save old handler (and enable SIG_ALRM)
` 3. Invoke ruby function to setup trap block and keep old Ruby handler
` 4. Install new timer and save old timer
` 5. Unblock SIG_ALRM

cleanup timer (cancel/restore)
--------------------
` 1. Block SIG_ALRM
` 2. Restore old Ruby handler
` 3. Reinstall the old handler
` 4. Restore the old timer
` 5. Restore SIG_ALRM BLOCKING status based on saved old mask


Error Handling
--------------
Write it properly. Recovery
81 changes: 81 additions & 0 deletions Rakefile
@@ -0,0 +1,81 @@
require 'rake'
require 'rake/testtask'
require 'rake/rdoctask'
require 'rake/gempackagetask'
require 'rake/clean'
require 'rubygems'

CLEAN.include '**/*.o'
CLEAN.include '**/*.so'
CLEAN.include '**/*.bundle'
CLOBBER.include '**/*.log'
CLOBBER.include '**/Makefile'
CLOBBER.include '**/extconf.h'

SYSTEM_TIMER_VERSION = "1.0"
SYSTEM_TIMER_GEM_NAME = "system_timer"

desc 'Default: run unit tests.'
task :default => :test

desc 'Install the gem into the local gem repository'
task :install => 'package' do
sh "gem install ./pkg/#{SYSTEM_TIMER_GEM_NAME}-#{SYSTEM_TIMER_VERSION}.gem"
end

desc 'Test SystemTimer'
Rake::TestTask.new(:test) do |t|
t.libs << 'lib'
t.pattern = 'test/**/*_test.rb'
t.verbose = true
end
task :test => 'ext/system_timer/libsystem_timer_native.so'

desc 'Generate documentation for SystemTimer.'
Rake::RDocTask.new(:rdoc) do |rdoc|
rdoc.rdoc_dir = 'rdoc'
rdoc.title = 'SystemTimer'
rdoc.options << '--line-numbers' << '--inline-source'
rdoc.rdoc_files.include('README')
rdoc.rdoc_files.include('lib/**/*.rb')
end

file 'ext/system_timer/Makefile' => 'ext/system_timer/extconf.rb' do
Dir.chdir('ext/system_timer') do
ruby 'extconf.rb'
end
end

file 'ext/system_timer/libsystem_timer_native.so' => 'ext/system_timer/Makefile' do
Dir.chdir('ext/system_timer') do
pid = fork { exec "make" }
Process.waitpid pid
end
fail "Make failed (status #{m})" unless $?.exitstatus == 0
end

specification = Gem::Specification.new do |s|
s.name = SYSTEM_TIMER_GEM_NAME
s.summary = "Set a Timeout based on signals, which are more reliable than Timeout. Timeout is based on green threads."
s.version = SYSTEM_TIMER_VERSION
if ENV['PACKAGE_FOR_WIN32'] || PLATFORM['win32']
s.platform = Gem::Platform::WIN32
s.files = FileList['lib/system_timer_stub.rb']
s.autorequire = "system_timer_stub"
else
s.platform = Gem::Platform::RUBY
s.files = ["ext/system_timer/system_timer_native.c", "ext/system_timer/extconf.rb"] + FileList['lib/**/*.rb'] + FileList['test/**/*.rb']
s.autorequire = "system_timer"
s.extensions = ["ext/system_timer/extconf.rb"]
end
s.require_path = "lib"
s.rdoc_options << '--title' << 'SystemTimer' << '--main' << 'README' << '--line-numbers'
s.has_rdoc = true
s.extra_rdoc_files = ['README']
s.test_file = "test/all_tests.rb"
end

Rake::GemPackageTask.new(specification) do |package|
package.need_zip = false
package.need_tar = false
end
3 changes: 3 additions & 0 deletions ext/system_timer/extconf.rb
@@ -0,0 +1,3 @@
#!/usr/bin/env ruby
require 'mkmf'
create_makefile("system_timer_native")
217 changes: 217 additions & 0 deletions ext/system_timer/system_timer_native.c
@@ -0,0 +1,217 @@
#include "ruby.h"
#include "rubysig.h"
#include <signal.h>
#include <errno.h>

#define DISPLAY_ERRNO 1
#define DO_NOT_DISPLAY_ERRNO 0

VALUE rb_cSystemTimer;
sigset_t original_mask;
sigset_t sigalarm_mask;
struct sigaction original_signal_handler;
struct itimerval original_timer_interval;

static void clear_pending_sigalrm_for_ruby_threads();
static void log_debug(char*);
static void log_error(char*, int);
static void install_ruby_sigalrm_handler(VALUE);
static void restore_original_ruby_sigalrm_handler(VALUE);
static void restore_original_sigalrm_mask_when_blocked();
static void restore_original_timer_interval();
static void set_itimerval(struct itimerval *, int);

static int debug_enabled = 0;

static VALUE install_timer(VALUE self, VALUE seconds)
{
struct itimerval timer_interval;

/*
* Block SIG_ALRM for safe processing of SIG_ALRM configuration and save mask.
*/
if (0 != sigprocmask(SIG_BLOCK, &sigalarm_mask, &original_mask)) {
log_error("install_timer: Could not block SIG_ALRM", DISPLAY_ERRNO);
return Qnil;
}
clear_pending_sigalrm_for_ruby_threads();
log_debug("install_timer: Succesfully blocked SIG_ALRM at O.S. level");

/*
* Save previous signal handler.
*/
original_signal_handler.sa_handler = NULL;
if (0 != sigaction(SIGALRM, NULL, &original_signal_handler)) {
log_error("install_timer: Could not save existing handler for SIG_ALRM", DISPLAY_ERRNO);
restore_original_sigalrm_mask_when_blocked();
return Qnil;
}
log_debug("install_timer: Succesfully saved existing SIG_ALRM handler");

/*
* Install Ruby Level SIG_ALRM handler
*/
install_ruby_sigalrm_handler(self);

/*
* Set new real time interval timer and save the original if any.
*/
set_itimerval(&original_timer_interval, 0);
set_itimerval(&timer_interval, NUM2INT(seconds));
if (0 != setitimer(ITIMER_REAL, &timer_interval, &original_timer_interval)) {
log_error("install_timer: Could not install our own timer, timeout will not work", DISPLAY_ERRNO);
restore_original_ruby_sigalrm_handler(self);
restore_original_sigalrm_mask_when_blocked();
return Qnil;
}
log_debug("install_timer: Successfully installed timer");

/*
* Unblock SIG_ALRM
*/
if (0 != sigprocmask(SIG_UNBLOCK, &sigalarm_mask, NULL)) {
log_error("install_timer: Could not unblock SIG_ALRM, timeout will not work", DISPLAY_ERRNO);
restore_original_timer_interval();
restore_original_ruby_sigalrm_handler(self);
restore_original_sigalrm_mask_when_blocked();
}
log_debug("install_timer: Succesfully unblocked SIG_ALRM.");

return Qnil;
}

static VALUE cleanup_timer(VALUE self, VALUE seconds)
{
/*
* Block SIG_ALRM for safe processing of SIG_ALRM configuration.
*/
if (0 != sigprocmask(SIG_BLOCK, &sigalarm_mask, NULL)) {
log_error("cleanup_timer: Could not block SIG_ALRM", errno);
}
clear_pending_sigalrm_for_ruby_threads();
log_debug("cleanup_timer: Blocked SIG_ALRM");

/*
* Install Ruby Level SIG_ALRM handler
*/
restore_original_ruby_sigalrm_handler(self);


if (original_signal_handler.sa_handler == NULL) {
log_error("cleanup_timer: Previous SIG_ALRM handler not initialized!", DO_NOT_DISPLAY_ERRNO);
} else if (0 == sigaction(SIGALRM, &original_signal_handler, NULL)) {
log_debug("cleanup_timer: Succesfully restored previous handler for SIG_ALRM");
} else {
log_error("cleanup_timer: Could not restore previous handler for SIG_ALRM", DISPLAY_ERRNO);
}
original_signal_handler.sa_handler = NULL;

restore_original_timer_interval();
restore_original_sigalrm_mask_when_blocked();
}

/*
* Restore original timer the way it was originally set. **WARNING** Breaks original timer semantics
*
* Not bothering to calculate how much time is left or if the timer already expired
* based on when the original timer was set and how much time is passed, just resetting
* the original timer as is for the sake of simplicity.
*
*/
static void restore_original_timer_interval() {
if (0 != setitimer (ITIMER_REAL, &original_timer_interval, NULL)) {
log_error("install_timer: Could not restore original timer", DISPLAY_ERRNO);
}
log_debug("install_timer: Successfully restored timer");
}

static void restore_original_sigalrm_mask_when_blocked()
{
if (!sigismember(&original_mask, SIGALRM)) {
sigprocmask(SIG_UNBLOCK, &sigalarm_mask, NULL);
log_debug("cleanup_timer: Unblocked SIG_ALRM");
} else {
log_debug("cleanup_timer: No Need to unblock SIG_ALRM");
}
}

static void install_ruby_sigalrm_handler(VALUE self) {
rb_thread_critical = 1;
rb_funcall(self, rb_intern("install_ruby_sigalrm_handler"), 0);
rb_thread_critical = 0;
}

static void restore_original_ruby_sigalrm_handler(VALUE self) {
rb_thread_critical = 1;
rb_funcall(self, rb_intern("restore_original_ruby_sigalrm_handler"), 0);
rb_thread_critical = 0;
}


static VALUE debug_enabled_p(VALUE self) {
return debug_enabled ? Qtrue : Qfalse;
}

static VALUE enable_debug(VALUE self) {
debug_enabled = 1;
return Qnil;
}

static VALUE disable_debug(VALUE self) {
debug_enabled = 0;
return Qnil;
}

static void log_debug(char* message)
{
if (0 != debug_enabled) {
printf("%s\n", message);
}
return;
}

static void log_error(char* message, int display_errno)
{
fprintf(stderr, "%s: %s\n", message, display_errno ? strerror(errno) : "");
return;
}

/*
* The intent is to clear SIG_ALRM signals at the Ruby level (green threads),
* eventually triggering existing SIG_ALRM handler as a courtesy.
*
* As we cannot access trap_pending_list outside of signal.c our best fallback option
* is to trigger all pending signals at the Ruby level (potentially triggering
* green thread scheduling).
*/
static void clear_pending_sigalrm_for_ruby_threads()
{
CHECK_INTS;
log_debug("Succesfully triggered all pending signals at Green Thread level");
}

static void init_sigalarm_mask()
{
sigemptyset(&sigalarm_mask);
sigaddset(&sigalarm_mask, SIGALRM);
return;
}

static void set_itimerval(struct itimerval *value, int seconds) {
value->it_interval.tv_usec = 0;
value->it_interval.tv_sec = 0;
value->it_value.tv_usec = 0;
value->it_value.tv_sec = seconds; // (long int)
return;
}

void Init_system_timer_native()
{
init_sigalarm_mask();
rb_cSystemTimer = rb_define_module("SystemTimer");
rb_define_singleton_method(rb_cSystemTimer, "install_timer", install_timer, 1);
rb_define_singleton_method(rb_cSystemTimer, "cleanup_timer", cleanup_timer, 0);
rb_define_singleton_method(rb_cSystemTimer, "debug_enabled?", debug_enabled_p, 0);
rb_define_singleton_method(rb_cSystemTimer, "enable_debug", enable_debug, 0);
rb_define_singleton_method(rb_cSystemTimer, "disable_debug", disable_debug, 0);
}
49 changes: 49 additions & 0 deletions lib/system_timer.rb
@@ -0,0 +1,49 @@
require 'rubygems'
require 'timeout'

module SystemTimer
class << self

def timeout_after(seconds)
install_timer(seconds)
return yield
ensure
cleanup_timer
end

protected

def install_ruby_sigalrm_handler
timed_thread = Thread.current # Ruby signals are always delivered to main thread by default.
@original_ruby_sigalrm_handler = trap('SIGALRM') do
log_timeout_received(timed_thread) if SystemTimer.debug_enabled?
timed_thread.raise Timeout::Error.new("time's up!")
end
end

def restore_original_ruby_sigalrm_handler
trap('SIGALRM', original_ruby_sigalrm_handler || 'DEFAULT')
ensure
reset_original_ruby_sigalrm_handler
end

def original_ruby_sigalrm_handler
@original_ruby_sigalrm_handler
end

def reset_original_ruby_sigalrm_handler
@original_ruby_sigalrm_handler = nil
end

def log_timeout_received(timed_thread)
puts <<-EOS
install_ruby_sigalrm_handler: Got Timeout in #{Thread.current}
Main thread : #{Thread.main}
Timed_thread : #{timed_thread}
All Threads : #{Thread.list.inspect}
EOS
end
end
end

require 'system_timer_native'
15 changes: 15 additions & 0 deletions lib/system_timer_stub.rb
@@ -0,0 +1,15 @@
require 'rubygems'
require 'timeout'

module SystemTimer
class << self

def timeout_after(seconds)
Timeout::timeout(seconds) do
yield
end
end

end

end
1 change: 1 addition & 0 deletions test/all_tests.rb
@@ -0,0 +1 @@
Dir["#{File.dirname __FILE__}/*_test.rb"].each { |test_case| require test_case }

0 comments on commit f1be074

Please sign in to comment.