edit: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Libraries.LCA_RESTRICTED/Builtins/ThreadOps.cs;C698557 File: ThreadOps.cs =================================================================== --- $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Libraries.LCA_RESTRICTED/Builtins/ThreadOps.cs;C698557 (server) 1/9/2009 11:38 AM +++ Shelved Change: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Libraries.LCA_RESTRICTED/Builtins/ThreadOps.cs;kill @@ -40,9 +40,6 @@ public static class ThreadOps { static bool _globalAbortOnException; - private class ThreadExitMarker { - } - /// /// The ThreadState enumeration is a flag, and multiple values could be set simultaneously. Also, /// there is other state that IronRuby tracks. RubyThreadStatus flattens out the different states @@ -62,9 +59,9 @@ Completed, /// - /// TODO - Ruby samples show this state. Is it really needed? + /// If Thread#kill has been called, and the thread is not sleeping /// - // Aborting, + Aborting, /// /// An unhandled exception was thrown by the thread @@ -160,6 +157,7 @@ internal Exception Exception { get; set; } internal object Result { get; set; } internal bool CreatedFromRuby { get; set; } + internal bool ExitRequested { get; set; } internal bool AbortOnException { get { @@ -298,6 +296,9 @@ case RubyThreadStatus.Sleeping: result.Append("sleep"); break; + case RubyThreadStatus.Aborting: + result.Append("aborting"); + break; case RubyThreadStatus.Completed: case RubyThreadStatus.Aborted: result.Append("dead"); @@ -347,11 +348,14 @@ [RubyMethod("terminate")] public static Thread Kill(Thread/*!*/ self) { RubyThreadInfo.RegisterThread(Thread.CurrentThread); -#if SILVERLIGHT // Thread.Abort(stateInfo) - self.Abort(); -#else - self.Abort(new ThreadExitMarker()); -#endif + RubyThreadInfo info = RubyThreadInfo.FromThread(self); + if (GetStatus(self) == RubyThreadStatus.Sleeping && (self.ThreadState & ThreadState.AbortRequested) != 0) { + info.Run(); + return self; + } + + info.ExitRequested = true; + RubyOps.ExitThread(self); return self; } @@ -504,6 +508,10 @@ return RubyThreadStatus.Sleeping; } + if ((state & ThreadState.AbortRequested) != 0) { + return RubyThreadStatus.Aborting; + } + if ((state & ThreadState.Running) == ThreadState.Running) { return RubyThreadStatus.Running; } @@ -521,6 +529,8 @@ return MutableString.Create("run"); case RubyThreadStatus.Sleeping: return MutableString.Create("sleep"); + case RubyThreadStatus.Aborting: + return MutableString.Create("aborting"); case RubyThreadStatus.Completed: return false; case RubyThreadStatus.Aborted: @@ -593,26 +603,6 @@ return result; } - /// - /// Thread#exit is implemented by calling Thread.Abort. However, we need to distinguish a call to Thread#exit - /// from a raw call to Thread.Abort. - /// - /// true if the exception was raised by Thread#exit. In that case, it should not be treated as an uncaught exception - private static bool IsRubyThreadExit(Exception e) { - ThreadAbortException tae = e as ThreadAbortException; - if (tae != null) { -#if SILVERLIGHT // Thread.ExceptionState - return true; -#else - if (tae.ExceptionState is ThreadExitMarker) { - Thread.ResetAbort(); - return true; - } -#endif - } - return false; - } - [RubyMethod("main", RubyMethodAttributes.PublicSingleton)] public static Thread/*!*/ GetMainThread(RubyContext/*!*/ context, RubyClass self) { return context.MainThread; @@ -642,9 +632,14 @@ startRoutine.Yield(args, out threadResult); info.Result = threadResult; } catch (Exception e) { - if (IsRubyThreadExit(e)) { + if (info.ExitRequested) { + // Note that "e" may not be ThreadAbortException at this point If an exception was raised from a finally block, + // we will get that here instead Utils.Log(String.Format("Thread {0} exited.", info.Thread.ManagedThreadId), "THREAD"); info.Result = false; +#if !SILVERLIGHT + Thread.ResetAbort(); +#endif } else { e = RubyOps.GetVisibleException(e); RubyExceptionData.ActiveExceptionHandled(e); =================================================================== edit: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Compiler/Ast/SourceUnitTree.cs;C695629 File: SourceUnitTree.cs =================================================================== --- $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Compiler/Ast/SourceUnitTree.cs;C695629 (server) 1/9/2009 1:42 PM +++ Shelved Change: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Compiler/Ast/SourceUnitTree.cs;kill @@ -228,7 +228,7 @@ public static void CheckForAsyncRaiseViaThreadAbort(RubyScope scope, System.Threading.ThreadAbortException exception) { Exception visibleException = RubyOps.GetVisibleException(exception); - if (exception == visibleException) { + if (exception == visibleException || visibleException == null) { return; } else { RubyOps.SetCurrentExceptionAndStackTrace(scope, exception); =================================================================== edit: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Hosting/RubyCommandLine.cs;C695629 File: RubyCommandLine.cs =================================================================== --- $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Hosting/RubyCommandLine.cs;C695629 (server) 1/9/2009 1:42 PM +++ Shelved Change: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Hosting/RubyCommandLine.cs;kill @@ -42,7 +42,7 @@ return base.TryInteractiveAction(); } catch (ThreadAbortException e) { Exception visibleException = RubyOps.GetVisibleException(e); - if (visibleException == e) { + if (visibleException == e || visibleException == null) { throw; } else { throw visibleException; =================================================================== edit: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyExceptionData.cs;C691193 File: RubyExceptionData.cs =================================================================== --- $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyExceptionData.cs;C691193 (server) 1/9/2009 1:43 PM +++ Shelved Change: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyExceptionData.cs;kill @@ -360,7 +360,7 @@ RubyExceptionData result; Exception visibleException = RubyOps.GetVisibleException(e); - if (e == visibleException) { + if (e == visibleException || visibleException == null) { result = new RubyExceptionData(e); } else { // Async exception =================================================================== edit: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyOps.cs;C695629 File: RubyOps.cs =================================================================== --- $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyOps.cs;C695629 (server) 1/9/2009 1:32 PM +++ Shelved Change: $/Dev10/feature/vs_langs01/Merlin/Main/Languages/Ruby/Ruby/Runtime/RubyOps.cs;kill @@ -1242,8 +1242,16 @@ #region Exceptions -#if SILVERLIGHT // Thread.ExceptionState +#if SILVERLIGHT // Thread.ExceptionState, Thread.Abort(stateInfo) public static Exception GetVisibleException(Exception e) { return e; } + + public static void ExitThread(Thread/*!*/ thread) { + thread.Abort(); + } + + public static bool IsRubyThreadExit(Exception e) { + return e is ThreadAbortException; + } #else /// /// Thread#raise is implemented on top of System.Threading.Thread.ThreadAbort, and squirreling @@ -1260,9 +1268,40 @@ thread.Abort(new AsyncExceptionMarker(e)); } + // TODO: This is redundant with ThreadOps.RubyThreadInfo.ExitRequested. However, we cannot access that + // from here as it is in a separate assembly. + private class ThreadExitMarker { + } + + public static void ExitThread(Thread/*!*/ thread) { + thread.Abort(new ThreadExitMarker()); + } + + /// + /// Thread#exit is implemented by calling Thread.Abort. However, we need to distinguish a call to Thread#exit + /// from a raw call to Thread.Abort. + /// + /// Note that if a finally block raises an exception while an Abort is pending, that exception can be propagated instead of a ThreadAbortException. + /// + public static bool IsRubyThreadExit(Exception e) { + ThreadAbortException tae = e as ThreadAbortException; + if (tae != null) { + if (tae.ExceptionState is ThreadExitMarker) { + return true; + } + } + return false; + } + + /// + /// Can return null for Thread#kill + /// public static Exception GetVisibleException(Exception e) { ThreadAbortException tae = e as ThreadAbortException; if (tae != null) { + if (IsRubyThreadExit(e)) { + return null; + } AsyncExceptionMarker asyncExceptionMarker = tae.ExceptionState as AsyncExceptionMarker; if (asyncExceptionMarker != null) { return asyncExceptionMarker.Exception; =================================================================== add: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/exit_tags.txt File: exit_tags.txt =================================================================== --- [no source file] +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/exit_tags.txt;kill @@ -1,0 +1,1 @@ +fails:Thread#exit can be rescued by outer rescue clause when inner ensure clause raises exception =================================================================== add: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/kill_tags.txt File: kill_tags.txt =================================================================== --- [no source file] +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/kill_tags.txt;kill @@ -1,0 +1,1 @@ +fails:Thread#kill can be rescued by outer rescue clause when inner ensure clause raises exception =================================================================== add: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/terminate_tags.txt File: terminate_tags.txt =================================================================== --- [no source file] +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/ironruby-tags/1.8/core/thread/terminate_tags.txt;kill @@ -1,0 +1,1 @@ +fails:Thread#terminate can be rescued by outer rescue clause when inner ensure clause raises exception =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/alive_spec.rb;C698557 File: alive_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/alive_spec.rb;C698557 (server) 1/9/2009 11:27 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/alive_spec.rb;kill @@ -14,6 +14,10 @@ ThreadSpecs.status_of_sleeping_thread.alive?.should == true end + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.alive?.should == true + end + it "describes a completed thread" do ThreadSpecs.status_of_completed_thread.alive?.should == false end @@ -26,6 +30,14 @@ ThreadSpecs.status_of_thread_with_uncaught_exception.alive?.should == false end + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.alive?.should == true + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.alive?.should == true + end + compliant_on(:ruby) do it "reports aborting on a killed thread" do ThreadSpecs.status_of_aborting_thread.alive?.should == true =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/inspect_spec.rb;C698557 File: inspect_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/inspect_spec.rb;C698557 (server) 1/9/2009 11:27 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/inspect_spec.rb;kill @@ -14,6 +14,10 @@ ThreadSpecs.status_of_sleeping_thread.inspect.should include('sleep') end + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.inspect.should include('sleep') + end + it "describes a completed thread" do ThreadSpecs.status_of_completed_thread.inspect.should include('dead') end @@ -26,6 +30,14 @@ ThreadSpecs.status_of_thread_with_uncaught_exception.inspect.should include('dead') end + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.inspect.should include('aborting') + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.status.should include('sleep') + end + compliant_on(:ruby) do it "reports aborting on a killed thread" do ThreadSpecs.status_of_aborting_thread.inspect.should include('aborting') =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/join_spec.rb;C638190 File: join_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/join_spec.rb;C638190 (server) 1/9/2009 11:57 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/join_spec.rb;kill @@ -39,4 +39,14 @@ t = Thread.new { raise NotImplementedError.new("Just kidding") } lambda { t.join }.should raise_error(NotImplementedError) end + + it "returns the dead thread" do + t = Thread.new { Thread.current.kill } + t.join.should equal(t) + end + + it "returns the dead thread even if an uncaught exception is thrown from ensure block" do + t = ThreadSpecs.dying_thread_ensures { raise "In dying thread" } + t.join.should equal(t) + end end =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/status_spec.rb;C698557 File: status_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/status_spec.rb;C698557 (server) 1/9/2009 11:13 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/status_spec.rb;kill @@ -14,6 +14,10 @@ ThreadSpecs.status_of_sleeping_thread.status.should == 'sleep' end + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.status.should == 'sleep' + end + it "describes a completed thread" do ThreadSpecs.status_of_completed_thread.status.should == false end @@ -26,6 +30,14 @@ ThreadSpecs.status_of_thread_with_uncaught_exception.status.should == nil end + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.status.should == 'aborting' + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.status.should == 'sleep' + end + compliant_on(:ruby) do it "reports aborting on a killed thread" do ThreadSpecs.status_of_aborting_thread.status.should == 'aborting' =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/stop_spec.rb;C698557 File: stop_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/stop_spec.rb;C698557 (server) 1/9/2009 11:27 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/stop_spec.rb;kill @@ -32,6 +32,10 @@ ThreadSpecs.status_of_sleeping_thread.stop?.should == true end + it "describes a blocked thread" do + ThreadSpecs.status_of_blocked_thread.stop?.should == true + end + it "describes a completed thread" do ThreadSpecs.status_of_completed_thread.stop?.should == true end @@ -44,6 +48,14 @@ ThreadSpecs.status_of_thread_with_uncaught_exception.stop?.should == true end + it "describes a dying running thread" do + ThreadSpecs.status_of_dying_running_thread.stop?.should == false + end + + it "describes a dying sleeping thread" do + ThreadSpecs.status_of_dying_sleeping_thread.stop?.should == true + end + compliant_on(:ruby) do it "reports aborting on a killed thread" do ThreadSpecs.status_of_aborting_thread.stop?.should == false =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/value_spec.rb;C698557 File: value_spec.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/value_spec.rb;C698557 (server) 1/9/2009 11:50 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/value_spec.rb;kill @@ -15,4 +15,9 @@ t = Thread.new { Thread.current.exit } t.value.should == false end + + it "is false for an uncaught exception thrown from a dying thread" do + t = ThreadSpecs.dying_thread_ensures { 1/0 } + t.value.should == false + end end =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/fixtures/classes.rb;C698557 File: classes.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/fixtures/classes.rb;C698557 (server) 1/9/2009 11:13 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/fixtures/classes.rb;kill @@ -57,6 +57,17 @@ status end + def self.status_of_blocked_thread + m = Mutex.new + m.lock + t = Thread.new { m.lock } + Thread.pass until t.status == 'sleep' + status = Status.new t + m.unlock + t.join + status + end + def self.status_of_aborting_thread t = Thread.new { sleep } begin @@ -85,6 +96,59 @@ Status.new t end + def self.status_of_dying_running_thread + status = nil + t = dying_thread_ensures { status = Status.new Thread.current } + t.join + status + end + + def self.status_of_dying_sleeping_thread + t = dying_thread_ensures { Thread.stop } + Thread.pass until t.status == 'sleep' + status = Status.new t + t.wakeup + t.join + status + end + + def self.dying_thread_ensures(kill_method_name=:kill) + t = Thread.new do + begin + Thread.current.send(kill_method_name) + ensure + yield + end + end + end + + def self.dying_thread_with_outer_ensure(kill_method_name=:kill) + t = Thread.new do + begin + begin + Thread.current.send(@method) + ensure + raise "In dying thread" + end + ensure + yield + end + end + end + + def self.join_dying_thread_with_outer_ensure(kill_method_name=:kill) + t = dying_thread_with_outer_ensure(kill_method_name) { yield } + lambda { t.join }.should raise_error(RuntimeError, "In dying thread") + return t + end + + def self.wakeup_dying_sleeping_thread(kill_method_name=:kill) + thread = ThreadSpecs.dying_thread_ensures(kill_method_name) { yield } + Thread.pass until thread.status == "sleep" + thread.wakeup + thread.join + end + def self.critical_is_reset # Create another thread to verify that it can call Thread.critical= t = Thread.new do =================================================================== edit: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/shared/exit.rb;C698557 File: exit.rb =================================================================== --- $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/shared/exit.rb;C698557 (server) 1/9/2009 11:48 AM +++ Shelved Change: $/Merlin_External/Languages/IronRuby/mspec/rubyspec/1.8/core/thread/shared/exit.rb;kill @@ -1,6 +1,9 @@ describe :thread_exit, :shared => true do + before(:each) do + ScratchPad.clear + end + it "kills sleeping thread" do - ScratchPad.clear() sleeping_thread = Thread.new do sleep ScratchPad.record :after_sleep @@ -12,7 +15,6 @@ end it "kills current thread" do - ScratchPad.clear() thread = Thread.new do Thread.current.send(@method) ScratchPad.record :after_sleep @@ -22,18 +24,76 @@ end it "runs ensure clause" do - ScratchPad.clear + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record :in_ensure_clause } + thread.join + ScratchPad.recorded.should == :in_ensure_clause + end + + it "does not set $!" do + thread = ThreadSpecs.dying_thread_ensures(@method) { ScratchPad.record $! } + thread.join + ScratchPad.recorded.should == nil + end + + it "cannot be rescued" do thread = Thread.new do begin Thread.current.send(@method) - ensure - ScratchPad.record :in_ensure_clause + rescue Exception + ScratchPad.record :in_rescue end + ScratchPad.record :end_of_thread_block end + thread.join - ScratchPad.recorded.should == :in_ensure_clause + ScratchPad.recorded.should == nil end + it "propogates inner exception to Thread.join if there is an outer ensure clause" do + thread = ThreadSpecs.dying_thread_with_outer_ensure(@method) { } + lambda { thread.join }.should raise_error(RuntimeError, "In dying thread") + end + + it "runs all outer ensure clauses even if inner ensure clause raises exception" do + thread = ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record :in_outer_ensure_clause } + ScratchPad.recorded.should == :in_outer_ensure_clause + end + + it "sets $! in outer ensure clause if inner ensure clause raises exception" do + thread = ThreadSpecs.join_dying_thread_with_outer_ensure(@method) { ScratchPad.record $! } + ScratchPad.recorded.to_s.should == "In dying thread" + end + + it "can be rescued by outer rescue clause when inner ensure clause raises exception" do + thread = Thread.new do + begin + begin + Thread.current.send(@method) + ensure + raise "In dying thread" + end + rescue Exception + ScratchPad.record $! + end + :end_of_thread_block + end + + thread.value.should == :end_of_thread_block + ScratchPad.recorded.to_s.should == "In dying thread" + end + + it "is deferred if ensure clause does Thread.stop" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { Thread.stop; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + + not_compliant_on(:ruby) do # Doing a sleep in the ensure block hangs the process + it "is deferred if ensure clause sleeps" do + ThreadSpecs.wakeup_dying_sleeping_thread(@method) { sleep; ScratchPad.record :after_sleep } + ScratchPad.recorded.should == :after_sleep + end + end + # This case occurred in JRuby where native threads are used to provide # the same behavior as MRI green threads. Key to this issue was the fact # that the thread which called #exit in its block was also being explicitly ===================================================================