//
// CustomSoftDebuggerEngine.cs
//
// Author:
// Michael Hutchinson <mhutchinson@novell.com>
//
// Copyright (c) 2010 Novell, Inc.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.
using System;
using System.Diagnostics;
using Mono.Debugging.Client;
using MonoDevelop.Core;
using MonoDevelop.Core.Execution;
using System.Net;
using System.Collections.Generic;
using Mono.Debugging.Soft;
namespace MonoDevelop.Debugger.Soft
{
class CustomSoftDebuggerEngine: IDebuggerEngine
{
bool? available;
public bool CanDebugCommand (ExecutionCommand cmd)
{
// This isn't polished enough to show it by default. GUI needs work, and it should be exposed
// via "run with->custom parameters", not a toplevel command and dialog.
// That would also make it possible to save settings.
if (!available.HasValue) {
available = !string.IsNullOrEmpty (Environment.GetEnvironmentVariable ("MONODEVELOP_SDB_TEST"));
}
return available.Value;
}
public DebuggerSession CreateSession ()
{
return new CustomSoftDebuggerSession ();
}
public ProcessInfo[] GetAttachableProcesses ()
{
return new ProcessInfo[0];
}
public DebuggerStartInfo CreateDebuggerStartInfo (ExecutionCommand c)
{
//WORKAROUND: explicit generic type argument works around a gmcs 2.6.x type inference bug
return InvokeSynch<SoftDebuggerStartInfo> (GetDebuggerInfo) ??
//HACK: flag object so we can cancel the session
new DebuggerStartInfo ();
}
static SoftDebuggerStartInfo GetDebuggerInfo ()
{
var dlg = new DebuggerOptionsDialog ();
try {
return dlg.Run ();
} finally {
dlg.Destroy ();
}
}
static T InvokeSynch<T> (Func<T> func)
{
if (MonoDevelop.Ide.DispatchService.IsGuiThread)
return func ();
var ev = new System.Threading.ManualResetEvent (false);
T val = default (T);
Exception caught = null;
Gtk.Application.Invoke (delegate {
try {
val = func ();
} catch (Exception ex) {
caught = ex;
} finally {
ev.Set ();
}
});
ev.WaitOne ();
if (caught != null)
throw caught;
return val;
}
class CustomSoftDebuggerSession : SoftDebuggerSession
{
IProcessAsyncOperation process;
bool usingExternalConsole;
protected override void OnRun (DebuggerStartInfo startInfo)
{
var info = startInfo as SoftDebuggerStartInfo;
if (info == null) {
EndSession ();
return;
}
StartProcess (info);
if (info.StartArgs is SoftDebuggerConnectArgs) {
//connecting to the process, so give it a moment to start listening
System.Threading.Thread.Sleep (200);
}
base.OnRun (startInfo);
}
void StartProcess (SoftDebuggerStartInfo info)
{
if (string.IsNullOrEmpty (info.Command))
return;
if (info.UseExternalConsole) {
usingExternalConsole = true;
var console = ExternalConsoleFactory.Instance.CreateConsole (info.CloseExternalConsoleOnExit);
process = Runtime.ProcessService.StartConsoleProcess (
info.Command, info.Arguments, info.WorkingDirectory, info.EnvironmentVariables,
console, null);
} else {
var psi = new ProcessStartInfo (info.Command, info.Arguments) {
WorkingDirectory = info.WorkingDirectory,
};
foreach (KeyValuePair<string,string> kvp in info.EnvironmentVariables)
psi.EnvironmentVariables [kvp.Key] = kvp.Value;
process = Runtime.ProcessService.StartProcess (psi, ProcessOutput, ProcessError, null);
}
}
void ProcessOutput (object sender, string message)
{
OnTargetOutput (true, message);
}
void ProcessError (object sender, string message)
{
OnTargetOutput (false, message);
}
protected override void EndSession ()
{
base.EndSession ();
EndProcess ();
}
void EndProcess ()
{
if (process == null)
return;
var p = process;
process = null;
if (usingExternalConsole || p.IsCompleted)
return;
try {
p.Cancel ();
} catch {}
}
protected override void EnsureExited ()
{
EndProcess ();
}
}
class DebuggerOptionsDialog : Gtk.Dialog
{
MonoDevelop.Components.FileEntry commandEntry = new MonoDevelop.Components.FileEntry ();
Gtk.Entry argsEntry = new Gtk.Entry ();
Gtk.Entry ipEntry = new Gtk.Entry ();
Gtk.Entry portEntry = new Gtk.Entry ();
Gtk.Entry consolePortEntry = new Gtk.Entry ();
Gtk.Button listenButton = new Gtk.Button ("Listen");
Gtk.Button connectButton = new Gtk.Button ("Connect");
const Gtk.ResponseType listenResponse = Gtk.ResponseType.Yes;
const Gtk.ResponseType connectResponse = Gtk.ResponseType.Ok;
IPAddress ip;
string command, args;
int? port, consolePort;
Properties properties;
//TODO: dropdown menus for picking string substitutions. also substitutions for port, ip
public DebuggerOptionsDialog () : base (
"Launch Soft Debugger", MonoDevelop.Ide.MessageService.RootWindow,
Gtk.DialogFlags.DestroyWithParent | Gtk.DialogFlags.Modal)
{
properties = PropertyService.Get ("MonoDevelop.Debugger.Soft.CustomSoftDebugger", new Properties());
AddActionWidget (new Gtk.Button (Gtk.Stock.Cancel), Gtk.ResponseType.Cancel);
AddActionWidget (listenButton, listenResponse);
AddActionWidget (connectButton, connectResponse);
var table = new Gtk.Table (5, 2, false);
table.BorderWidth = 6;
VBox.PackStart (table, true, true, 0);
table.Attach (new Gtk.Label ("Command:") { Xalign = 0 }, 0, 1, 0, 1);
table.Attach (new Gtk.Label ("Arguments:") { Xalign = 0 }, 0, 1, 1, 2);
table.Attach (new Gtk.Label ("IP:") { Xalign = 0 }, 0, 1, 2, 3);
table.Attach (new Gtk.Label ("Port:") { Xalign = 0 }, 0, 1, 3, 4);
table.Attach (new Gtk.Label ("Output:") { Xalign = 0 }, 0, 1, 4, 5);
table.Attach (commandEntry, 1, 2, 0, 1);
table.Attach (argsEntry, 1, 2, 1, 2);
table.Attach (ipEntry, 1, 2, 2, 3);
table.Attach (portEntry, 1, 2, 3, 4);
table.Attach (consolePortEntry, 1, 2, 4, 5);
argsEntry.WidthRequest = 500;
commandEntry.PathChanged += delegate {
try {
//check it parses
MonoDevelop.Core.StringParserService.Parse (commandEntry.Path);
command = commandEntry.Path;
} catch {
command = null;
}
CheckValid ();
};
argsEntry.Changed += delegate {
try {
//check it parses
MonoDevelop.Core.StringParserService.Parse (argsEntry.Text);
args = argsEntry.Text;
} catch {
args = null;
}
CheckValid ();
};
ipEntry.Changed += delegate {
if (string.IsNullOrEmpty (ipEntry.Text)) {
ip = IPAddress.Loopback;
} else if (!IPAddress.TryParse (ipEntry.Text, out ip)) {
ip = null;
}
CheckValid ();
};
portEntry.Changed += delegate {
port = ParsePort (portEntry.Text);
CheckValid ();
};
consolePortEntry.Changed += delegate {
consolePort = ParsePort (consolePortEntry.Text);
CheckValid ();
};
command = properties.Get ("Command", "");
args = properties.Get ("Arguments", "");
if (!IPAddress.TryParse (properties.Get ("IpAddress", "127.0.0.1"), out ip) || ip == null)
ip = IPAddress.Loopback;
string portStr = properties.Get<string> ("Port");
port = ParsePort (portStr) ?? 10000;
string consolePortStr = properties.Get<string> ("ConsolePort");
consolePort = ParsePort (consolePortStr);
commandEntry.Path = command;
argsEntry.Text = args;
ipEntry.Text = ip.ToString ();
portEntry.Text = PortToString (port) ?? "";
consolePortEntry.Text = PortToString (consolePort) ?? "";
CheckValid ();
VBox.ShowAll ();
}
int? ParsePort (string port)
{
if (string.IsNullOrEmpty (port))
return null;
int value;
if (!int.TryParse (port, out value))
return -1;
return value;
}
string PortToString (int? port)
{
return port.HasValue? port.Value.ToString () : null;
}
public new SoftDebuggerStartInfo Run ()
{
var response = (Gtk.ResponseType) MonoDevelop.Ide.MessageService.RunCustomDialog (this);
if (response != listenResponse && response != connectResponse)
return null;
properties.Set ("Command", command);
properties.Set ("Arguments", args);
properties.Set ("IpAddress", ip.ToString ());
properties.Set ("Port", PortToString (port));
properties.Set ("ConsolePort", PortToString (consolePort));
var name = string.IsNullOrEmpty (command)? "" : command;
bool listen = response == listenResponse;
var agentArgs = string.Format ("transport=dt_socket,address={0}:{1}{2}", ip, port, listen?"":",server=y");
var customArgsTags = new string[,] {
{ "AgentArgs", agentArgs },
{ "IP", ip.ToString () },
{ "Port", PortToString (port) },
{ "Console", PortToString (consolePort) },
};
SoftDebuggerRemoteArgs startArgs;
if (listen) {
startArgs = (SoftDebuggerRemoteArgs) new SoftDebuggerListenArgs (name, ip, port.Value, consolePort ?? -1);
} else {
startArgs = new SoftDebuggerConnectArgs (name, ip, port.Value, consolePort ?? -1) {
//infinite connection retries (user can cancel), 800ms between them
TimeBetweenConnectionAttempts = 800,
MaxConnectionAttempts = -1,
};
};
var dsi = new SoftDebuggerStartInfo (startArgs) {
Command = StringParserService.Parse (command),
Arguments = StringParserService.Parse (args, customArgsTags),
};
//FIXME: GUI for env vars
//dsi.EnvironmentVariables [kvp.Key] = kvp.Value;
return dsi;
}
void CheckValid ()
{
bool valid = ip != null
&& (port.HasValue && port.Value >= 0)
&& (!consolePort.HasValue || consolePort >= 0);
listenButton.Sensitive = valid;
connectButton.Sensitive = valid && port > 0 && (!consolePort.HasValue || consolePort > 0);
}
}
}
}