int timeout;
char *launch;
gboolean embedding;
+ gboolean defer;
} AgentConfig;
typedef struct
static FILE *log_file;
-/* Classes whose class load event has been sent */
-static GHashTable *loaded_classes;
-
/* Assemblies whose assembly load event has no been sent yet */
static GPtrArray *pending_assembly_loads;
-/* Types whose type load event has no been sent yet */
-static GPtrArray *pending_type_loads;
-
/* Whenever the debugger thread has exited */
static gboolean debugger_thread_exited;
static void appdomain_unload (MonoProfiler *prof, MonoDomain *domain);
+static void emit_appdomain_load (gpointer key, gpointer value, gpointer user_data);
+
+static void emit_thread_start (gpointer key, gpointer value, gpointer user_data);
+
static void invalidate_each_thread (gpointer key, gpointer value, gpointer user_data);
static void assembly_load (MonoProfiler *prof, MonoAssembly *assembly, int result);
static void assembly_unload (MonoProfiler *prof, MonoAssembly *assembly);
+static void emit_assembly_load (gpointer assembly, gpointer user_data);
+
+static void emit_type_load (gpointer key, gpointer type, gpointer user_data);
+
static void start_runtime_invoke (MonoProfiler *prof, MonoMethod *method);
static void end_runtime_invoke (MonoProfiler *prof, MonoMethod *method);
static void clear_event_requests_for_assembly (MonoAssembly *assembly);
+static void clear_types_for_assembly (MonoAssembly *assembly);
+
static void clear_breakpoints_for_domain (MonoDomain *domain);
-static void clear_types_for_assembly (MonoAssembly *assembly);
+static void process_profiler_event (EventKind event, gpointer arg);
/* Submodule init/cleanup */
static void breakpoints_init (void);
fprintf (stderr, " address=<hostname>:<port>\tAddress to connect to (mandatory)\n");
fprintf (stderr, " loglevel=<n>\t\t\tLog level (defaults to 0)\n");
fprintf (stderr, " logfile=<file>\t\tFile to log to (defaults to stdout)\n");
- fprintf (stderr, " suspend=y/n\t\t\tWhenever to suspend after startup.\n");
+ fprintf (stderr, " suspend=y/n\t\t\tWhether to suspend after startup.\n");
fprintf (stderr, " timeout=<n>\t\t\tTimeout for connecting in milliseconds.\n");
+ fprintf (stderr, " server=y/n\t\t\tWhether to listen for a client connection.\n");
fprintf (stderr, " help\t\t\t\tPrint this help.\n");
}
agent_config.enabled = TRUE;
agent_config.suspend = TRUE;
agent_config.server = FALSE;
+ agent_config.defer = FALSE;
+ agent_config.address = NULL;
args = g_strsplit (options, ",", -1);
for (ptr = args; ptr && *ptr; ptr ++) {
}
}
+ if (agent_config.server && !agent_config.suspend) {
+ /* Waiting for deferred attachment */
+ agent_config.defer = TRUE;
+ if (agent_config.address == NULL) {
+ agent_config.address = g_strdup_printf ("0.0.0.0:%u", 56000 + (GetCurrentProcessId () % 1000));
+ }
+ }
+
if (agent_config.transport == NULL) {
fprintf (stderr, "debugger-agent: The 'transport' option is mandatory.\n");
exit (1);
tid_to_thread_obj = mono_g_hash_table_new_type (NULL, NULL, MONO_HASH_VALUE_GC);
MONO_GC_REGISTER_ROOT_FIXED (tid_to_thread_obj);
- loaded_classes = g_hash_table_new (mono_aligned_addr_hash, NULL);
pending_assembly_loads = g_ptr_array_new ();
- pending_type_loads = g_ptr_array_new ();
domains = g_hash_table_new (mono_aligned_addr_hash, NULL);
log_level = agent_config.log_level;
embedding = agent_config.embedding;
+ disconnected = TRUE;
if (agent_config.log_file) {
log_file = fopen (agent_config.log_file, "w+");
#define HAVE_GETADDRINFO 1
#endif
+static int
+transport_accept (int socket_fd)
+{
+ conn_fd = accept (socket_fd, NULL, NULL);
+ if (conn_fd == -1) {
+ fprintf (stderr, "debugger-agent: Unable to listen on %d\n", socket_fd);
+ } else {
+ DEBUG (1, fprintf (log_file, "Accepted connection from client, connection fd=%d.\n", conn_fd));
+ }
+
+ return conn_fd;
+}
+
static gboolean
transport_handshake (void)
{
{
int flag = 1;
int result = setsockopt (conn_fd,
- IPPROTO_TCP,
- TCP_NODELAY,
- (char *) &flag,
- sizeof(int));
+ IPPROTO_TCP,
+ TCP_NODELAY,
+ (char *) &flag,
+ sizeof(int));
g_assert (result >= 0);
}
return TRUE;
}
-static int
-transport_accept (int socket_fd)
-{
- conn_fd = accept (socket_fd, NULL, NULL);
- if (conn_fd == -1) {
- fprintf (stderr, "debugger-agent: Unable to listen on %d\n", socket_fd);
- } else {
- DEBUG (1, fprintf (log_file, "Accepted connection from client, connection fd=%d.\n", conn_fd));
- }
-
- return conn_fd;
-}
-
/*
* transport_connect:
*
#endif
}
+ if (agent_config.defer)
+ return;
+
DEBUG (1, fprintf (log_file, "Listening on %s:%d (timeout=%d ms)...\n", host, port, agent_config.timeout));
if (agent_config.timeout) {
#endif
#endif
}
-
- if (!transport_handshake ())
+
+ disconnected = !transport_handshake ();
+ if (disconnected)
exit (1);
}
/* Close the read part only so it can still send back replies */
/* Also shut down the connection listener so that we can exit normally */
#ifdef HOST_WIN32
- shutdown (conn_fd, SD_RECEIVE);
+ /* SD_RECEIVE doesn't break the recv in the debugger thread */
+ shutdown (conn_fd, SD_BOTH);
shutdown (listen_fd, SD_BOTH);
closesocket (listen_fd);
#else
*/
//WaitForSingleObject (debugger_thread_handle, INFINITE);
if (GetCurrentThreadId () != debugger_thread_id) {
- mono_mutex_lock (&debugger_thread_exited_mutex);
- if (!debugger_thread_exited)
- {
+ do {
+ mono_mutex_lock (&debugger_thread_exited_mutex);
+ if (!debugger_thread_exited) {
#ifdef HOST_WIN32
- if (WAIT_TIMEOUT == WaitForSingleObject(debugger_thread_exited_cond, 0)) {
- mono_mutex_unlock (&debugger_thread_exited_mutex);
- Sleep(0);
- mono_mutex_lock (&debugger_thread_exited_mutex);
- }
+ if (WAIT_TIMEOUT == WaitForSingleObject(debugger_thread_exited_cond, 0)) {
+ mono_mutex_unlock (&debugger_thread_exited_mutex);
+ Sleep(1);
+ mono_mutex_lock (&debugger_thread_exited_mutex);
+ }
#else
- mono_cond_wait (&debugger_thread_exited_cond, &debugger_thread_exited_mutex);
+ mono_cond_wait (&debugger_thread_exited_cond, &debugger_thread_exited_mutex);
#endif
- }
- mono_mutex_unlock (&debugger_thread_exited_mutex);
+ }
+ mono_mutex_unlock (&debugger_thread_exited_mutex);
+ } while (!debugger_thread_exited);
}
#ifdef HOST_WIN32
typedef struct {
/* Maps runtime structure -> Id */
GHashTable *val_to_id [ID_NUM];
+ /* Classes whose class load event has been sent */
+ GHashTable *loaded_classes;
} AgentDomainInfo;
/* Maps id -> Id */
for (i = 0; i < ID_NUM; ++i)
if (info->val_to_id [i])
g_hash_table_destroy (info->val_to_id [i]);
+ g_hash_table_destroy (info->loaded_classes);
g_free (info);
}
mono_loader_unlock ();
}
+/*
+ * Called when deferred debugger session is attached,
+ * after the VM start event has been sent successfully
+ */
+static void
+mono_debugger_agent_on_attach (void)
+{
+ /* Emit load events for currently loaded appdomains, assemblies, and types */
+ mono_loader_lock ();
+ g_hash_table_foreach (domains, emit_appdomain_load, NULL);
+ mono_g_hash_table_foreach (tid_to_thread, emit_thread_start, NULL);
+ mono_assembly_foreach (emit_assembly_load, NULL);
+ mono_loader_unlock ();
+}
+
+static AgentDomainInfo*
+get_agent_domain_info (MonoDomain *domain)
+{
+ AgentDomainInfo *info = NULL;
+
+ mono_domain_lock (domain);
+
+ info = domain_jit_info (domain)->agent_info;
+ if (!info) {
+ info = domain_jit_info (domain)->agent_info = g_new0 (AgentDomainInfo, 1);
+ info->loaded_classes = g_hash_table_new (mono_aligned_addr_hash, NULL);
+ }
+
+ mono_domain_unlock (domain);
+
+ return info;
+}
+
static int
get_id (MonoDomain *domain, IdType type, gpointer val)
{
mono_domain_lock (domain);
- if (!domain_jit_info (domain)->agent_info)
- domain_jit_info (domain)->agent_info = g_new0 (AgentDomainInfo, 1);
- info = domain_jit_info (domain)->agent_info;
+ info = get_agent_domain_info (domain);
+
if (info->val_to_id [type] == NULL)
info->val_to_id [type] = g_hash_table_new (mono_aligned_addr_hash, NULL);
if (ji) {
/* Running managed code, will be suspended by the single step code */
- DEBUG (1, printf ("[%p] Received interrupt while at %s(%p), continuing.\n", (gpointer)GetCurrentThreadId (), ji->method->name, mono_arch_ip_from_context (sigctx)));
+ DEBUG (1, fprintf (log_file, "[%p] Received interrupt while at %s(%p), continuing.\n", (gpointer)GetCurrentThreadId (), ji->method->name, mono_arch_ip_from_context (sigctx)));
return TRUE;
} else {
/*
// FIXME: printf is not signal safe, but this is only used during
// debugger debugging
if (sigctx)
- DEBUG (1, printf ("[%p] Received interrupt while at %p, treating as suspended.\n", (gpointer)GetCurrentThreadId (), mono_arch_ip_from_context (sigctx)));
+ DEBUG (1, fprintf (log_file, "[%p] Received interrupt while at %p, treating as suspended.\n", (gpointer)GetCurrentThreadId (), mono_arch_ip_from_context (sigctx)));
//save_thread_context (&ctx);
if (!tls->thread)
guint8 *ip = MONO_CONTEXT_GET_IP (ctx);
MonoJitInfo *ji;
+ if (mono_loader_lock_is_owned_by_self ()) {
+ /*
+ * Shortcut for the check in suspend_current (). This speeds up processing
+ * when executing long running code inside the loader lock, i.e. assembly load
+ * hooks.
+ */
+ return;
+ }
+
if (debugger_thread_id == GetCurrentThreadId ())
return;
if (WAIT_TIMEOUT == WaitForSingleObject(suspend_cond, 0))
{
mono_mutex_unlock (&suspend_mutex);
- Sleep(0);
+ Sleep(1);
mono_mutex_lock (&suspend_mutex);
}
else
tls->frames_up_to_date = TRUE;
}
+/*
+ * GHFunc to emit an appdomain creation event
+ * @param key Don't care
+ * @param value A loaded appdomain
+ * @param user_data Don't care
+ */
+static void
+emit_appdomain_load (gpointer key, gpointer value, gpointer user_data)
+{
+ process_profiler_event (EVENT_KIND_APPDOMAIN_CREATE, value);
+ g_hash_table_foreach (get_agent_domain_info (value)->loaded_classes, emit_type_load, NULL);
+}
+
+/*
+ * GHFunc to emit a thread start event
+ * @param key A thread id
+ * @param value A thread object
+ * @param user_data Don't care
+ */
+static void
+emit_thread_start (gpointer key, gpointer value, gpointer user_data)
+{
+ if (GPOINTER_TO_INT (key) != debugger_thread_id)
+ process_profiler_event (EVENT_KIND_THREAD_START, value);
+}
+
+/*
+ * GFunc to emit an assembly load event
+ * @param value A loaded assembly
+ * @param user_data Don't care
+ */
+static void
+emit_assembly_load (gpointer value, gpointer user_data)
+{
+ process_profiler_event (EVENT_KIND_ASSEMBLY_LOAD, value);
+}
+
+/*
+ * GFunc to emit a type load event
+ * @param value A loaded type
+ * @param user_data Don't care
+ */
+static void
+emit_type_load (gpointer key, gpointer value, gpointer user_data)
+{
+ process_profiler_event (EVENT_KIND_TYPE_LOAD, value);
+}
+
+
/*
* EVENT HANDLING
*/
Buffer buf;
GSList *l;
MonoDomain *domain = mono_domain_get ();
- MonoThread *thread;
+ MonoThread *thread = NULL;
gboolean send_success = FALSE;
if (!inited) {
if (events == NULL)
return;
-
- if (debugger_thread_id == GetCurrentThreadId () && event != EVENT_KIND_VM_DEATH)
- // FIXME: Send these with a NULL thread, don't suspend the current thread
- return;
+
+ if (agent_config.defer) {
+ /* Make sure the thread id is always set when doing deferred debugging */
+ if (debugger_thread_id == GetCurrentThreadId ())
+ thread = mono_thread_get_main ();
+ else thread = mono_thread_current ();
+ } else {
+ if (debugger_thread_id == GetCurrentThreadId () && event != EVENT_KIND_VM_DEATH)
+ // FIXME: Send these with a NULL thread, don't suspend the current thread
+ return;
+ }
buffer_init (&buf, 128);
buffer_add_byte (&buf, suspend_policy);
buffer_add_byte (&buf, event); // event kind
buffer_add_int (&buf, GPOINTER_TO_INT (l->data)); // request id
- thread = mono_thread_current ();
+ if (!thread)
+ thread = mono_thread_current ();
- if (event == EVENT_KIND_VM_START)
+ if (event == EVENT_KIND_VM_START && arg != NULL)
thread = arg;
- else if (event == EVENT_KIND_THREAD_START)
- g_assert (mono_thread_internal_current () == arg);
buffer_add_objid (&buf, (MonoObject*)thread); // thread
if (event == EVENT_KIND_VM_START) {
suspend_policy = agent_config.suspend ? SUSPEND_POLICY_ALL : SUSPEND_POLICY_NONE;
- start_debugger_thread ();
+ if (!agent_config.defer)
+ start_debugger_thread ();
}
if (event == EVENT_KIND_VM_DEATH) {
vm_death_event_sent = TRUE;
-
suspend_policy = SUSPEND_POLICY_NONE;
}
DEBUG (2, fprintf (log_file, "Sending command %s failed.\n", event_to_string (event)));
return;
}
-
- if (event == EVENT_KIND_VM_START)
+
+ if (event == EVENT_KIND_VM_START) {
vm_start_event_sent = TRUE;
-
+ if (agent_config.defer)
+ mono_debugger_agent_on_attach ();
+ }
+
DEBUG (1, fprintf (log_file, "[%p] Sent event %s, suspend=%d.\n", (gpointer)GetCurrentThreadId (), event_to_string (event), suspend_policy));
runtime_initialized (MonoProfiler *prof)
{
process_profiler_event (EVENT_KIND_VM_START, mono_thread_current ());
-}
+ if (agent_config.defer)
+ start_debugger_thread ();
+}
static void
runtime_shutdown (MonoProfiler *prof)
static void
appdomain_unload (MonoProfiler *prof, MonoDomain *domain)
{
+ clear_breakpoints_for_domain (domain);
+
+ mono_loader_lock ();
/* Invalidate each thread's frame stack */
mono_g_hash_table_foreach (thread_to_tls, invalidate_each_thread, NULL);
- clear_breakpoints_for_domain (domain);
+ mono_loader_unlock ();
+
process_profiler_event (EVENT_KIND_APPDOMAIN_UNLOAD, domain);
}
send_type_load (MonoClass *klass)
{
gboolean type_load = FALSE;
+ MonoDomain *domain = mono_domain_get ();
+ AgentDomainInfo *info = NULL;
mono_loader_lock ();
- if (!g_hash_table_lookup (loaded_classes, klass)) {
+ mono_domain_lock (domain);
+
+ info = get_agent_domain_info (domain);
+
+ if (!g_hash_table_lookup (info->loaded_classes, klass)) {
type_load = TRUE;
- g_hash_table_insert (loaded_classes, klass, klass);
+ g_hash_table_insert (info->loaded_classes, klass, klass);
}
+
+ mono_domain_unlock (domain);
mono_loader_unlock ();
if (type_load)
- process_profiler_event (EVENT_KIND_TYPE_LOAD, klass);
+ emit_type_load (klass, klass, NULL);
}
static void
}
mono_loader_unlock ();
- if (assembly)
+ if (assembly) {
process_profiler_event (EVENT_KIND_ASSEMBLY_LOAD, assembly);
- else
+ } else {
break;
- }
-
- if (!vm_start_event_sent) {
- /* Save these so they can be sent after the vm start event */
- mono_loader_lock ();
- g_ptr_array_add (pending_type_loads, method->klass);
- mono_loader_unlock ();
- } else {
- /* Send all pending type load events */
- MonoClass *klass;
- while (TRUE) {
- klass = NULL;
- mono_loader_lock ();
- if (pending_type_loads->len > 0) {
- klass = g_ptr_array_index (pending_type_loads, 0);
- g_ptr_array_remove_index (pending_type_loads, 0);
- }
- mono_loader_unlock ();
- if (klass)
- send_type_load (klass);
- else
- break;
}
-
- send_type_load (method->klass);
}
+ send_type_load (method->klass);
+
if (!result)
add_pending_breakpoints (method, jinfo);
}
}
if (!ss_req->bps) {
- DEBUG (1, printf ("[dbg] Turning on global single stepping.\n"));
+ DEBUG (1, fprintf (log_file, "[dbg] Turning on global single stepping.\n"));
ss_req->global = TRUE;
start_single_stepping ();
} else {
// FIXME: Multiple requests
if (ss_req) {
- DEBUG (0, printf ("Received a single step request while the previous one was still active.\n"));
+ DEBUG (0, fprintf (log_file, "Received a single step request while the previous one was still active.\n"));
return ERR_NOT_IMPLEMENTED;
}
- DEBUG (1, printf ("[dbg] Starting single step of thread %p (depth=%s).\n", thread, ss_depth_to_string (depth)));
+ DEBUG (1, fprintf (log_file, "[dbg] Starting single step of thread %p (depth=%s).\n", thread, ss_depth_to_string (depth)));
ss_req = g_new0 (SingleStepReq, 1);
ss_req->req = req;
static void
clear_types_for_assembly (MonoAssembly *assembly)
{
+ MonoDomain *domain = mono_domain_get ();
+ AgentDomainInfo *info = NULL;
+
mono_loader_lock ();
- g_hash_table_foreach_remove (loaded_classes, type_comes_from_assembly, assembly);
+ info = get_agent_domain_info (domain);
+ g_hash_table_foreach_remove (info->loaded_classes, type_comes_from_assembly, assembly);
mono_loader_unlock ();
}
* Invoke this method directly, currently only Environment.Exit () is supported.
*/
this = NULL;
- DEBUG (1, printf ("[%p] Invoking method '%s' on receiver '%s'.\n", (gpointer)GetCurrentThreadId (), mono_method_full_name (invoke->method, TRUE), this ? this->vtable->klass->name : "<null>"));
+ DEBUG (1, fprintf (log_file, "[%p] Invoking method '%s' on receiver '%s'.\n", (gpointer)GetCurrentThreadId (), mono_method_full_name (invoke->method, TRUE), this ? this->vtable->klass->name : "<null>"));
mono_runtime_invoke (invoke->method, NULL, invoke->args, &exc);
g_assert_not_reached ();
}
else
this = NULL;
- DEBUG (1, printf ("[%p] Invoking method '%s' on receiver '%s'.\n", (gpointer)GetCurrentThreadId (), mono_method_full_name (m, TRUE), this ? this->vtable->klass->name : "<null>"));
+ DEBUG (1, fprintf (log_file, "[%p] Invoking method '%s' on receiver '%s'.\n", (gpointer)GetCurrentThreadId (), mono_method_full_name (m, TRUE), this ? this->vtable->klass->name : "<null>"));
if (this && this->vtable->domain != domain)
NOT_IMPLEMENTED;
} else if (MONO_TYPE_IS_REFERENCE (sig->ret)) {
buffer_add_value (buf, sig->ret, &res, domain);
} else if (mono_class_from_mono_type (sig->ret)->valuetype) {
- g_assert (res);
- buffer_add_value (buf, sig->ret, mono_object_unbox (res), domain);
+ if (mono_class_is_nullable (mono_class_from_mono_type (sig->ret))) {
+ if (!res)
+ buffer_add_value (buf, &mono_defaults.object_class->byval_arg, &res, domain);
+ else
+ buffer_add_value (buf, sig->ret, mono_object_unbox (res), domain);
+ } else {
+ g_assert (res);
+ buffer_add_value (buf, sig->ret, mono_object_unbox (res), domain);
+ }
} else {
NOT_IMPLEMENTED;
}
tls->resume_count -= invoke->suspend_count;
}
- DEBUG (1, printf ("[%p] Invoke finished, resume_count = %d.\n", (gpointer)GetCurrentThreadId (), tls->resume_count));
+ DEBUG (1, fprintf (log_file, "[%p] Invoke finished, resume_count = %d.\n", (gpointer)GetCurrentThreadId (), tls->resume_count));
/*
* Take the loader lock to avoid race conditions with CMD_VM_ABORT_INVOKE:
}
mono_loader_unlock ();
- // FIXME: Count resumes
- resume_vm ();
+ while (suspend_count > 0)
+ resume_vm ();
disconnected = TRUE;
+ vm_start_event_sent = FALSE;
break;
case CMD_VM_EXIT: {
MonoInternalThread *thread;
wait_for_suspend ();
env_class = mono_class_from_name (mono_defaults.corlib, "System", "Environment");
- g_assert (env_class);
- exit_method = mono_class_get_method_from_name (env_class, "Exit", 1);
- g_assert (exit_method);
+ if (env_class)
+ exit_method = mono_class_get_method_from_name (env_class, "Exit", 1);
mono_loader_lock ();
thread = mono_g_hash_table_find (tid_to_thread, is_really_suspended, NULL);
mono_loader_unlock ();
- if (thread) {
+ if (thread && exit_method) {
mono_loader_lock ();
tls = mono_g_hash_table_lookup (thread_to_tls, thread);
mono_loader_unlock ();
return NULL;
}
+static gboolean
+wait_for_attach (void)
+{
+ if (listen_fd == -1) {
+ DEBUG (1, fprintf (log_file, "[dbg] Invalid listening socket\n"));
+ return FALSE;
+ }
+
+ /* Block and wait for client connection */
+ conn_fd = transport_accept (listen_fd);
+ DEBUG (1, fprintf (log_file, "Accepted connection on %d\n", conn_fd));
+ if (conn_fd == -1) {
+ DEBUG (1, fprintf (log_file, "[dbg] Bad client connection\n"));
+ return FALSE;
+ }
+
+ /* Handshake */
+ disconnected = !transport_handshake ();
+ if (disconnected) {
+ DEBUG (1, fprintf (log_file, "Transport handshake failed!\n"));
+ return FALSE;
+ }
+
+ return TRUE;
+}
+
/*
* debugger_thread:
*
Buffer buf;
ErrorCode err;
gboolean no_reply;
+ gboolean attach_failed = FALSE;
DEBUG (1, fprintf (log_file, "[dbg] Agent thread started, pid=%p\n", (gpointer)GetCurrentThreadId ()));
mono_thread_internal_current ()->flags |= MONO_THREAD_FLAG_DONT_MANAGE;
mono_set_is_debugger_attached (TRUE);
-
- while (TRUE) {
+
+ if (agent_config.defer) {
+ if (!wait_for_attach ()) {
+ DEBUG (1, fprintf (log_file, "[dbg] Can't attach, aborting debugger thread.\n"));
+ attach_failed = TRUE; // Don't abort process when we can't listen
+ } else {
+ /* Send start event to client */
+ process_profiler_event (EVENT_KIND_VM_START, mono_thread_get_main ());
+ }
+ }
+
+ while (!attach_failed) {
res = recv_length (conn_fd, header, HEADER_LENGTH, 0);
/* This will break if the socket is closed during shutdown too */
}
mono_set_is_debugger_attached (FALSE);
-
+
mono_mutex_lock (&debugger_thread_exited_mutex);
debugger_thread_exited = TRUE;
mono_cond_signal (&debugger_thread_exited_cond);
mono_mutex_unlock (&debugger_thread_exited_mutex);
- DEBUG (1, printf ("[dbg] Debugger thread exited.\n"));
-
+ DEBUG (1, fprintf (log_file, "[dbg] Debugger thread exited.\n"));
+
+ if (!attach_failed && command_set == CMD_SET_VM && command == CMD_VM_DISPOSE && !(vm_death_event_sent || mono_runtime_is_shutting_down ())) {
+ DEBUG (2, fprintf (log_file, "[dbg] Detached - restarting clean debugger thread.\n"));
+ start_debugger_thread ();
+ }
+
return 0;
}