/**
* erlyberly, erlang trace debugger
* Copyright (C) 2016 Andy Till
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package erlyberly.node;
import static erlyberly.node.OtpUtil.OK_ATOM;
import static erlyberly.node.OtpUtil.atom;
import static erlyberly.node.OtpUtil.isTupleTagged;
import static erlyberly.node.OtpUtil.list;
import static erlyberly.node.OtpUtil.tuple;
import java.io.IOException;
import java.io.InputStream;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import com.ericsson.otp.erlang.OtpAuthException;
import com.ericsson.otp.erlang.OtpConn;
import com.ericsson.otp.erlang.OtpErlangAtom;
import com.ericsson.otp.erlang.OtpErlangBinary;
import com.ericsson.otp.erlang.OtpErlangDecodeException;
import com.ericsson.otp.erlang.OtpErlangException;
import com.ericsson.otp.erlang.OtpErlangExit;
import com.ericsson.otp.erlang.OtpErlangFun;
import com.ericsson.otp.erlang.OtpErlangInt;
import com.ericsson.otp.erlang.OtpErlangList;
import com.ericsson.otp.erlang.OtpErlangLong;
import com.ericsson.otp.erlang.OtpErlangObject;
import com.ericsson.otp.erlang.OtpErlangPid;
import com.ericsson.otp.erlang.OtpErlangString;
import com.ericsson.otp.erlang.OtpErlangTuple;
import com.ericsson.otp.erlang.OtpMbox;
import com.ericsson.otp.erlang.OtpPeer;
import com.ericsson.otp.erlang.OtpSelfNode;
import erlyberly.ModFunc;
import erlyberly.PrefBind;
import erlyberly.ProcInfo;
import erlyberly.SeqTraceLog;
import erlyberly.TraceLog;
import javafx.application.Platform;
import javafx.beans.Observable;
import javafx.beans.property.SimpleBooleanProperty;
import javafx.beans.property.SimpleObjectProperty;
import javafx.beans.property.SimpleStringProperty;
import javafx.beans.value.ChangeListener;
import javafx.beans.value.ObservableValue;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
public class NodeAPI {
private static final OtpErlangAtom ERLYBERLY_TRACE_OVERLOAD_ATOM = atom("erlyberly_trace_overload");
private static final String ERLYBERLY = "erlyberly";
private static final String CANNOT_RUN_THIS_METHOD_FROM_THE_FX_THREAD = "cannot run this method from the FX thread";
private static final OtpErlangAtom ERLYBERLY_TRACE_LOG = atom("erlyberly_trace_log");
private static final OtpErlangAtom ERLYBERLY_XREF_STARTED_ATOM = atom("erlyberly_xref_started");
private static final OtpErlangAtom ERLYBERLY_ERROR_REPORT_ATOM = atom("erlyberly_error_report");
private static final OtpErlangAtom ERLYBERLY_MODULE_RELOADED_ATOM = atom("erlyberly_module_loaded");
private static final OtpErlangAtom ERLYBERLY_ATOM = new OtpErlangAtom(ERLYBERLY);
private static final OtpErlangAtom BET_SERVICES_MSG_ATOM = new OtpErlangAtom("add_locator");
private static final OtpErlangAtom REX_ATOM = atom("rex");
public interface RpcCallback<T> {
void callback(T result);
}
private static final OtpErlangAtom MODULE_ATOM = new OtpErlangAtom("module");
private static final String ERLYBERLY_BEAM_PATH = "/erlyberly/beam/erlyberly.beam";
private static final int BEAM_SIZE_LIMIT = 1024 * 50;
private static final AtomicLong CHECK_ALIVE_THREAD_COUNTER = new AtomicLong();
private final TraceManager traceManager;
private final SimpleBooleanProperty connectedProperty;
private final SimpleBooleanProperty xrefStartedProperty;
private final SimpleStringProperty summary;
private OtpConn connection;
private OtpSelfNode self;
private String remoteNodeName;
private String cookie;
private volatile Thread checkAliveThread;
private final SimpleObjectProperty<AppProcs> appProcs;
private OtpMbox mbox;
private volatile boolean connected = false;
private final ObservableList<OtpErlangObject> crashReports = FXCollections.observableArrayList();
private boolean manuallyDisconnected = false;
private RpcCallback<OtpErlangTuple> moduleLoadedCallback;
/**
* Called when a trace log is received.
* <br/>
* Should only accessed from the FX thread.
*/
private RpcCallback<TraceLog> traceLogCallback;
/**
* When tracing is paused, NodeAPI will stop all traces. When tracing is un-suspended
* the DbgController must reapply all the traces.
*/
private final SimpleBooleanProperty suspendedProperty;
public NodeAPI() {
traceManager = new TraceManager();
connectedProperty = new SimpleBooleanProperty();
connectedProperty.addListener(new ChangeListener<Boolean>() {
@Override
public void changed(ObservableValue<? extends Boolean> obv, Boolean o, Boolean n) {
connected = n;
if(!n) {
xrefStartedProperty.set(false);
}
}});
summary = new SimpleStringProperty("erlyberly not connected");
appProcs = new SimpleObjectProperty<AppProcs>(new AppProcs(0, LocalDateTime.now()));
connectedProperty.addListener(this::summaryUpdater);
xrefStartedProperty = new SimpleBooleanProperty(false);
suspendedProperty = new SimpleBooleanProperty();
}
public NodeAPI connectionInfo(String remoteNodeName, String cookie) {
this.remoteNodeName = remoteNodeName;
this.cookie = cookie;
return this;
}
public ObservableList<OtpErlangObject> getCrashReports() {
return crashReports;
}
public SimpleObjectProperty<AppProcs> appProcsProperty() {
return appProcs;
}
public synchronized void manualConnect() throws IOException, OtpErlangException, OtpAuthException {
// TODO: here, we've cleared (set to false) being Manually/intentionally disconnected.
manuallyDisconnected = false;
connect();
}
public synchronized void connect() throws IOException, OtpErlangException, OtpAuthException {
assert !Platform.isFxApplicationThread() : CANNOT_RUN_THIS_METHOD_FROM_THE_FX_THREAD;
// clear previous connections and threads if any, before we reconnect
// TODO: investigate whether we need this ...
disconnect();
self = new OtpSelfNode("erlyberly-" + System.currentTimeMillis());
if(!cookie.isEmpty()) {
self.setCookie(cookie);
}
// if the node name does not contain a host then assume it is on the
// same machine
if(!remoteNodeName.contains("@")) {
String[] split = self.toString().split("\\@");
remoteNodeName += "@" + split[1];
}
connection = self.connect(new OtpPeer(remoteNodeName));
mbox = self.createMbox();
loadRemoteErlyberly();
addErrorLoggerHandler();
// start dbg so we can listen for module loads
ensureDbgStarted();
loadModulesOnPath(PrefBind.getOrDefault("loadModulesRegex", "").toString());
Platform.runLater(() -> { connectedProperty.set(true); });
if(checkAliveThread == null) {
checkAliveThread = new CheckAliveThread();
checkAliveThread.start();
}
}
public void manuallyDisconnect() throws IOException, OtpErlangException{
// TODO: have a look at this: ( How can we properly "Close", or is the below acceptable? )
// com.ericsson.otp.erlang.OtpErlangExit: 'Remote has closed connection'
// at com.ericsson.otp.erlang.AbstractConnection.run(AbstractConnection.java:733)
manuallyDisconnected = true;
stopAllTraces();
removeErrorLoggerHandler();
unloadRemoteErlyberly();
mbox.close();
Platform.runLater(() -> { connectedProperty.set(false); });
}
public void disconnect() {
try {
if (connection != null)
connection.close();
}
catch(Exception e) {
System.out.println(e);
}
try {
if (self != null)
self.close();
}
catch(Exception e) {
System.out.println(e);
}
connection = null;
self = null;
connected = false;
Platform.runLater(() -> {
suspendedProperty.set(false);
});
}
private synchronized void ensureDbgStarted() throws IOException, OtpErlangException {
sendRPC(
ERLYBERLY, "ensure_dbg_started",
list(tuple(atom(self.node()), mbox.self()))
);
// flush the return value
receiveRPC();
}
private void loadModulesOnPath(String regex) throws IOException, OtpErlangException {
if(regex == null || "".equals(regex))
return;
sendRPC(
ERLYBERLY, "load_modules_on_path",
list(new OtpErlangString(regex))
);
// flush the return value
receiveRPC();
}
private synchronized void addErrorLoggerHandler() throws IOException, OtpErlangException {
OtpErlangList args = OtpUtil.list(mbox.self());
sendRPC(
"error_logger", "add_report_handler",
OtpUtil.list(ERLYBERLY_ATOM, args)
);
// flush the return value
receiveRPC();
}
private synchronized void removeErrorLoggerHandler() throws IOException, OtpErlangException {
OtpErlangList args = OtpUtil.list(mbox.self());
sendRPC(
"error_logger", "delete_report_handler",
OtpUtil.list(ERLYBERLY_ATOM, args)
);
// flush the return value
receiveRPC();
}
class CheckAliveThread extends Thread {
public CheckAliveThread() {
setDaemon(true);
setName("Erlyberly Check Alive "+ CHECK_ALIVE_THREAD_COUNTER.incrementAndGet());
}
@Override
public void run() {
while(true) {
if (!manuallyDisconnected) {
ensureAlive();
}
mySleep(150);
}
}
private synchronized boolean ensureAlive() {
try {
receiveRPC(0);
if(connection != null && connection.isAlive())
return true;
}
catch(OtpErlangExit oee) {
// an exit is what we're checking for so no need to log it
}
catch (OtpErlangException | IOException e1) {
e1.printStackTrace();
}
Platform.runLater(() -> { connectedProperty.set(false); });
while(true) {
try {
if(!manuallyDisconnected){
connect();
break;
}
}
catch(Exception e) {
int millis = 50;
mySleep(millis);
}
}
return true;
}
}
private void loadRemoteErlyberly() throws IOException, OtpErlangException {
OtpErlangBinary otpErlangBinary = new OtpErlangBinary(loadBeamFile());
sendRPC("code", "load_binary",
list(
atom(ERLYBERLY),
new OtpErlangString(ERLYBERLY_BEAM_PATH),
otpErlangBinary));
OtpErlangObject result = receiveRPC();
if(result instanceof OtpErlangTuple) {
OtpErlangObject e0 = ((OtpErlangTuple) result).elementAt(0);
if(!MODULE_ATOM.equals(e0)) {
throw new RuntimeException("error loading the erlyberly module, result was " + result);
}
}
else {
throw new RuntimeException("error loading the erlyberly module, result was " + result);
}
}
private void unloadRemoteErlyberly() throws IOException, OtpErlangException {
sendRPC("code", "purge", list(atom(ERLYBERLY)));
receiveRPC();
sendRPC("code", "delete", list(atom(ERLYBERLY)));
receiveRPC();
sendRPC("code", "soft_purge", list(atom(ERLYBERLY)));
receiveRPC();
}
private OtpErlangObject receiveRPC() throws IOException, OtpErlangException {
int timeout = 5000;
return receiveRPC(timeout);
}
private OtpErlangObject receiveRPC(int timeout) throws OtpErlangExit,
OtpErlangDecodeException, IOException, OtpErlangException {
OtpErlangTuple receive = OtpUtil.receiveRPC(mbox, timeout);
if(receive == null) {
return null;
}
else if(isTupleTagged(ERLYBERLY_TRACE_LOG, receive)) {
traceLogNotification(receive);
return receiveRPC(timeout);
}
else if(isTupleTagged(ERLYBERLY_ERROR_REPORT_ATOM, receive)) {
Platform.runLater(() -> { crashReports.add(receive.elementAt(1)); });
return receiveRPC(timeout);
}
else if(isTupleTagged(ERLYBERLY_MODULE_RELOADED_ATOM, receive)) {
Platform.runLater(() -> {
if(moduleLoadedCallback != null)
moduleLoadedCallback.callback((OtpErlangTuple) receive.elementAt(2));
});
return receiveRPC(timeout);
}
else if(isTupleTagged(ERLYBERLY_TRACE_OVERLOAD_ATOM, receive)) {
Platform.runLater(() -> { suspendedProperty.set(true); });
}
else if(!isTupleTagged(REX_ATOM, receive)) {
throw new RuntimeException("Expected tuple tagged with atom rex but got " + receive);
}
OtpErlangObject result = receive.elementAt(1);
// hack to support certain projects, don't ask...
if(isTupleTagged(BET_SERVICES_MSG_ATOM, result)) {
result = receiveRPC(timeout);
}
else if(isTupleTagged(ERLYBERLY_XREF_STARTED_ATOM, result)) {
Platform.runLater(() -> { xrefStartedProperty.set(true); });
return receiveRPC(timeout);
}
return result;
}
private void traceLogNotification(OtpErlangTuple receive) {
Platform.runLater(() -> {
OtpErlangTuple traceLog = (OtpErlangTuple) receive.elementAt(1);
List<TraceLog> collatedTraces = traceManager.collateTraceSingle(traceLog);
if(traceLogCallback != null) {
for (TraceLog log : collatedTraces) {
traceLogCallback.callback(log);
}
}
});
}
private static byte[] loadBeamFile() throws IOException {
InputStream resourceAsStream = OtpUtil.class.getResourceAsStream(ERLYBERLY_BEAM_PATH);
byte[] b = new byte[BEAM_SIZE_LIMIT];
int total = 0;
int read = 0;
do {
total += read;
read = resourceAsStream.read(b, total, BEAM_SIZE_LIMIT - total);
} while (read != -1);
if(total >= BEAM_SIZE_LIMIT) {
throw new RuntimeException("erlyberly.beam file is too big");
}
return Arrays.copyOf(b, total);
}
public synchronized void retrieveProcessInfo(List<ProcInfo> processes) throws Exception {
assert !Platform.isFxApplicationThread() : CANNOT_RUN_THIS_METHOD_FROM_THE_FX_THREAD;
if(connection == null || !connected)
return;
OtpErlangObject receiveRPC = null;
try {
sendRPC(ERLYBERLY, "process_info", new OtpErlangList());
receiveRPC = receiveRPC();
OtpErlangList received = (OtpErlangList) receiveRPC;
for (OtpErlangObject recv : received) {
if(recv instanceof OtpErlangList) {
OtpErlangList pinfo = (OtpErlangList) recv;
Map<Object, Object> propsToMap = OtpUtil.propsToMap(pinfo);
processes.add(ProcInfo.toProcessInfo(propsToMap));
}
}
Platform.runLater(() -> { appProcs.set(new AppProcs(processes.size(), LocalDateTime.now())); });
}
catch (ClassCastException e) {
throw new RuntimeException("unexpected result: " + receiveRPC, e);
}
}
private void mySleep(int millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e1) {
e1.printStackTrace();
}
}
public synchronized OtpErlangList requestFunctions() throws Exception {
assert !Platform.isFxApplicationThread() : CANNOT_RUN_THIS_METHOD_FROM_THE_FX_THREAD;
sendRPC(ERLYBERLY, "module_functions", new OtpErlangList());
return (OtpErlangList) receiveRPC();
}
public synchronized void startTrace(ModFunc mf, int maxQueueLen) throws Exception {
assert mf.getFuncName() != null : "function name cannot be null";
// if tracing is suspended, we can't apply a new trace because that will
// leave us in a state where some traces are active and others are not
if(isSuspended())
return;
sendRPC(ERLYBERLY, "start_trace", toStartTraceFnArgs(mf, maxQueueLen));
OtpErlangObject result = receiveRPC();
if(!isTupleTagged(OK_ATOM, result)) {
System.out.println(result);
// TODO notify caller of failure!
return;
}
}
public synchronized void stopTrace(ModFunc mf) throws Exception {
assert mf.getFuncName() != null : "function name cannot be null";
// if tracing is suspended, do not attempt to remove a trace, it should already
// be removed
if(isSuspended())
return;
sendRPC(ERLYBERLY, "stop_trace",
list(
OtpUtil.atom(mf.getModuleName()),
OtpUtil.atom(mf.getFuncName()),
new OtpErlangInt(mf.getArity()),
new OtpErlangAtom(mf.isExported())
));
receiveRPC();
}
public synchronized void stopAllTraces() throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "stop_traces", list());
receiveRPC();
}
private OtpErlangList toStartTraceFnArgs(ModFunc mf, int maxQueueLen) {
String node = self.node();
OtpErlangPid self2 = mbox.self();
return list(
tuple(OtpUtil.atom(node), self2),
atom(mf.getModuleName()),
atom(mf.getFuncName()),
mf.getArity(),
maxQueueLen
);
}
public SimpleBooleanProperty connectedProperty() {
return connectedProperty;
}
public SimpleBooleanProperty xrefStartedProperty() {
return xrefStartedProperty;
}
private void sendRPC(String module, String function, OtpErlangList args) throws IOException {
OtpUtil.sendRPC(connection, mbox, atom(module), atom(function), args);
}
public synchronized List<TraceLog> collectTraceLogs() throws Exception {
sendRPC(ERLYBERLY, "collect_trace_logs", new OtpErlangList());
OtpErlangObject prcResult = receiveRPC();
if(!isTupleTagged(OK_ATOM, prcResult)) {
if(prcResult != null) {
System.out.println(prcResult);
}
return new ArrayList<TraceLog>();
}
OtpErlangList traceLogs = (OtpErlangList) ((OtpErlangTuple) prcResult).elementAt(1);
return traceManager.collateTraces(traceLogs);
}
public synchronized List<SeqTraceLog> collectSeqTraceLogs() throws Exception {
sendRPC(ERLYBERLY, "collect_seq_trace_logs", new OtpErlangList());
OtpErlangObject prcResult = receiveRPC();
if(!isTupleTagged(OK_ATOM, prcResult)) {
return new ArrayList<SeqTraceLog>();
}
ArrayList<SeqTraceLog> seqLogs = new ArrayList<SeqTraceLog>();
try {
OtpErlangList traceLogs = (OtpErlangList) ((OtpErlangTuple) prcResult)
.elementAt(1);
for (OtpErlangObject otpErlangObject : traceLogs) {
seqLogs.add(SeqTraceLog.build(OtpUtil
.propsToMap((OtpErlangList) otpErlangObject)));
}
}
catch (ClassCastException e) {
System.out.println("did not understand result from collect_seq_trace_logs " + prcResult);
e.printStackTrace();
}
return seqLogs;
}
public ObservableValue<? extends String> summaryProperty() {
return summary;
}
private void summaryUpdater(Observable o, Boolean wasConnected, Boolean isConnected) {
String summaryText = ERLYBERLY;
OtpSelfNode self2 = self;
if(self2 != null && !wasConnected && isConnected)
summaryText = self2.node() + " connected to " + this.remoteNodeName;
else if(wasConnected && !isConnected)
summaryText = "erlyberly, connection lost. reconnecting...";
summary.set(summaryText);
}
public synchronized void seqTrace(ModFunc mf) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "seq_trace",
list(
tuple(OtpUtil.atom(self.node()), mbox.self()),
atom(mf.getModuleName()),
atom(mf.getFuncName()),
mf.getArity(),
new OtpErlangAtom(mf.isExported())
));
OtpErlangObject result = receiveRPC();
System.out.println(result);
}
public synchronized OtpErlangObject getProcessState(String pidString) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "get_process_state", list(pidString));
OtpErlangObject result = receiveRPC();
if(isTupleTagged(OK_ATOM, result)) {
return ((OtpErlangTuple)result).elementAt(1);
}
return null;
}
public synchronized Map<Object, Object> erlangMemory() throws IOException, OtpErlangException {
sendRPC("erlang", "memory", list());
OtpErlangList result = (OtpErlangList) receiveRPC();
return OtpUtil.propsToMap(result);
}
public boolean isConnected() {
return connected;
}
public boolean manuallyDisconnected(){
return manuallyDisconnected;
}
public synchronized OtpErlangObject callGraph(OtpErlangList skippedModuleAtoms, OtpErlangAtom module, OtpErlangAtom function, OtpErlangLong arity) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "xref_analysis", list(skippedModuleAtoms, module, function, arity));
OtpErlangObject result = receiveRPC();
return result;
}
/**
* Start xref but
*/
public synchronized void asyncEnsureXRefStarted() throws IOException {
sendRPC(ERLYBERLY, "ensure_xref_started", list());
}
public synchronized String moduleFunctionSourceCode(String module, String function, Integer arity) throws IOException, OtpErlangException {
OtpErlangInt otpArity = new OtpErlangInt(arity);
sendRPC(ERLYBERLY, "get_source_code", list( tuple(atom(module), atom(function), otpArity) ));
OtpErlangObject result = receiveRPC();
return returnCode(result, "Failed to get source code for " + module + ":" + function + "/" + arity.toString() + ".");
}
public synchronized String moduleFunctionSourceCode(String module) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "get_source_code", list( atom(module) ));
OtpErlangObject result = receiveRPC();
return returnCode(result, "Failed to get source code for " + module + ".");
}
public synchronized String moduleFunctionAbstCode(String module) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "get_abstract_code", list(atom(module)));
OtpErlangObject result = receiveRPC();
return returnCode(result, "Failed to get abstract code for " + module + ".");
}
public synchronized String moduleFunctionAbstCode(String module, String function, Integer arity) throws IOException, OtpErlangException {
OtpErlangInt otpArity = new OtpErlangInt(arity);
sendRPC(ERLYBERLY, "get_abstract_code", list( tuple(atom(module), atom(function), otpArity) ));
OtpErlangObject result = receiveRPC();
return returnCode(result, "Failed to get abstract code for " + module + ".");
}
public String returnCode(OtpErlangObject result, String errorResponse){
if(isTupleTagged(OK_ATOM, result)) {
OtpErlangBinary bin = (OtpErlangBinary) ((OtpErlangTuple)result).elementAt(1);
String ss = new String(bin.binaryValue());
return ss;
} else {
OtpErlangBinary bin = (OtpErlangBinary) ((OtpErlangTuple)result).elementAt(1);
String err = new String(bin.binaryValue());
System.out.println(err);
return errorResponse;
}
}
public synchronized OtpErlangList dictToPropslist(OtpErlangObject dict) throws IOException, OtpErlangException {
sendRPC("dict", "to_list", list(dict));
return (OtpErlangList) receiveRPC(5000);
}
/**
* Set the callback that is invoked when erlyberly receives a message that a
* module has been loaded, or reloaded by the VM. The callback argument is in
* the format {module(), ExportedFuncs, UnexportedFuncs}. A function is the
* format {atom(), integer()}.
*/
public void setModuleLoadedCallback(RpcCallback<OtpErlangTuple> aModuleLoadedCallback) {
moduleLoadedCallback = aModuleLoadedCallback;
}
public synchronized String decompileFun(OtpErlangFun fun) throws IOException, OtpErlangException {
sendRPC(ERLYBERLY, "saleyn_fun_src", list(fun));
OtpErlangObject received = receiveRPC(5000);
if(received instanceof OtpErlangString) {
OtpErlangString otpString = (OtpErlangString) received;
return otpString.stringValue();
}
else {
throw new OtpErlangException(Objects.toString(received));
}
}
public RpcCallback<TraceLog> getTraceLogCallback() {
return traceLogCallback;
}
public void setTraceLogCallback(RpcCallback<TraceLog> traceLogCallback) {
this.traceLogCallback = traceLogCallback;
}
public void toggleSuspended() throws OtpErlangException, IOException {
assert Platform.isFxApplicationThread();
if(!isSuspended())
stopAllTraces();
suspendedProperty.set(!isSuspended());
}
public boolean isSuspended() {
assert Platform.isFxApplicationThread();
return suspendedProperty.get();
}
public SimpleBooleanProperty suspendedProperty() {
assert Platform.isFxApplicationThread();
return suspendedProperty;
}
}