/***** BEGIN LICENSE BLOCK *****
* Version: CPL 1.0/GPL 2.0/LGPL 2.1
*
* The contents of this file are subject to the Common Public
* License Version 1.0 (the "License"); you may not use this file
* except in compliance with the License. You may obtain a copy of
* the License at http://www.eclipse.org/legal/cpl-v10.html
*
* Software distributed under the License is distributed on an "AS
* IS" basis, WITHOUT WARRANTY OF ANY KIND, either express or
* implied. See the License for the specific language governing
* rights and limitations under the License.
*
* Copyright (C) 2002 Jason Voegele <jason@jvoegele.com>
* Copyright (C) 2002-2004 Anders Bengtsson <ndrsbngtssn@yahoo.se>
* Copyright (C) 2002-2004 Jan Arne Petersen <jpetersen@uni-bonn.de>
* Copyright (C) 2004 Thomas E Enebo <enebo@acm.org>
* Copyright (C) 2004-2005 Charles O Nutter <headius@headius.com>
* Copyright (C) 2004 Stefan Matthias Aust <sma@3plus4.de>
*
* Alternatively, the contents of this file may be used under the terms of
* either of the GNU General Public License Version 2 or later (the "GPL"),
* or the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
* in which case the provisions of the GPL or the LGPL are applicable instead
* of those above. If you wish to allow use of your version of this file only
* under the terms of either the GPL or the LGPL, and not to allow others to
* use your version of this file under the terms of the CPL, indicate your
* decision by deleting the provisions above and replace them with the notice
* and other provisions required by the GPL or the LGPL. If you do not delete
* the provisions above, a recipient may use your version of this file under
* the terms of any one of the CPL, the GPL or the LGPL.
***** END LICENSE BLOCK *****/
package org.jruby;
import java.util.HashMap;
import java.util.Map;
import org.jruby.exceptions.RaiseException;
import org.jruby.exceptions.ThreadKill;
import org.jruby.internal.runtime.FutureThread;
import org.jruby.internal.runtime.NativeThread;
import org.jruby.internal.runtime.RubyNativeThread;
import org.jruby.internal.runtime.RubyRunnable;
import org.jruby.internal.runtime.ThreadLike;
import org.jruby.internal.runtime.ThreadService;
import org.jruby.runtime.Block;
import org.jruby.runtime.CallbackFactory;
import org.jruby.runtime.ObjectAllocator;
import org.jruby.runtime.ThreadContext;
import org.jruby.runtime.builtin.IRubyObject;
import edu.emory.mathcs.backport.java.util.concurrent.ExecutionException;
import edu.emory.mathcs.backport.java.util.concurrent.TimeoutException;
import edu.emory.mathcs.backport.java.util.concurrent.locks.ReentrantLock;
import org.jruby.runtime.Arity;
import org.jruby.runtime.ObjectMarshal;
/**
* Implementation of Ruby's <code>Thread</code> class. Each Ruby thread is
* mapped to an underlying Java Virtual Machine thread.
* <p>
* Thread encapsulates the behavior of a thread of execution, including the main
* thread of the Ruby script. In the descriptions that follow, the parameter
* <code>aSymbol</code> refers to a symbol, which is either a quoted string or a
* <code>Symbol</code> (such as <code>:name</code>).
*
* Note: For CVS history, see ThreadClass.java.
*/
public class RubyThread extends RubyObject {
private ThreadLike threadImpl;
private Map threadLocalVariables = new HashMap();
private boolean abortOnException;
private IRubyObject finalResult;
private RaiseException exitingException;
private IRubyObject receivedException;
private RubyThreadGroup threadGroup;
private ThreadService threadService;
private volatile boolean isStopped = false;
public Object stopLock = new Object();
private volatile boolean killed = false;
public Object killLock = new Object();
public final ReentrantLock lock = new ReentrantLock();
private static boolean USE_POOLING;
private static final boolean DEBUG = false;
static {
if (Ruby.isSecurityRestricted()) USE_POOLING = false;
else USE_POOLING = Boolean.getBoolean("jruby.thread.pooling");
}
public static RubyClass createThreadClass(Ruby runtime) {
// FIXME: In order for Thread to play well with the standard 'new' behavior,
// it must provide an allocator that can create empty object instances which
// initialize then fills with appropriate data.
RubyClass threadClass = runtime.defineClass("Thread", runtime.getObject(), ObjectAllocator.NOT_ALLOCATABLE_ALLOCATOR);
CallbackFactory callbackFactory = runtime.callbackFactory(RubyThread.class);
threadClass.defineFastMethod("[]", callbackFactory.getFastMethod("aref", RubyKernel.IRUBY_OBJECT));
threadClass.defineFastMethod("[]=", callbackFactory.getFastMethod("aset", RubyKernel.IRUBY_OBJECT, RubyKernel.IRUBY_OBJECT));
threadClass.defineFastMethod("abort_on_exception", callbackFactory.getFastMethod("abort_on_exception"));
threadClass.defineFastMethod("abort_on_exception=", callbackFactory.getFastMethod("abort_on_exception_set", RubyKernel.IRUBY_OBJECT));
threadClass.defineFastMethod("alive?", callbackFactory.getFastMethod("is_alive"));
threadClass.defineFastMethod("group", callbackFactory.getFastMethod("group"));
threadClass.defineFastMethod("join", callbackFactory.getFastOptMethod("join"));
threadClass.defineFastMethod("value", callbackFactory.getFastMethod("value"));
threadClass.defineMethod("initialize", callbackFactory.getOptMethod("initialize"));
threadClass.defineFastMethod("inspect", callbackFactory.getFastMethod("inspect"));
threadClass.defineFastMethod("key?", callbackFactory.getFastMethod("has_key", RubyKernel.IRUBY_OBJECT));
threadClass.defineFastMethod("keys", callbackFactory.getFastMethod("keys"));
threadClass.defineFastMethod("priority", callbackFactory.getFastMethod("priority"));
threadClass.defineFastMethod("priority=", callbackFactory.getFastMethod("priority_set", RubyKernel.IRUBY_OBJECT));
threadClass.defineMethod("raise", callbackFactory.getOptMethod("raise"));
//threadClass.defineFastMethod("raise", callbackFactory.getFastMethod("raise", RubyKernel.IRUBY_OBJECT));
threadClass.defineFastMethod("run", callbackFactory.getFastMethod("run"));
threadClass.defineFastMethod("status", callbackFactory.getFastMethod("status"));
threadClass.defineFastMethod("stop?", callbackFactory.getFastMethod("isStopped"));
threadClass.defineFastMethod("wakeup", callbackFactory.getFastMethod("wakeup"));
// threadClass.defineMethod("value",
// callbackFactory.getMethod("value"));
threadClass.defineFastMethod("kill", callbackFactory.getFastMethod("kill"));
threadClass.defineFastMethod("exit", callbackFactory.getFastMethod("exit"));
threadClass.getMetaClass().defineFastMethod("current", callbackFactory.getFastSingletonMethod("current"));
threadClass.getMetaClass().defineMethod("fork", callbackFactory.getOptSingletonMethod("newInstance"));
threadClass.getMetaClass().defineMethod("new", callbackFactory.getOptSingletonMethod("newInstance"));
threadClass.getMetaClass().defineFastMethod("list", callbackFactory.getFastSingletonMethod("list"));
threadClass.getMetaClass().defineFastMethod("pass", callbackFactory.getFastSingletonMethod("pass"));
threadClass.getMetaClass().defineMethod("start", callbackFactory.getOptSingletonMethod("start"));
threadClass.getMetaClass().defineFastMethod("critical=", callbackFactory.getFastSingletonMethod("critical_set", RubyBoolean.class));
threadClass.getMetaClass().defineFastMethod("critical", callbackFactory.getFastSingletonMethod("critical"));
threadClass.getMetaClass().defineFastMethod("stop", callbackFactory.getFastSingletonMethod("stop"));
threadClass.getMetaClass().defineMethod("kill", callbackFactory.getSingletonMethod("s_kill", RubyThread.class));
threadClass.getMetaClass().defineMethod("exit", callbackFactory.getSingletonMethod("s_exit"));
threadClass.getMetaClass().defineFastMethod("abort_on_exception", callbackFactory.getFastSingletonMethod("abort_on_exception"));
threadClass.getMetaClass().defineFastMethod("abort_on_exception=", callbackFactory.getFastSingletonMethod("abort_on_exception_set", RubyKernel.IRUBY_OBJECT));
RubyThread rubyThread = new RubyThread(runtime, threadClass);
// TODO: need to isolate the "current" thread from class creation
rubyThread.threadImpl = new NativeThread(rubyThread, Thread.currentThread());
runtime.getThreadService().setMainThread(rubyThread);
threadClass.getMetaClass().defineFastMethod("main", callbackFactory.getFastSingletonMethod("main"));
threadClass.setMarshal(ObjectMarshal.NOT_MARSHALABLE_MARSHAL);
return threadClass;
}
/**
* <code>Thread.new</code>
* <p>
* Thread.new( <i>[ arg ]*</i> ) {| args | block } -> aThread
* <p>
* Creates a new thread to execute the instructions given in block, and
* begins running it. Any arguments passed to Thread.new are passed into the
* block.
* <pre>
* x = Thread.new { sleep .1; print "x"; print "y"; print "z" }
* a = Thread.new { print "a"; print "b"; sleep .2; print "c" }
* x.join # Let the threads finish before
* a.join # main thread exits...
* </pre>
* <i>produces:</i> abxyzc
*/
public static IRubyObject newInstance(IRubyObject recv, IRubyObject[] args, Block block) {
return startThread(recv, args, true, block);
}
/**
* Basically the same as Thread.new . However, if class Thread is
* subclassed, then calling start in that subclass will not invoke the
* subclass's initialize method.
*/
public static RubyThread start(IRubyObject recv, IRubyObject[] args, Block block) {
return startThread(recv, args, false, block);
}
public static RubyThread adopt(IRubyObject recv, Thread t) {
return adoptThread(recv, t, Block.NULL_BLOCK);
}
private static RubyThread adoptThread(final IRubyObject recv, Thread t, Block block) {
final Ruby runtime = recv.getRuntime();
final RubyThread rubyThread = new RubyThread(runtime, (RubyClass) recv);
rubyThread.threadImpl = new NativeThread(rubyThread, t);
runtime.getThreadService().registerNewThread(rubyThread);
runtime.getCurrentContext().preAdoptThread();
// adoptThread does not call init, since init expects a block to be passed in
if (USE_POOLING) {
rubyThread.threadImpl = new FutureThread(rubyThread, new RubyRunnable(rubyThread, IRubyObject.NULL_ARRAY, block));
} else {
rubyThread.threadImpl = new NativeThread(rubyThread, new RubyNativeThread(rubyThread, IRubyObject.NULL_ARRAY, block));
}
rubyThread.threadImpl.start();
return rubyThread;
}
public IRubyObject initialize(IRubyObject[] args, Block block) {
if (!block.isGiven()) throw getRuntime().newThreadError("must be called with a block");
if (USE_POOLING) {
threadImpl = new FutureThread(this, new RubyRunnable(this, args, block));
} else {
threadImpl = new NativeThread(this, new RubyNativeThread(this, args, block));
}
threadImpl.start();
return this;
}
private static RubyThread startThread(final IRubyObject recv, final IRubyObject[] args, boolean callInit, Block block) {
RubyThread rubyThread = new RubyThread(recv.getRuntime(), (RubyClass) recv);
if (callInit) {
rubyThread.callInit(args, block);
} else {
// for Thread::start, which does not call the subclass's initialize
rubyThread.initialize(args, block);
}
return rubyThread;
}
private void ensureCurrent() {
if (this != getRuntime().getCurrentContext().getThread()) {
throw new RuntimeException("internal thread method called from another thread");
}
}
private void ensureNotCurrent() {
if (this == getRuntime().getCurrentContext().getThread()) {
throw new RuntimeException("internal thread method called from another thread");
}
}
public void cleanTerminate(IRubyObject result) {
finalResult = result;
isStopped = true;
}
public void pollThreadEvents() {
// check for criticalization *before* locking ourselves
threadService.waitForCritical();
ensureCurrent();
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " before");
if (killed) throw new ThreadKill();
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " after");
if (receivedException != null) {
// clear this so we don't keep re-throwing
IRubyObject raiseException = receivedException;
receivedException = null;
RubyModule kernelModule = getRuntime().getModule("Kernel");
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " before propagating exception: " + killed);
kernelModule.callMethod(getRuntime().getCurrentContext(), "raise", raiseException);
}
}
private RubyThread(Ruby runtime, RubyClass type) {
super(runtime, type);
this.threadService = runtime.getThreadService();
// set to default thread group
RubyThreadGroup defaultThreadGroup = (RubyThreadGroup)runtime.getClass("ThreadGroup").getConstant("Default");
defaultThreadGroup.add(this, Block.NULL_BLOCK);
finalResult = runtime.getNil();
}
/**
* Returns the status of the global ``abort on exception'' condition. The
* default is false. When set to true, will cause all threads to abort (the
* process will exit(0)) if an exception is raised in any thread. See also
* Thread.abort_on_exception= .
*/
public static RubyBoolean abort_on_exception(IRubyObject recv) {
Ruby runtime = recv.getRuntime();
return runtime.isGlobalAbortOnExceptionEnabled() ? recv.getRuntime().getTrue() : recv.getRuntime().getFalse();
}
public static IRubyObject abort_on_exception_set(IRubyObject recv, IRubyObject value) {
recv.getRuntime().setGlobalAbortOnExceptionEnabled(value.isTrue());
return value;
}
public static RubyThread current(IRubyObject recv) {
return recv.getRuntime().getCurrentContext().getThread();
}
public static RubyThread main(IRubyObject recv) {
return recv.getRuntime().getThreadService().getMainThread();
}
public static IRubyObject pass(IRubyObject recv) {
Ruby runtime = recv.getRuntime();
ThreadService ts = runtime.getThreadService();
boolean critical = ts.getCritical();
ts.setCritical(false);
Thread.yield();
ts.setCritical(critical);
return recv.getRuntime().getNil();
}
public static RubyArray list(IRubyObject recv) {
RubyThread[] activeThreads = recv.getRuntime().getThreadService().getActiveRubyThreads();
return recv.getRuntime().newArrayNoCopy(activeThreads);
}
private IRubyObject getSymbolKey(IRubyObject originalKey) {
if (originalKey instanceof RubySymbol) {
return originalKey;
} else if (originalKey instanceof RubyString) {
return RubySymbol.newSymbol(getRuntime(), originalKey.asSymbol());
} else if (originalKey instanceof RubyFixnum) {
getRuntime().getWarnings().warn("Do not use Fixnums as Symbols");
throw getRuntime().newArgumentError(originalKey + " is not a symbol");
} else {
throw getRuntime().newArgumentError(originalKey + " is not a symbol");
}
}
public IRubyObject aref(IRubyObject key) {
key = getSymbolKey(key);
if (!threadLocalVariables.containsKey(key)) {
return getRuntime().getNil();
}
return (IRubyObject) threadLocalVariables.get(key);
}
public IRubyObject aset(IRubyObject key, IRubyObject value) {
key = getSymbolKey(key);
threadLocalVariables.put(key, value);
return value;
}
public RubyBoolean abort_on_exception() {
return abortOnException ? getRuntime().getTrue() : getRuntime().getFalse();
}
public IRubyObject abort_on_exception_set(IRubyObject val) {
abortOnException = val.isTrue();
return val;
}
public RubyBoolean is_alive() {
return threadImpl.isAlive() ? getRuntime().getTrue() : getRuntime().getFalse();
}
public RubyThread join(IRubyObject[] args) {
long timeoutMillis = 0;
if (args.length > 0) {
if (args.length > 1) {
throw getRuntime().newArgumentError(args.length,1);
}
// MRI behavior: value given in seconds; converted to Float; less
// than or equal to zero returns immediately; returns nil
timeoutMillis = (long)(1000.0D * args[0].convertToFloat().getValue());
if (timeoutMillis <= 0) {
return null;
}
}
if (isCurrent()) {
throw getRuntime().newThreadError("thread tried to join itself");
}
try {
if (threadService.getCritical()) {
threadImpl.interrupt(); // break target thread out of critical
}
threadImpl.join(timeoutMillis);
} catch (InterruptedException iExcptn) {
assert false : iExcptn;
} catch (TimeoutException iExcptn) {
assert false : iExcptn;
} catch (ExecutionException iExcptn) {
assert false : iExcptn;
}
if (exitingException != null) {
throw exitingException;
}
return null;
}
public IRubyObject value() {
join(new IRubyObject[0]);
synchronized (this) {
return finalResult;
}
}
public IRubyObject group() {
if (threadGroup == null) {
return getRuntime().getNil();
}
return threadGroup;
}
void setThreadGroup(RubyThreadGroup rubyThreadGroup) {
threadGroup = rubyThreadGroup;
}
public IRubyObject inspect() {
// FIXME: There's some code duplication here with RubyObject#inspect
StringBuffer part = new StringBuffer();
String cname = getMetaClass().getRealClass().getName();
part.append("#<").append(cname).append(":0x");
part.append(Integer.toHexString(System.identityHashCode(this)));
if (threadImpl.isAlive()) {
if (isStopped) {
part.append(getRuntime().newString(" sleep"));
} else if (killed) {
part.append(getRuntime().newString(" aborting"));
} else {
part.append(getRuntime().newString(" run"));
}
} else {
part.append(" dead");
}
part.append(">");
return getRuntime().newString(part.toString());
}
public RubyBoolean has_key(IRubyObject key) {
key = getSymbolKey(key);
return getRuntime().newBoolean(threadLocalVariables.containsKey(key));
}
public RubyArray keys() {
IRubyObject[] keys = new IRubyObject[threadLocalVariables.size()];
return RubyArray.newArrayNoCopy(getRuntime(), (IRubyObject[])threadLocalVariables.keySet().toArray(keys));
}
public static IRubyObject critical_set(IRubyObject receiver, RubyBoolean value) {
receiver.getRuntime().getThreadService().setCritical(value.isTrue());
return value;
}
public static IRubyObject critical(IRubyObject receiver) {
return receiver.getRuntime().newBoolean(receiver.getRuntime().getThreadService().getCritical());
}
public static IRubyObject stop(IRubyObject receiver) {
RubyThread rubyThread = receiver.getRuntime().getThreadService().getCurrentContext().getThread();
Object stopLock = rubyThread.stopLock;
synchronized (stopLock) {
try {
rubyThread.isStopped = true;
// attempt to decriticalize all if we're the critical thread
receiver.getRuntime().getThreadService().setCritical(false);
stopLock.wait();
} catch (InterruptedException ie) {
// ignore, continue;
}
rubyThread.isStopped = false;
}
return receiver.getRuntime().getNil();
}
public static IRubyObject s_kill(IRubyObject receiver, RubyThread rubyThread, Block block) {
return rubyThread.kill();
}
public static IRubyObject s_exit(IRubyObject receiver, Block block) {
RubyThread rubyThread = receiver.getRuntime().getThreadService().getCurrentContext().getThread();
rubyThread.killed = true;
// attempt to decriticalize all if we're the critical thread
receiver.getRuntime().getThreadService().setCritical(false);
throw new ThreadKill();
}
public RubyBoolean isStopped() {
// not valid for "dead" state
return getRuntime().newBoolean(isStopped);
}
public RubyThread wakeup() {
synchronized (stopLock) {
stopLock.notifyAll();
}
return this;
}
public RubyFixnum priority() {
return getRuntime().newFixnum(threadImpl.getPriority());
}
public IRubyObject priority_set(IRubyObject priority) {
// FIXME: This should probably do some translation from Ruby priority levels to Java priority levels (until we have green threads)
int iPriority = RubyNumeric.fix2int(priority);
if (iPriority < Thread.MIN_PRIORITY) {
iPriority = Thread.MIN_PRIORITY;
} else if (iPriority > Thread.MAX_PRIORITY) {
iPriority = Thread.MAX_PRIORITY;
}
threadImpl.setPriority(iPriority);
return priority;
}
public IRubyObject raise(IRubyObject[] args, Block block) {
ensureNotCurrent();
Ruby runtime = getRuntime();
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " before raising");
RubyThread currentThread = getRuntime().getCurrentContext().getThread();
try {
while (!(currentThread.lock.tryLock() && this.lock.tryLock())) {
if (currentThread.lock.isHeldByCurrentThread()) currentThread.lock.unlock();
}
currentThread.pollThreadEvents();
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " raising");
receivedException = prepareRaiseException(runtime, args, block);
// interrupt the target thread in case it's blocking or waiting
threadImpl.interrupt();
} finally {
if (currentThread.lock.isHeldByCurrentThread()) currentThread.lock.unlock();
if (this.lock.isHeldByCurrentThread()) this.lock.unlock();
}
return this;
}
private IRubyObject prepareRaiseException(Ruby runtime, IRubyObject[] args, Block block) {
Arity.checkArgumentCount(getRuntime(), args, 0, 3);
if(args.length == 0) {
IRubyObject lastException = runtime.getGlobalVariables().get("$!");
if(lastException.isNil()) {
return new RaiseException(runtime, runtime.getClass("RuntimeError"), "", false).getException();
}
return lastException;
}
IRubyObject exception;
ThreadContext context = getRuntime().getCurrentContext();
if(args.length == 1) {
if(args[0] instanceof RubyString) {
return runtime.getClass("RuntimeError").newInstance(args, block);
}
if(!args[0].respondsTo("exception")) {
return runtime.newTypeError("exception class/object expected").getException();
}
exception = args[0].callMethod(context, "exception");
} else {
if (!args[0].respondsTo("exception")) {
return runtime.newTypeError("exception class/object expected").getException();
}
exception = args[0].callMethod(context, "exception", args[1]);
}
if (!exception.isKindOf(runtime.getClass("Exception"))) {
return runtime.newTypeError("exception object expected").getException();
}
if (args.length == 3) {
((RubyException) exception).set_backtrace(args[2]);
}
return exception;
}
public IRubyObject run() {
// if stopped, unstop
synchronized (stopLock) {
if (isStopped) {
isStopped = false;
stopLock.notifyAll();
}
}
return this;
}
public void sleep(long millis) throws InterruptedException {
ensureCurrent();
synchronized (stopLock) {
try {
isStopped = true;
stopLock.wait(millis);
} finally {
isStopped = false;
pollThreadEvents();
}
}
}
public IRubyObject status() {
if (threadImpl.isAlive()) {
if (isStopped) {
return getRuntime().newString("sleep");
} else if (killed) {
return getRuntime().newString("aborting");
}
return getRuntime().newString("run");
} else if (exitingException != null) {
return getRuntime().getNil();
} else {
return getRuntime().newBoolean(false);
}
}
public IRubyObject kill() {
// need to reexamine this
RubyThread currentThread = getRuntime().getCurrentContext().getThread();
try {
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " trying to kill");
while (!(currentThread.lock.tryLock() && this.lock.tryLock())) {
if (currentThread.lock.isHeldByCurrentThread()) currentThread.lock.unlock();
}
currentThread.pollThreadEvents();
if (DEBUG) System.out.println("thread " + Thread.currentThread() + " succeeded with kill");
killed = true;
threadImpl.interrupt(); // break out of wait states and blocking IO
} finally {
if (currentThread.lock.isHeldByCurrentThread()) currentThread.lock.unlock();
if (this.lock.isHeldByCurrentThread()) this.lock.unlock();
}
try {
threadImpl.join();
} catch (InterruptedException ie) {
// we were interrupted, check thread events again
currentThread.pollThreadEvents();
} catch (ExecutionException ie) {
// we were interrupted, check thread events again
currentThread.pollThreadEvents();
}
return this;
}
public IRubyObject exit() {
return kill();
}
private boolean isCurrent() {
return threadImpl.isCurrent();
}
public void exceptionRaised(RaiseException exception) {
assert isCurrent();
Ruby runtime = exception.getException().getRuntime();
if (abortOnException(runtime)) {
// FIXME: printError explodes on some nullpointer
//getRuntime().getRuntime().printError(exception.getException());
// TODO: Doesn't SystemExit have its own method to make this less wordy..
RubyException re = RubyException.newException(getRuntime(), getRuntime().getClass("SystemExit"), exception.getMessage());
re.setInstanceVariable("status", getRuntime().newFixnum(1));
threadService.getMainThread().raise(new IRubyObject[]{re}, Block.NULL_BLOCK);
} else {
exitingException = exception;
}
}
private boolean abortOnException(Ruby runtime) {
return (runtime.isGlobalAbortOnExceptionEnabled() || abortOnException);
}
public static RubyThread mainThread(IRubyObject receiver) {
return receiver.getRuntime().getThreadService().getMainThread();
}
}
|