[Pkg-mono-svn-commits] [sdb] 01/02: Imported Upstream version 1.2

Jo Shields directhex at moszumanska.debian.org
Fri Feb 7 12:52:32 UTC 2014


This is an automated email from the git hooks/post-receive script.

directhex pushed a commit to annotated tag debian/1.2-1
in repository sdb.

commit 042a7e8093aadd62544cb2f22c4cf906300e4cac
Author: Jo Shields <directhex at apebox.org>
Date:   Fri Feb 7 11:31:10 2014 +0000

    Imported Upstream version 1.2
---
 .gitignore                         |   7 +
 .gitmodules                        |   9 +
 LICENSE                            |  21 ++
 Makefile                           | 224 ++++++++++++
 README.md                          | 301 ++++++++++++++++
 chk/check.fs                       | 221 ++++++++++++
 chk/cs/cwl.cs                      |   9 +
 chk/cs/throw.cs                    |  15 +
 chk/fs/print.fs                    |   4 +
 mono.snk                           | Bin 0 -> 596 bytes
 sdb.exe.config                     |   4 +
 sdb.in                             |   2 +
 src/AssemblyInfo.cs                |  30 ++
 src/Color.cs                       | 117 +++++++
 src/Command.cs                     |  42 +++
 src/CommandAttribute.cs            |  33 ++
 src/CommandLine.cs                 | 318 +++++++++++++++++
 src/Commands/ArgumentsCommand.cs   |  74 ++++
 src/Commands/AttachCommand.cs      |  61 ++++
 src/Commands/BacktraceCommand.cs   |  83 +++++
 src/Commands/BreakpointCommand.cs  | 531 ++++++++++++++++++++++++++++
 src/Commands/CatchpointCommand.cs  | 218 ++++++++++++
 src/Commands/ConfigCommand.cs      | 278 +++++++++++++++
 src/Commands/ConnectCommand.cs     | 104 ++++++
 src/Commands/ContinueCommand.cs    |  63 ++++
 src/Commands/DatabaseCommand.cs    | 185 ++++++++++
 src/Commands/DecompileCommand.cs   |  62 ++++
 src/Commands/DirectoryCommand.cs   |  73 ++++
 src/Commands/DisassembleCommand.cs | 114 ++++++
 src/Commands/EnvironmentCommand.cs | 305 ++++++++++++++++
 src/Commands/FrameCommand.cs       | 439 +++++++++++++++++++++++
 src/Commands/HelpCommand.cs        | 118 +++++++
 src/Commands/KillCommand.cs        |  65 ++++
 src/Commands/ListenCommand.cs      | 101 ++++++
 src/Commands/PluginCommand.cs      |  66 ++++
 src/Commands/PrintCommand.cs       |  92 +++++
 src/Commands/QuitCommand.cs        |  67 ++++
 src/Commands/ResetCommand.cs       |  72 ++++
 src/Commands/RootCommand.cs        |  99 ++++++
 src/Commands/RunCommand.cs         | 100 ++++++
 src/Commands/SourceCommand.cs      | 160 +++++++++
 src/Commands/StepCommand.cs        | 307 ++++++++++++++++
 src/Commands/ThreadCommand.cs      | 295 ++++++++++++++++
 src/Commands/WatchCommand.cs       | 256 ++++++++++++++
 src/Configuration.cs               | 199 +++++++++++
 src/CustomLogger.cs                |  50 +++
 src/Debugger.cs                    | 702 +++++++++++++++++++++++++++++++++++++
 src/LibEdit.cs                     |  40 +++
 src/Log.cs                         | 105 ++++++
 src/MultiCommand.cs                | 159 +++++++++
 src/Plugins.cs                     | 122 +++++++
 src/Program.cs                     | 110 ++++++
 src/SessionKind.cs                 |  34 ++
 src/State.cs                       |  33 ++
 src/Utilities.cs                   | 131 +++++++
 55 files changed, 7430 insertions(+)

diff --git a/.gitignore b/.gitignore
new file mode 100644
index 0000000..6426033
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,7 @@
+bin/*
+dep/*
+
+*.dll
+*.exe
+*.gz
+*.mdb
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..55310a0
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,9 @@
+[submodule "dep/debugger-libs"]
+	path = dep/debugger-libs
+	url = git://github.com/mono/debugger-libs
+[submodule "dep/cecil"]
+	path = dep/cecil
+	url = git://github.com/mono/cecil.git
+[submodule "dep/nrefactory"]
+	path = dep/nrefactory
+	url = git://github.com/icsharpcode/NRefactory.git
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..fe34fff
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2013 Alex Rønne Petersen
+
+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.
diff --git a/Makefile b/Makefile
new file mode 100644
index 0000000..0f47683
--- /dev/null
+++ b/Makefile
@@ -0,0 +1,224 @@
+#
+# The MIT License (MIT)
+#
+# Copyright (c) 2013 Alex Rønne Petersen
+#
+# 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.
+#
+
+CD ?= cd
+CHMOD ?= chmod
+CP ?= cp
+FSHARPC ?= fsharpc
+GENDARME ?= gendarme
+MCS ?= mcs
+MKDIR ?= mkdir
+PKG_CONFIG ?= pkg-config
+SED ?= sed
+TAR ?= tar
+XBUILD ?= xbuild
+
+export MONO_PREFIX ?= /usr
+MODE ?= Debug
+
+ifeq ($(MODE), Debug)
+	override xb_mode = net_4_0_Debug
+	override mono_opt = --debug
+
+	FSHARPC_FLAGS += --debug+
+	MCS_FLAGS += -debug
+else
+	override xb_mode = net_4_0_Release
+	override mono_opt =
+
+	FSHARPC_FLAGS += --optimize
+	MCS_FLAGS += -optimize
+endif
+
+FSHARPC_FLAGS += --nologo --warnaserror
+GENDARME_FLAGS += --severity all --confidence all
+MCS_FLAGS += -langversion:future -unsafe -warnaserror
+XBUILD_FLAGS += /verbosity:quiet /nologo /property:Configuration=$(xb_mode)
+
+FSHARPC_TEST_FLAGS += --debug+ --nologo --warnaserror
+MCS_TEST_FLAGS += -debug -langversion:future -unsafe -warnaserror
+
+.PHONY: all check clean clean-check clean-deps gendarme
+
+override results = \
+	LICENSE \
+	README \
+	sdb.exe \
+	sdb.exe.config \
+	sdb
+
+all: $(addprefix bin/, $(results))
+
+define TargetType
+$(if $(findstring .exe, $(1)),exe,library)
+endef
+
+override cs_tests = \
+	cwl.exe \
+	throw.exe
+
+define CSharpTestTemplate
+chk/cs/$(1): chk/cs/$(basename $(1)).cs
+	$(MCS) $(MCS_TEST_FLAGS) -out:chk/cs/$(1) -target:$(call TargetType, $(1)) chk/cs/$(basename $(1)).cs
+endef
+
+$(foreach tgt, $(cs_tests), $(eval $(call CSharpTestTemplate,$(tgt))))
+
+override fs_tests = \
+	print.exe
+
+define FSharpTestTemplate
+chk/fs/$(1): chk/fs/$(basename $(1)).fs
+	$(FSHARPC) $(FSHARPC_TEST_FLAGS) --out:chk/fs/$(1) --target:$(call TargetType, $(1)) chk/fs/$(basename $(1)).fs
+endef
+
+$(foreach tgt, $(fs_tests), $(eval $(call FSharpTestTemplate,$(tgt))))
+
+override tests = \
+	$(addprefix chk/cs/, $(cs_tests)) \
+	$(addprefix chk/cs/, $(addsuffix .mdb, $(cs_tests))) \
+	$(addprefix chk/fs/, $(fs_tests)) \
+	$(addprefix chk/fs/, $(addsuffix .mdb, $(fs_tests)))
+
+check: chk/check.exe $(addprefix bin/, $(results)) $(tests)
+	$(CD) chk && MONO_PATH=. $(MONO_PREFIX)/bin/mono check.exe
+
+clean:
+	$(RM) -r bin
+
+clean-check:
+	$(RM) chk/check.exe chk/check.exe.mdb
+	$(RM) $(tests)
+
+clean-deps:
+	$(CD) dep/debugger-libs && $(XBUILD) $(XBUILD_FLAGS) /target:Clean
+
+gendarme: bin/sdb.exe
+	$(GENDARME) $(GENDARME_FLAGS) --log bin/sdb.log $<
+
+override refs = \
+	ICSharpCode.NRefactory.dll \
+	ICSharpCode.NRefactory.CSharp.dll \
+	Mono.Cecil.dll \
+	Mono.Cecil.Mdb.dll \
+	Mono.Debugger.Soft.dll \
+	Mono.Debugging.dll \
+	Mono.Debugging.Soft.dll
+
+$(addprefix bin/, $(refs)):
+	$(CD) dep/debugger-libs && $(XBUILD) $(XBUILD_FLAGS) debugger-libs.sln
+	$(MKDIR) -p bin
+	$(CP) dep/nrefactory/bin/Debug/ICSharpCode.NRefactory.dll \
+		bin/ICSharpCode.NRefactory.dll
+	$(CP) dep/nrefactory/bin/Debug/ICSharpCode.NRefactory.CSharp.dll \
+		bin/ICSharpCode.NRefactory.CSharp.dll
+	$(CP) dep/cecil/bin/$(xb_mode)/Mono.Cecil.dll \
+		bin/Mono.Cecil.dll
+	$(CP) dep/cecil/bin/$(xb_mode)/Mono.Cecil.Mdb.dll \
+		bin/Mono.Cecil.Mdb.dll
+	$(CP) dep/debugger-libs/Mono.Debugger.Soft/bin/Debug/Mono.Debugger.Soft.dll \
+		bin/Mono.Debugger.Soft.dll
+	$(CP) dep/debugger-libs/Mono.Debugging/bin/Debug/Mono.Debugging.dll \
+		bin/Mono.Debugging.dll
+	$(CP) dep/debugger-libs/Mono.Debugging.Soft/bin/Debug/Mono.Debugging.Soft.dll \
+		bin/Mono.Debugging.Soft.dll
+
+dep/Options.cs:
+	$(CP) `$(PKG_CONFIG) --variable=Sources mono-options` $@
+
+dep/getline.cs:
+	$(CP) `$(PKG_CONFIG) --variable=Sources mono-lineeditor` $@
+
+override srcs = \
+	dep/Options.cs \
+	dep/getline.cs \
+	src/Commands/AttachCommand.cs \
+	src/Commands/BacktraceCommand.cs \
+	src/Commands/BreakpointCommand.cs \
+	src/Commands/CatchpointCommand.cs \
+	src/Commands/ConfigCommand.cs \
+	src/Commands/ConnectCommand.cs \
+	src/Commands/ContinueCommand.cs \
+	src/Commands/DatabaseCommand.cs \
+	src/Commands/DecompileCommand.cs \
+	src/Commands/DirectoryCommand.cs \
+	src/Commands/DisassembleCommand.cs \
+	src/Commands/EnvironmentCommand.cs \
+	src/Commands/FrameCommand.cs \
+	src/Commands/HelpCommand.cs \
+	src/Commands/KillCommand.cs \
+	src/Commands/ListenCommand.cs \
+	src/Commands/PluginCommand.cs \
+	src/Commands/PrintCommand.cs \
+	src/Commands/QuitCommand.cs \
+	src/Commands/ResetCommand.cs \
+	src/Commands/RootCommand.cs \
+	src/Commands/RunCommand.cs \
+	src/Commands/SourceCommand.cs \
+	src/Commands/StepCommand.cs \
+	src/Commands/ThreadCommand.cs \
+	src/Commands/WatchCommand.cs \
+	src/AssemblyInfo.cs \
+	src/Color.cs \
+	src/Command.cs \
+	src/CommandAttribute.cs \
+	src/CommandLine.cs \
+	src/Configuration.cs \
+	src/CustomLogger.cs \
+	src/Debugger.cs \
+	src/LibEdit.cs \
+	src/Log.cs \
+	src/MultiCommand.cs \
+	src/Plugins.cs \
+	src/Program.cs \
+	src/SessionKind.cs \
+	src/State.cs \
+	src/Utilities.cs
+
+bin/sdb.exe: $(srcs) $(addprefix bin/, $(refs)) mono.snk
+	$(MCS) $(MCS_FLAGS) -keyfile:mono.snk -lib:bin -out:bin/sdb.exe -target:exe -r:Mono.Posix $(addprefix -r:, $(refs)) $(srcs)
+
+bin/sdb.exe.config: sdb.exe.config
+	$(MKDIR) -p bin
+	$(CP) $< $@
+
+bin/sdb: sdb.in
+	$(MKDIR) -p bin
+	$(SED) s/__MONO_OPTIONS__/$(mono_opt)/ $< > $@
+	$(CHMOD) +x $@
+
+bin/LICENSE: LICENSE
+	$(MKDIR) -p bin
+	$(CP) $< $@
+
+bin/README: README.md
+	$(MKDIR) -p bin
+	$(CP) $< $@
+
+chk/check.exe: chk/check.fs mono.snk
+	$(FSHARPC) $(FSHARPC_FLAGS) --keyfile:mono.snk --out:$@ --target:exe chk/check.fs
+
+sdb.tar.gz: $(addprefix bin/, $(results))
+	$(RM) sdb.tar.gz
+	$(CD) bin && $(TAR) -zcf ../sdb.tar.gz $(results) $(refs)
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..742d882
--- /dev/null
+++ b/README.md
@@ -0,0 +1,301 @@
+# SDB: Mono Soft Debugger Client
+
+SDB is a command line client for Mono's soft debugger, a cooperative debugger
+that is part of the Mono VM. It tries to be similar in command syntax to tools
+such as GDB and LLDB.
+
+## Building
+
+Building and using SDB requires a basic POSIX-like environment, the `libedit`
+library, and an installed Mono framework.
+
+First, clone the submodules:
+
+	$ git submodule update --init --recursive
+
+To build, run:
+
+    $ make
+
+This compiles SDB and its dependencies, and puts everything in the `bin`
+directory. This directory can be moved around freely; when executed, the `sdb`
+script will set up the environment so all the dependency assemblies are found.
+
+You could, for example, just add the `bin` directory to your `PATH`:
+
+    $ export PATH=`pwd`/bin:$PATH
+    $ sdb
+    Welcome to the Mono soft debugger (sdb 1.0.5058.39468)
+    Type 'help' for a list of commands or 'quit' to exit
+
+    (sdb)
+
+You can run the SDB test suite with:
+
+    $ make check
+
+It's generally a good idea to do that to ensure that SDB works correctly on
+your system before you start using it.
+
+The following variables can be set in your environment or on the Make command
+line to affect the build:
+
+* `CD`: Path to the `cd` POSIX utility.
+* `CHMOD`: Path to the `chmod` POSIX utility.
+* `CP`: Path to the `cp` POSIX utility.
+* `FSHARPC`: Which F# compiler executable to use.
+* `FSHARPC_FLAGS`: Flags to pass to the F# compiler.
+* `FSHARPC_TEST_FLAGS`: Flags to pass to the F# compiler for tests.
+* `GENDARME`: Which Gendarme executable to use (optional).
+* `GENDARME_FLAGS`: Flags to pass to Gendarme.
+* `MCS`: Which C# compiler executable to use.
+* `MCS_FLAGS`: Flags to pass to the C# compiler.
+* `MCS_TEST_FLAGS`: Flags to pass to the C# compiler for tests.
+* `MKDIR`: Path to the `mkdir` POSIX utility.
+* `PKG_CONFIG`: Path to the `pkg-config` utility.
+* `SED`: Path to the `sed` POSIX utility.
+* `TAR`: Path to the `tar` POSIX utility.
+* `XBUILD`: Which XBuild executable to use.
+* `XBUILD_FLAGS`: Flags to pass to XBuild.
+
+Note that the F# tools are only necessary to run the test suite. Gendarme is
+also optional and is mostly used by the SDB developers. `tar` is also only used
+to package SDB releases.
+
+Additionally, `MODE` can be set to `Debug` (default) or `Release` to indicate
+the kind of build desired.
+
+Finally, `MONO_PREFIX` can be set to tell the test runner which Mono executable
+should be used. See the description of `RuntimePrefix` further down for more
+information.
+
+## Usage
+
+Running a program is simple:
+
+    $ cat test.cs
+    using System;
+    using System.Diagnostics;
+
+    static class Program
+    {
+        static void Main()
+        {
+            var str = "Foo!";
+
+            Foo(str);
+        }
+
+        static void Foo(string str)
+        {
+            Console.WriteLine(str);
+
+            Bar();
+        }
+
+        static void Bar()
+        {
+            Debugger.Break();
+        }
+    }
+    $ mcs -debug test.cs
+    $ sdb
+    Welcome to the Mono soft debugger (sdb 1.0.5060.15368)
+    Type 'help' for a list of commands or 'quit' to exit
+
+    (sdb) r test.exe
+    Inferior process '5234' ('test.exe') started
+    Foo!
+    Inferior process '5234' ('test.exe') suspended
+    #0 [0x00000001] Program.Bar at /home/alexrp/Projects/tests/cs/test.cs:22
+            Debugger.Break();
+
+A stack trace can be generated with `bt`:
+
+    (sdb) bt
+    #0 [0x00000001] Program.Bar at /home/alexrp/Projects/tests/cs/test.cs:22
+            Debugger.Break();
+    #1 [0x00000007] Program.Foo at /home/alexrp/Projects/tests/cs/test.cs:17
+            Bar();
+    #2 [0x00000008] Program.Main at /home/alexrp/Projects/tests/cs/test.cs:10
+            Foo(str);
+
+We can select a frame and inspect locals:
+
+    (sdb) f up
+    #1 [0x00000007] Program.Foo at /home/alexrp/Projects/tests/cs/test.cs:17
+            Bar();
+    (sdb) p str
+    string it = "Foo!"
+
+Or globals:
+
+    (sdb) p Environment.CommandLine
+    string it = "/home/alexrp/Projects/tests/cs/test.exe"
+
+To continue execution, do:
+
+    (sdb) c
+    Inferior process '5234' ('test.exe') resumed
+    Inferior process '5234' ('test.exe') exited
+    (sdb)
+
+We can then exit SDB:
+
+    (sdb) q
+    Bye
+
+For more commands, consult `help` in SDB.
+
+## Options
+
+SDB has a few command line options that are useful for automation. For the full
+list, issue `sdb --help`.
+
+First of all, all non-option arguments passed to SDB are treated as commands
+that SDB will execute at startup. For instance:
+
+    $ sdb "run test.exe"
+
+Or:
+
+    $ sdb "args --foo --bar baz" "run test.exe"
+
+This starts SDB and immediately executes `test.exe` with the given arguments.
+
+The first option is `-f`. This option specifies files that SDB should read
+commands from. These commands are executed before any commands specified as
+non-option arguments. This option is useful for longer command sequences that
+are easier to maintain in a separate file. Example:
+
+    $ cat cmds.txt
+    args --foo --bar baz
+    run test.exe
+    $ sdb -f cmds.txt
+
+The second option is `-b`. This runs SDB in batch mode; that is, it will exit
+as soon as all commands have finished and no inferior process is running. This
+goes well with `-f` for running programs regularly under SDB.
+
+## Settings
+
+One configuration element that you almost certainly need to alter is the
+`RuntimePrefix` string value. It is set to `/usr` by default, regardless of
+OS, which is probably not desirable everywhere. For example, on Windows, you
+will want to set it to something like `C:\Program Files (x86)\Mono-3.0.10`.
+Or if you have Mono in some other directory, you might set it to e.g.
+`/opt/mono`.
+
+You may want to set `DisableColors` to `true` if you don't want the fancy ANSI
+color codes that SDB emits.
+
+Finally, three useful settings for debugging SDB itself exist: `DebugLogging`
+can be set to `true` to make SDB spew a bunch of diagnostic information.
+`LogInternalErrors` can be set to `true` to log any internal errors that are
+encountered in the Mono debugging libraries. `LogRuntimeSpew` can be set to
+`true` to log all messages from the Mono VM.
+
+## Paths
+
+When configuration elements are changed with `config set`, SDB will store the
+configuration data in `~/.sdb.cfg`. The content of the file is the .NET binary
+serialization of the `Mono.Debugger.Client.Configuration` class. This file is
+read on startup if it exists.
+
+At startup, SDB will scan the `~/.sdb` directory for plugin assemblies. It will
+attempt to load all command and type formatter definitions.
+
+Finally, SDB will read `~/.sdb.rc` and execute any commands (one per line) from
+it. This is useful if you prefer to change your settings with commands that you
+write down manually, rather than storing the data in a binary file.
+
+## Environment
+
+The `SDB_COLORS` variable can be set to `disable` to tell SDB to not use colors
+in output. Normally, SDB will not use colors if it detects that `stdout` has
+been redirected, that `TERM` is set to `dumb` (or not set at all), or if the
+`DisableColors` configuration element is `true`.
+
+`SDB_CFG` can be set to a specific configuration file to use instead of the
+defaults `~/.sdb.cfg`. If set to the empty string (i.e. `SDB_CFG="" sdb`), SDB
+will not load any configuration file at all, and changed configuration values
+will not be saved.
+
+The `SDB_PATH` variable can be set to a list of additional directories that SDB
+will scan for plugin assemblies in. Each directory should be separated by a
+semicolon (Windows) or a colon (POSIX).
+
+`SDB_DEBUG` can be set to `enable` to make SDB print diagnostic information
+while debugging. This may be useful to debug SDB itself.
+
+## Plugins
+
+At the moment, SDB has one extension point which is the `Command` class and the
+associated `CommandAttribute` class. A class implementing `Command` that is
+tagged with `CommandAttribute` will be instantiated at startup time and put
+into the root command list.
+
+For SDB to find custom commands, they should be compiled into `.dll` assemblies
+and put in `~/.sdb` (or some other directory specified in `SDB_PATH`).
+
+Here's an example of compiling and using a test plugin:
+
+    $ cat test.cs
+    using Mono.Debugger.Client;
+
+    [Command]
+    public sealed class MyCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "mycmd" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Performs magic."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "mycmd"; }
+        }
+
+        public override string Help
+        {
+            get { return "Some sort of detailed help text goes here."; }
+        }
+
+        public override void Process(string args)
+        {
+            Log.Info("Hello! I received: {0}", args);
+        }
+    }
+    $ mcs -debug -t:library test.cs -r:`which sdb`.exe -out:$HOME/.sdb/test.dll
+    $ sdb
+    Welcome to the Mono soft debugger (sdb 1.0.5061.14716)
+    Type 'help' for a list of commands or 'quit' to exit
+
+    (sdb) h mycmd
+
+      mycmd
+
+    Some sort of detailed help text goes here.
+
+    (sdb) mycmd foo bar baz
+    Hello! I received: foo bar baz
+
+## Issues
+
+* There is no completion for commands - the default completion instead tries to
+  complete file names which is not very useful most of the time.
+* Decompilation is not implemented. The `ICSharpCode.Decompiler` library needs
+  to be separated from ILSpy for this to be practical.
+* The exit code of inferior processes is not shown. There is apparently no way
+  to get it from `Mono.Debugging`.
+* Attach support is not implemented. This requires special support in the
+  debugging libraries.
+* Some Mono versions throw a `NullReferenceException` when SDB shuts down. This
+  is because of a bug in the finalizer of `System.Timers.Timer` in Mono. This
+  bug has been fixed and should be available in whatever Mono version comes
+  after 3.2.5.
diff --git a/chk/check.fs b/chk/check.fs
new file mode 100644
index 0000000..a31008c
--- /dev/null
+++ b/chk/check.fs
@@ -0,0 +1,221 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.
+//
+
+open System
+open System.Diagnostics
+open System.IO
+open System.Text.RegularExpressions
+
+(* ------------------------------------------------------------------- *)
+(* Initialization Logic                                                *)
+(* ------------------------------------------------------------------- *)
+
+let mutable successes = 0
+let mutable failures = 0
+
+let mutable testName = ""
+let mutable testArgs = ""
+let mutable testLogic = fun p -> ()
+
+let recv (proc : Process) =
+    let s = proc.StandardOutput.ReadLine()
+
+    Console.WriteLine(s)
+
+    s
+
+let recvErr (proc : Process) =
+    let s = proc.StandardError.ReadLine()
+
+    Console.Error.WriteLine(s)
+
+    s
+
+let send (proc : Process) (str : string) =
+    proc.StandardInput.WriteLine(str)
+
+let runTest () =
+    let delim = String.replicate 30 "-"
+
+    Console.WriteLine("{0} {1} {0}", delim, testName)
+    Console.WriteLine()
+
+    let psi = ProcessStartInfo(Path.Combine("..", "bin", "sdb"),
+                               Arguments = testArgs,
+                               CreateNoWindow = true,
+                               RedirectStandardError = true,
+                               RedirectStandardInput = true,
+                               RedirectStandardOutput = true,
+                               UseShellExecute = false)
+
+    psi.EnvironmentVariables.Add("SDB_CFG", "")
+    psi.EnvironmentVariables.Add("SDB_COLORS", "disable")
+    psi.EnvironmentVariables.Add("SDB_DEBUG", "disable")
+
+    use proc = Process.Start(psi)
+
+    // Read the logo lines.
+    for _ in 1 .. 3 do
+        recv proc |> ignore
+
+    // Set the `RuntimePrefix` to `MONO_PREFIX` as
+    // specified in the `Makefile`.
+    let prefix = Environment.GetEnvironmentVariable("MONO_PREFIX")
+
+    send proc ("config set RuntimePrefix " + prefix)
+    recv proc |> ignore
+
+    let mutable ex = null
+
+    try
+        testLogic proc
+    with
+    | e ->
+        try
+            proc.Kill()
+        with
+        | :? InvalidOperationException -> ()
+
+        ex <- e
+
+    proc.WaitForExit()
+
+    let sep = String.Format("{0}-{1}-{0}", delim, String.replicate testName.Length "-")
+
+    Console.WriteLine(sep)
+    Console.Write("Result: ")
+
+    if proc.ExitCode = 0 && ex = null then
+        successes <- successes + 1
+
+        Console.Write("Success")
+    else
+        failures <- failures + 1
+
+        Console.Write("Failure")
+
+    Console.WriteLine(" ({0})", proc.ExitCode)
+
+    if ex <> null then
+        Console.WriteLine()
+        Console.WriteLine(ex)
+
+    Console.WriteLine(sep)
+    Console.WriteLine()
+
+(* ------------------------------------------------------------------- *)
+(* Assertion Functions                                                 *)
+(* ------------------------------------------------------------------- *)
+
+let assertRecv (proc : Process) (pat : string) =
+    let txt = recv proc
+
+    if not(Regex.IsMatch(txt, pat)) then
+        failwith(String.Format("stdout string '{0}' did not match pattern '{1}'", txt, pat))
+
+let assertRecvErr (proc : Process) (pat : string) =
+    let txt = recvErr proc
+
+    if not(Regex.IsMatch(txt, pat)) then
+        failwith(String.Format("stderr string '{0}' did not match pattern '{1}'", txt, pat))
+
+(* ------------------------------------------------------------------- *)
+(* Test Cases                                                          *)
+(* ------------------------------------------------------------------- *)
+
+testName <- "simple quit"
+testLogic <- fun p ->
+    send p "quit"
+runTest ()
+
+testName <- "debugger state reset"
+testLogic <- fun p ->
+    send p "reset"
+    assertRecv p "^All debugger state reset$"
+    send p "quit"
+runTest ()
+
+testName <- "configuration manipulation"
+testLogic <- fun p ->
+    send p "config get ChunkRawStrings"
+    assertRecv p "^'ChunkRawStrings' = 'False'$"
+    send p "config set ChunkRawStrings True"
+    assertRecv p "^'ChunkRawStrings' = 'True' \(was 'False'\)$"
+    send p "quit"
+runTest ()
+
+testName <- "catchpoint"
+testLogic <- fun p ->
+    send p "catchpoint add System.Exception"
+    assertRecv p "^Catchpoint for 'System.Exception' added$"
+    send p "run cs/throw.exe"
+    assertRecv p "^Inferior process '\d*' \('throw.exe'\) started$"
+    assertRecv p "^Trapped first-chance exception of type 'System.Exception'$"
+    assertRecv p "^#0 \[0x[A-Fa-f0-9]{8}\] Program.Main at .*/chk/cs/throw.cs:9$"
+    assertRecv p "^             throw new Exception\(\);$"
+    assertRecv p "^System.Exception: Exception of type 'System.Exception' was thrown.$"
+    send p "kill"
+    assertRecv p "^Inferior process '\d*' \('throw.exe'\) exited$"
+    send p "quit"
+runTest ()
+
+testName <- "source breakpoint"
+testLogic <- fun p ->
+    send p "breakpoint add location fs/print.fs 3"
+    assertRecv p "^Breakpoint '0' added at '.*/chk/fs/print.fs:3'$"
+    send p "run fs/print.exe"
+    assertRecv p "^Inferior process '\d*' \('print.exe'\) started$"
+    assertRecv p "^Hit breakpoint at '.*/chk/fs/print.fs:3'$"
+    assertRecv p "^#0 \[0x[A-Fa-f0-9]{8}\] Print.main at .*/chk/fs/print.fs:3$"
+    assertRecv p "^    printfn \"Hello World\"$"
+    send p "kill"
+    assertRecv p "^Inferior process '\d*' \('print.exe'\) exited$"
+    send p "quit"
+runTest ()
+
+testName <- "method breakpoint"
+testLogic <- fun p ->
+    send p "breakpoint add method Program.Main"
+    assertRecv p "^Breakpoint '0' added for method 'Program.Main'$"
+    send p "run cs/cwl.exe"
+    assertRecv p "^Inferior process '\d*' \('cwl.exe'\) started$"
+    assertRecv p "^Hit method breakpoint on 'Program.Main'$"
+    assertRecv p "^#0 \[0x[A-Fa-f0-9]{8}\] Program.Main at .*/chk/cs/cwl.cs:6$"
+    assertRecv p "^    {$"
+    send p "kill"
+    assertRecv p "^Inferior process '\d*' \('cwl.exe'\) exited$"
+    send p "quit"
+runTest ()
+
+(* ------------------------------------------------------------------- *)
+(* Teardown Logic                                                      *)
+(* ------------------------------------------------------------------- *)
+
+if failures = 0 then Console.ForegroundColor <- ConsoleColor.Green
+Console.WriteLine("Successes: {0}", successes)
+Console.ResetColor()
+
+if failures <> 0 then Console.ForegroundColor <- ConsoleColor.Red
+Console.WriteLine("Failures:  {0}", failures)
+Console.ResetColor()
diff --git a/chk/cs/cwl.cs b/chk/cs/cwl.cs
new file mode 100644
index 0000000..f917545
--- /dev/null
+++ b/chk/cs/cwl.cs
@@ -0,0 +1,9 @@
+using System;
+
+static class Program
+{
+    static void Main()
+    {
+        Console.WriteLine("Hello World");
+    }
+}
diff --git a/chk/cs/throw.cs b/chk/cs/throw.cs
new file mode 100644
index 0000000..f111976
--- /dev/null
+++ b/chk/cs/throw.cs
@@ -0,0 +1,15 @@
+using System;
+
+static class Program
+{
+    static void Main()
+    {
+        try
+        {
+             throw new Exception();
+        }
+        catch (Exception)
+        {
+        }
+    }
+}
diff --git a/chk/fs/print.fs b/chk/fs/print.fs
new file mode 100644
index 0000000..5d934b6
--- /dev/null
+++ b/chk/fs/print.fs
@@ -0,0 +1,4 @@
+[<EntryPoint>]
+let main _ =
+    printfn "Hello World"
+    0
diff --git a/mono.snk b/mono.snk
new file mode 100644
index 0000000..380116c
Binary files /dev/null and b/mono.snk differ
diff --git a/sdb.exe.config b/sdb.exe.config
new file mode 100644
index 0000000..931778c
--- /dev/null
+++ b/sdb.exe.config
@@ -0,0 +1,4 @@
+<configuration>
+    <dllmap dll="libedit" target="libedit.so.2" os="!windows" />
+    <dllmap dll="libedit" target="libedit.dylib" os="osx" />
+</configuration>
diff --git a/sdb.in b/sdb.in
new file mode 100644
index 0000000..2648e88
--- /dev/null
+++ b/sdb.in
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+MONO_PATH=`dirname $0`:$MONO_PATH exec `which mono` __MONO_OPTIONS__ $MONO_OPTIONS `dirname $0`/sdb.exe "$@"
diff --git a/src/AssemblyInfo.cs b/src/AssemblyInfo.cs
new file mode 100644
index 0000000..d962754
--- /dev/null
+++ b/src/AssemblyInfo.cs
@@ -0,0 +1,30 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Reflection;
+
+[assembly: AssemblyTitle("Mono.Debugger.Client")]
+[assembly: AssemblyProduct("Mono")]
+[assembly: AssemblyCopyright("Copyright 2013 Alex Rønne Petersen")]
+[assembly: AssemblyVersion("1.2.*")]
diff --git a/src/Color.cs b/src/Color.cs
new file mode 100644
index 0000000..939b525
--- /dev/null
+++ b/src/Color.cs
@@ -0,0 +1,117 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Linq;
+
+namespace Mono.Debugger.Client
+{
+    public static class Color
+    {
+        // We need this class because the color sequences emitted by
+        // the ForegroundColor, BackgroundColor, and ResetColor helpers
+        // on System.Console mess with libedit's input and history
+        // tracker.
+
+        static readonly bool _disableColors;
+
+        static Color()
+        {
+            _disableColors = Console.IsOutputRedirected ||
+                             new[] { null, "dumb" }.Contains(Environment.GetEnvironmentVariable("TERM")) ||
+                             Environment.GetEnvironmentVariable("SDB_COLORS") == "disable";
+        }
+
+        static string GetColor(string modifier, string color)
+        {
+            return _disableColors || Configuration.Current.DisableColors ?
+                   string.Empty : modifier + color;
+        }
+
+        public static string Red
+        {
+            get { return GetColor("\x1b[1m", "\x1b[31m"); }
+        }
+
+        public static string DarkRed
+        {
+            get { return GetColor(string.Empty, "\x1b[31m"); }
+        }
+
+        public static string Green
+        {
+            get { return GetColor("\x1b[1m", "\x1b[32m"); }
+        }
+
+        public static string DarkGreen
+        {
+            get { return GetColor(string.Empty, "\x1b[32m"); }
+        }
+
+        public static string Yellow
+        {
+            get { return GetColor("\x1b[1m", "\x1b[33m"); }
+        }
+
+        public static string DarkYellow
+        {
+            get { return GetColor(string.Empty, "\x1b[33m"); }
+        }
+
+        public static string Blue
+        {
+            get { return GetColor("\x1b[1m", "\x1b[34m"); }
+        }
+
+        public static string DarkBlue
+        {
+            get { return GetColor(string.Empty, "\x1b[34m"); }
+        }
+
+        public static string Magenta
+        {
+            get { return GetColor("\x1b[1m", "\x1b[35m"); }
+        }
+
+        public static string DarkMagenta
+        {
+            get { return GetColor(string.Empty, "\x1b[35m"); }
+        }
+
+        public static string Cyan
+        {
+            get { return GetColor("\x1b[1m", "\x1b[36m"); }
+        }
+
+        public static string DarkCyan
+        {
+            get { return GetColor(string.Empty, "\x1b[36m"); }
+        }
+
+        public static string Reset
+        {
+            get { return GetColor("\x1b[0m", string.Empty); }
+        }
+    }
+}
diff --git a/src/Command.cs b/src/Command.cs
new file mode 100644
index 0000000..93da373
--- /dev/null
+++ b/src/Command.cs
@@ -0,0 +1,42 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.
+//
+
+namespace Mono.Debugger.Client
+{
+    public abstract class Command
+    {
+        public abstract string[] Names { get; }
+
+        public abstract string Summary { get; }
+
+        public abstract string Syntax { get; }
+
+        public virtual string Help
+        {
+            get { return "No detailed help text available."; }
+        }
+
+        public abstract void Process(string args);
+    }
+}
diff --git a/src/CommandAttribute.cs b/src/CommandAttribute.cs
new file mode 100644
index 0000000..310d4ea
--- /dev/null
+++ b/src/CommandAttribute.cs
@@ -0,0 +1,33 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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;
+
+namespace Mono.Debugger.Client
+{
+    [AttributeUsage(AttributeTargets.Class, Inherited = false)]
+    public sealed class CommandAttribute : Attribute
+    {
+    }
+}
diff --git a/src/CommandLine.cs b/src/CommandLine.cs
new file mode 100644
index 0000000..34b5d83
--- /dev/null
+++ b/src/CommandLine.cs
@@ -0,0 +1,318 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Concurrent;
+using System.Collections.Generic;
+using System.IO;
+using System.Runtime.InteropServices;
+using System.Threading;
+using Mono.Terminal;
+using Mono.Unix;
+using Mono.Unix.Native;
+using Mono.Debugger.Client.Commands;
+
+namespace Mono.Debugger.Client
+{
+    public static class CommandLine
+    {
+        internal static bool Stop { get; set; }
+
+        internal static RootCommand Root { get; private set; }
+
+        internal static AutoResetEvent ResumeEvent { get; private set; }
+
+        internal static bool InferiorExecuting { get; set; }
+
+        static readonly ConcurrentQueue<string> _queue = new ConcurrentQueue<string>();
+
+        static readonly LineEditor _lineEditor;
+
+        static bool _windowsConsoleHandlerSet;
+
+        static Thread _signalThread;
+
+        static CommandLine()
+        {
+            Root = new RootCommand();
+            ResumeEvent = new AutoResetEvent(false);
+
+            try
+            {
+                LibEdit.Initialize();
+            }
+            catch (DllNotFoundException)
+            {
+                // Fall back to `Mono.Terminal.LineEditor`.
+                _lineEditor = new LineEditor(null);
+            }
+        }
+
+        static void Process(string cmd, bool rc)
+        {
+            if (!rc && _lineEditor == null)
+                LibEdit.AddHistory(cmd);
+
+            var args = cmd.Trim();
+
+            if (args.Length == 0)
+                return;
+
+            try
+            {
+                Root.Process(args);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Command threw an exception:");
+                Log.Error(ex.ToString());
+            }
+        }
+
+        static string GetPrompt()
+        {
+            return string.Format("{0}{1}{2} ",
+                                 Color.DarkMagenta,
+                                 Configuration.Current.InputPrompt,
+                                 Color.Reset);
+        }
+
+        static string GetFilePath()
+        {
+            var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+            return Path.Combine(home, ".sdb.rc");
+        }
+
+        public static void RunCommands(IEnumerable<string> commands)
+        {
+            foreach (var cmd in commands)
+                _queue.Enqueue(cmd);
+        }
+
+        public static void RunFile(string path, bool swallowAndLog)
+        {
+            var commands = new List<string>();
+
+            try
+            {
+                string line;
+
+                using (var reader = new StreamReader(path))
+                    while ((line = reader.ReadLine()) != null)
+                        commands.Add(line);
+            }
+            catch (Exception ex)
+            {
+                if (swallowAndLog)
+                {
+                    Log.Error("Could not read commands in file '{0}':", path);
+                    Log.Error(ex.ToString());
+
+                    return;
+                }
+                else
+                    throw;
+            }
+
+            foreach (var cmd in commands)
+                _queue.Enqueue(cmd);
+        }
+
+        static void ControlCHandler()
+        {
+            Log.Info(string.Empty);
+
+            switch (Debugger.State)
+            {
+                case State.Running:
+                    Debugger.Pause();
+
+                    break;
+                case State.Suspended:
+                    Log.Error("Inferior is already suspended");
+                    Log.InfoSameLine(GetPrompt());
+
+                    break;
+                case State.Exited:
+                    // If `InferiorExecuting` is set while the state is
+                    // `Exited`, it means that we were listening or
+                    // connecting. So cancel.
+                    if (InferiorExecuting)
+                        Debugger.Kill();
+                    else
+                    {
+                        Log.Error("No inferior process");
+                        Log.InfoSameLine(GetPrompt());
+                    }
+
+                    break;
+            }
+        }
+
+        static void ConsoleControlCHandler(object sender, ConsoleCancelEventArgs e)
+        {
+            // This method is only fired on Windows.
+            ControlCHandler();
+
+            e.Cancel = true;
+        }
+
+        internal static void SetControlCHandler()
+        {
+            if (Utilities.IsWindows)
+            {
+                if (!_windowsConsoleHandlerSet)
+                    Console.CancelKeyPress += ConsoleControlCHandler;
+
+                _windowsConsoleHandlerSet = true;
+            }
+            else if (_signalThread == null)
+            {
+                Stdlib.SetSignalAction(Signum.SIGINT, SignalAction.Default);
+
+                _signalThread = new Thread(() =>
+                {
+                    try
+                    {
+                        using (var sig = new UnixSignal(Signum.SIGINT))
+                        {
+                            while (true)
+                            {
+                                sig.WaitOne();
+
+                                ControlCHandler();
+                            }
+                        }
+                    }
+                    catch (ThreadAbortException)
+                    {
+                    }
+                });
+
+                _signalThread.Start();
+            }
+        }
+
+        internal static void UnsetControlCHandler()
+        {
+            if (Utilities.IsWindows)
+            {
+                Console.CancelKeyPress -= ConsoleControlCHandler;
+
+                _windowsConsoleHandlerSet = false;
+            }
+            else if (_signalThread != null)
+            {
+                _signalThread.Abort();
+                _signalThread.Join();
+
+                _signalThread = null;
+
+                Stdlib.SetSignalAction(Signum.SIGINT, SignalAction.Ignore);
+            }
+        }
+
+        internal static void Run(Version ver, bool batch, bool rc,
+                                 IEnumerable<string> commands,
+                                 IEnumerable<string> files)
+        {
+            if (!Configuration.Read())
+                Configuration.Defaults();
+
+            Configuration.Apply();
+
+            Log.Notice("Welcome to the Mono soft debugger (sdb {0})", ver);
+            Log.Notice("Type 'help' for a list of commands or 'quit' to exit");
+            Log.Info(string.Empty);
+
+            Root.AddCommands(Plugins.LoadDefault());
+
+            var rcFile = GetFilePath();
+
+            if (rc && File.Exists(rcFile))
+                RunFile(rcFile, true);
+
+            foreach (var file in files)
+                if (File.Exists(file))
+                    RunFile(file, true);
+
+            RunCommands(commands);
+
+            while (!Stop)
+            {
+                // If the command caused the debuggee to start
+                // or resume execution, wait for it to suspend.
+                if (InferiorExecuting)
+                {
+                    ResumeEvent.WaitOne();
+                    InferiorExecuting = false;
+                }
+
+                string cmd;
+
+                // We use a queue here so that batch commands are
+                // also subject to the suspension check above. It
+                // also makes things easier since everything gets
+                // executed in one thread.
+                if (_queue.TryDequeue(out cmd))
+                    Process(cmd, true);
+                else if (batch)
+                    Stop = true;
+                else
+                {
+                    cmd = _lineEditor != null ?
+                          _lineEditor.Edit(GetPrompt(), string.Empty) :
+                          LibEdit.ReadLine(GetPrompt());
+
+                    // Did we get EOF?
+                    if (cmd == null)
+                    {
+                        Log.Info(string.Empty);
+
+                        if (Debugger.State != State.Exited)
+                            Log.Error("An inferior process is active");
+                        else
+                            Stop = true;
+                    }
+                    else
+                        Process(cmd, false);
+                }
+
+            }
+
+            // Clean up just in case.
+            UnsetControlCHandler();
+
+            // Let's not leave dead Mono processes behind...
+            Debugger.Pause();
+            Debugger.Kill();
+
+            while (!Debugger.DebuggeeKilled)
+                Thread.Sleep(10);
+
+            Log.Notice("Bye");
+        }
+    }
+}
diff --git a/src/Commands/ArgumentsCommand.cs b/src/Commands/ArgumentsCommand.cs
new file mode 100644
index 0000000..a841e97
--- /dev/null
+++ b/src/Commands/ArgumentsCommand.cs
@@ -0,0 +1,74 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ArgumentsCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "arguments", "args" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Get or set the current program arguments."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "arguments|args [args]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Without any argument, this command prints the arguments to be passed to\n" +
+                       "inferior processes launched locally. If an argument is given, it is set\n" +
+                       "as the arguments to be passed.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (args.Length == 0)
+            {
+                Log.Info("Program arguments: '{0}'", Debugger.Arguments);
+                return;
+            }
+            else
+            {
+                var old = Debugger.Arguments;
+
+                Debugger.Arguments = args;
+
+                Log.Info("Program arguments set to '{0}' (were '{1}')", args, old);
+            }
+        }
+    }
+}
diff --git a/src/Commands/AttachCommand.cs b/src/Commands/AttachCommand.cs
new file mode 100644
index 0000000..ef26f07
--- /dev/null
+++ b/src/Commands/AttachCommand.cs
@@ -0,0 +1,61 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class AttachCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "attach" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Attach to a running process."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "attach <id>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Attempts to attach to the given process ID.\n" +
+                       "\n" +
+                       "Currently unimplemented.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            Log.Error("Attach support is not yet implemented.");
+        }
+    }
+}
diff --git a/src/Commands/BacktraceCommand.cs b/src/Commands/BacktraceCommand.cs
new file mode 100644
index 0000000..8328b4e
--- /dev/null
+++ b/src/Commands/BacktraceCommand.cs
@@ -0,0 +1,83 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class BacktraceCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "backtrace", "bt" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Print a backtrace of the call stack."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "backtrace|bt"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Prints a backtrace of the call stack for the active thread. Includes\n" +
+                       "IL offsets, source locations, and source lines.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            var bt = Debugger.ActiveBacktrace;
+
+            if (bt == null)
+            {
+                Log.Error("No active backtrace");
+                return;
+            }
+
+            if (bt.FrameCount == 0)
+            {
+                Log.Info("Backtrace for this thread is unavailable");
+                return;
+            }
+
+            for (var i = 0; i < bt.FrameCount; i++)
+            {
+                var f = bt.GetFrame(i);
+                var str = Utilities.StringizeFrame(f, true);
+
+                if (f == Debugger.ActiveFrame)
+                    Log.Emphasis(str);
+                else
+                    Log.Info(str);
+            }
+        }
+    }
+}
diff --git a/src/Commands/BreakpointCommand.cs b/src/Commands/BreakpointCommand.cs
new file mode 100644
index 0000000..b9f5186
--- /dev/null
+++ b/src/Commands/BreakpointCommand.cs
@@ -0,0 +1,531 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class BreakpointCommand : MultiCommand
+    {
+        sealed class BreakpointAddCommand : MultiCommand
+        {
+            sealed class BreakpointAddLocationCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "location", "at" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Add a breakpoint at a source location."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "break|bp add location <file> <line>"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Adds a breakpoint to the specified source location.\n" +
+                               "\n" +
+                               "This is a 'normal' breakpoint associated with a file name and line\n" +
+                               "which can have an optional condition expression.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    var splitArgs = args.Split(' ').Where(x => x != string.Empty);
+                    var count = splitArgs.Count();
+
+                    if (count == 0)
+                    {
+                        Log.Error("No file name given");
+                        return;
+                    }
+
+                    if (count == 1)
+                    {
+                        Log.Error("No line number given");
+                        return;
+                    }
+
+                    var lineStr = splitArgs.Last();
+
+                    int line;
+
+                    if (!int.TryParse(lineStr, out line))
+                    {
+                        Log.Error("Invalid line number");
+                        return;
+                    }
+
+                    var file = new string(args.Take(args.Length - lineStr.Length).ToArray()).Trim();
+
+                    try
+                    {
+                        file = Path.GetFullPath(file);
+                    }
+                    catch (Exception ex)
+                    {
+                        Log.Info("Could not compute absolute path of '{0}':");
+                        Log.Info(ex.ToString());
+
+                        return;
+                    }
+
+                    foreach (var be in Debugger.Breakpoints)
+                    {
+                        var bp = be.Value as Breakpoint;
+
+                        if (bp == null)
+                            continue;
+
+                        if (bp.FileName == file && bp.Line == line)
+                        {
+                            Log.Error("A breakpoint at '{0}:{1}' already exists ('{2}')", file, line, be.Key);
+                            return;
+                        }
+                    }
+
+                    var id = Debugger.GetBreakpointId();
+
+                    Debugger.Breakpoints.Add(id, Debugger.BreakEvents.Add(file, line));
+
+                    Log.Info("Breakpoint '{0}' added at '{1}:{2}'", id, file, line);
+                }
+            }
+
+            sealed class BreakpointAddMethodCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "method", "function" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Add a breakpoint at a method."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "break|bp add method|function <name>"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Adds a breakpoint at the specified method.\n" +
+                               "\n" +
+                               "A breakpint of this type has no location or condition; it simply\n" +
+                               "triggers whenever execution enters the specified method.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    if (args.Length == 0)
+                    {
+                        Log.Error("No method name given");
+                        return;
+                    }
+
+                    foreach (var be in Debugger.Breakpoints)
+                    {
+                        if (!(be.Value is FunctionBreakpoint))
+                            continue;
+
+                        if (((FunctionBreakpoint)be.Value).FunctionName == args)
+                        {
+                            Log.Error("A method breakpoint for '{0}' already exists ('{1}')", args, be.Key);
+                            return;
+                        }
+                    }
+
+                    // TODO: Parameter types too.
+
+                    var id = Debugger.GetBreakpointId();
+                    var fbp = new FunctionBreakpoint(args, "C#");
+
+                    Debugger.Breakpoints.Add(id, fbp);
+                    Debugger.BreakEvents.Add(fbp);
+
+                    Log.Info("Breakpoint '{0}' added for method '{1}'", id, args);
+                }
+            }
+
+            public BreakpointAddCommand()
+            {
+                AddCommand<BreakpointAddLocationCommand>();
+                AddCommand<BreakpointAddMethodCommand>();
+            }
+
+            public override string[] Names
+            {
+                get { return new[] { "add" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Add a breakpoint at a location or method."; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Adds a breakpoint at a source location or method entry.\n" +
+                           "\n" +
+                           "Breakpoints are initially toggled on when added.";
+                }
+            }
+
+            public override string Parent
+            {
+                get { return "break"; }
+            }
+        }
+
+        sealed class BreakpointClearCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "clear" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Clear all breakpoints."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "break|bp clear"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Removes all breakpoints.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                Debugger.Breakpoints.Clear();
+                Debugger.BreakEvents.ClearBreakpoints();
+
+                Log.Info("All breakpoints cleared");
+            }
+        }
+
+        sealed class BreakpointConditionCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "condition", "expression" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Set a conditional expression for a breakpoint."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "break|bp condition|expression <id> [expr]"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Without any expression argument, unsets the condition for the given\n" +
+                           "breakpoint. If an expression is given, the breakpoint's condition is\n" +
+                           "set to that value, such that it will only trigger if the condition\n" +
+                           "evaluates to true.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var id = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+                if (id == null)
+                {
+                    Log.Error("No breakpoint ID given");
+                    return;
+                }
+
+                long num;
+
+                if (!long.TryParse(id, out num))
+                {
+                    Log.Error("Invalid breakpoint ID");
+                    return;
+                }
+
+                BreakEvent b;
+
+                if (!Debugger.Breakpoints.TryGetValue(num, out b))
+                {
+                    Log.Error("Breakpoint '{0}' not found", num);
+                    return;
+                }
+
+                if (b is FunctionBreakpoint)
+                {
+                    Log.Error("Breakpoint '{0}' is a method breakpoint", num);
+                    return;
+                }
+
+                var expr = new string(args.Skip(id.Length).ToArray()).Trim();
+
+                var bp = (Breakpoint)b;
+                var was = bp.ConditionExpression != null ?
+                          string.Format(" (was '{0}')", bp.ConditionExpression) :
+                          string.Empty;
+
+                if (expr.Length == 0)
+                {
+                    bp.ConditionExpression = null;
+
+                    Log.Info("Condition for breakpoint '{0}' unset{1}", num, was);
+                }
+                else
+                {
+                    bp.ConditionExpression = expr;
+
+                    Log.Info("Condition for breakpoint '{0}' set to '{1}'{2}", num, expr, was);
+                }
+            }
+        }
+
+        sealed class BreakpointDeleteCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "delete", "remove" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Delete a breakpoint by ID."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "break|bp delete|remove <id>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Deletes the breakpoint with the specified ID, if it exists.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                long num;
+
+                if (!long.TryParse(args, out num))
+                {
+                    Log.Error("Invalid breakpoint ID");
+                    return;
+                }
+
+                BreakEvent b;
+
+                if (!Debugger.Breakpoints.TryGetValue(num, out b))
+                {
+                    Log.Error("Breakpoint '{0}' not found", num);
+                    return;
+                }
+
+                Debugger.Breakpoints.Remove(num);
+                Debugger.BreakEvents.Remove(b);
+
+                Log.Info("Breakpoint '{0}' deleted", num);
+            }
+        }
+
+        sealed class BreakpointListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all set breakpoints and their IDs."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "break|bp list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all breakpoints, along with their IDs and settings.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (Debugger.Breakpoints.Count == 0)
+                {
+                    Log.Info("No breakpoints");
+                    return;
+                }
+
+                foreach (var pair in Debugger.Breakpoints)
+                {
+                    var bp = pair.Value as Breakpoint;
+                    var fbp = pair.Value as FunctionBreakpoint;
+
+                    var at = fbp != null ? fbp.FunctionName : string.Format("{0}:{1}", bp.FileName, bp.Line);
+                    var expr = bp != null && bp.ConditionExpression != null ?
+                               string.Format(" '{0}'", bp.ConditionExpression) :
+                               string.Empty;
+
+                    // TODO: Parameter types too.
+
+                    Log.Info("#{0} '{1}'{2}", pair.Key, at, expr);
+                }
+            }
+        }
+
+        sealed class BreakpointToggleCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "toggle" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Toggle a breakpoint on/off."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "break|bp toggle <id>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Toggles a breakpint on or off.\n" +
+                           "\n" +
+                           "Toggling a breakpoint off keeps it in the breakpoint list but makes it\n" +
+                           "not actually trigger, regardless of its type or condition.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                long num;
+
+                if (!long.TryParse(args, out num))
+                {
+                    Log.Error("Invalid breakpoint ID");
+                    return;
+                }
+
+                BreakEvent b;
+
+                if (!Debugger.Breakpoints.TryGetValue(num, out b))
+                {
+                    Log.Error("Breakpoint '{0}' not found", num);
+                    return;
+                }
+
+                if (Debugger.BreakEvents.Contains(b))
+                {
+                    Debugger.BreakEvents.Remove(b);
+
+                    Log.Info("Breakpoint '{0}' disabled", num);
+                }
+                else
+                {
+                    Debugger.BreakEvents.Add(b);
+
+                    Log.Info("Breakpoint '{0}' enabled", num);
+                }
+            }
+        }
+
+        public BreakpointCommand()
+        {
+            AddCommand<BreakpointAddCommand>();
+            AddCommand<BreakpointClearCommand>();
+            AddCommand<BreakpointConditionCommand>();
+            AddCommand<BreakpointDeleteCommand>();
+            AddCommand<BreakpointListCommand>();
+            AddCommand<BreakpointToggleCommand>();
+
+            Forward<BreakpointListCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "breakpoint", "bp" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Add, delete, and show breakpoints."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Manipulates breakpoints.\n" +
+                       "\n" +
+                       "Breakpoints can be set at specific lines of source files or on the entry of\n" +
+                       "managed methods. Non-method breakpoints can also have conditions set such\n" +
+                       "that they only cause the debugger to break if the condition is true.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/CatchpointCommand.cs b/src/Commands/CatchpointCommand.cs
new file mode 100644
index 0000000..1f81598
--- /dev/null
+++ b/src/Commands/CatchpointCommand.cs
@@ -0,0 +1,218 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class CatchpointCommand : MultiCommand
+    {
+        sealed class CatchpointAddCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "add" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Add a catchpoint for an exception type."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "catch|cp add <type>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Adds a catchpoint for the given exception type.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                foreach (var cp in Debugger.BreakEvents.GetCatchpoints())
+                {
+                    if (cp.ExceptionName == args)
+                    {
+                        Log.Error("Catchpoint for '{0}' already exists");
+                        return;
+                    }
+                }
+
+                Debugger.BreakEvents.AddCatchpoint(args);
+
+                Log.Info("Catchpoint for '{0}' added", args);
+            }
+        }
+
+        sealed class CatchpointClearCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "clear" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Clear all catchpoints."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "catch|cp clear"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Removes all active catchpoints.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                Debugger.BreakEvents.ClearCatchpoints();
+
+                Log.Info("All catchpoints cleared");
+            }
+        }
+
+        sealed class CatchpointDeleteCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "delete", "remove" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Delete a catchpoint by type."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "catch|cp delete|remove <type>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Deletes the catchpoint for the given exception type, if it exists.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (!Debugger.BreakEvents.GetCatchpoints().Any(x => x.ExceptionName == args))
+                {
+                    Log.Error("No catchpoint for '{0}' found", args);
+                    return;
+                }
+
+                Debugger.BreakEvents.RemoveCatchpoint(args);
+
+                Log.Info("Catchpoint for '{0}' deleted", args);
+            }
+        }
+
+        sealed class CatchpointListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all set catchpoints."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "catch|cp list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all currently active catchpoints.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var cps = Debugger.BreakEvents.GetCatchpoints();
+
+                if (cps.Count == 0)
+                {
+                    Log.Info("No catchpoints");
+                    return;
+                }
+
+                foreach (var cp in cps)
+                    Log.Info("'{0}'", cp.ExceptionName);
+            }
+        }
+
+        public CatchpointCommand()
+        {
+            AddCommand<CatchpointAddCommand>();
+            AddCommand<CatchpointClearCommand>();
+            AddCommand<CatchpointDeleteCommand>();
+            AddCommand<CatchpointListCommand>();
+
+            Forward<CatchpointListCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "catchpoint", "cp" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Add, delete, and show catchpoints."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Manipulates catchpoints.\n" +
+                       "\n" +
+                       "When a catchpoint is added for an exception type and the debuggee code\n" +
+                       "actively catches it, the debugger will break on the 'throw' site of the\n" +
+                       "exception.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/ConfigCommand.cs b/src/Commands/ConfigCommand.cs
new file mode 100644
index 0000000..9f74949
--- /dev/null
+++ b/src/Commands/ConfigCommand.cs
@@ -0,0 +1,278 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Linq;
+using System.Reflection;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ConfigCommand : MultiCommand
+    {
+        sealed class ConfigGetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "get" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Get the value of a configuration element."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "config|cfg get <name>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Gets the value of the given configuration element.\n" +
+                           "\n" +
+                           "This is either the default value or the value in '~/.sdb.cfg'.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var name = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+                if (name == null)
+                {
+                    Log.Error("No configuration element name given");
+                    return;
+                }
+
+                var props = typeof(Configuration).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+                foreach (var prop in props)
+                {
+                    if (prop.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
+                    {
+                        Log.Info("'{0}' = '{1}'", prop.Name, prop.GetValue(Configuration.Current));
+                        return;
+                    }
+                }
+
+                Log.Error("Configuration element '{0}' not found", name);
+            }
+        }
+
+        sealed class ConfigListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all configuration elements and their values."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "config|cfg list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all configuration elements and their values.\n" +
+                           "\n" +
+                           "These are either the default values or the values in '~/.sdb.cfg'.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var props = typeof(Configuration).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+                foreach (var prop in props)
+                    Log.Info("'{0}' = '{1}'", prop.Name, prop.GetValue(Configuration.Current));
+            }
+        }
+
+        sealed class ConfigResetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "reset" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Reset configuration values to their defaults."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "config|cfg reset"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Resets all configuration elements to their default values.\n" +
+                           "\n" +
+                           "Note that this overwrites '~/.sdb.cfg' too.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                Configuration.Defaults();
+                Configuration.Apply();
+
+                Log.Info("All configuration values reset");
+            }
+        }
+
+        sealed class ConfigSetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "set" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Set the value of a configuration element."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "config|cfg set <name> <value>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the value of the given configuration element.\n" +
+                           "\n" +
+                           "If '~/.sdb.cfg' doesn't exist, this command causes it to be created.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var name = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+                if (name == null)
+                {
+                    Log.Error("No configuration element name given");
+                    return;
+                }
+
+                var rest = new string(args.Skip(name.Length).ToArray()).Trim();
+
+                if (rest.Length == 0)
+                {
+                    Log.Error("No configuration value given");
+                    return;
+                }
+
+                var props = typeof(Configuration).GetProperties(BindingFlags.Public | BindingFlags.Instance);
+
+                foreach (var prop in props)
+                {
+                    if (prop.Name.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
+                    {
+                        var was = prop.GetValue(Configuration.Current);
+
+                        object value = null;
+
+                        try
+                        {
+                            if (prop.PropertyType == typeof(bool))
+                                value = bool.Parse(rest);
+                            else if (prop.PropertyType == typeof(int))
+                                value = int.Parse(rest);
+                            else if (prop.PropertyType == typeof(string))
+                                value = rest;
+                        }
+                        catch (Exception ex)
+                        {
+                            if (ex is FormatException || ex is OverflowException)
+                            {
+                                Log.Error("Invalid configuration value");
+                                return;
+                            }
+
+                            throw;
+                        }
+
+                        prop.SetValue(Configuration.Current, value);
+
+                        Configuration.Write();
+                        Configuration.Apply();
+
+                        Log.Info("'{0}' = '{1}' (was '{2}')", prop.Name, value, was);
+
+                        return;
+                    }
+                }
+
+                Log.Error("Configuration element '{0}' not found", name);
+            }
+        }
+
+        public ConfigCommand()
+        {
+            AddCommand<ConfigGetCommand>();
+            AddCommand<ConfigListCommand>();
+            AddCommand<ConfigResetCommand>();
+            AddCommand<ConfigSetCommand>();
+
+            Forward<ConfigListCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "config", "cfg" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Manipulate the debugger configuration."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Manipulates the debugger configuration.\n" +
+                       "\n" +
+                       "Configuration values are stored in '~/.sdb.cfg' and are loaded on debugger\n" +
+                       "startup. The file is only created when a configuration value is set.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/ConnectCommand.cs b/src/Commands/ConnectCommand.cs
new file mode 100644
index 0000000..828aa7e
--- /dev/null
+++ b/src/Commands/ConnectCommand.cs
@@ -0,0 +1,104 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+using System.Net;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ConnectCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "connect", "remote" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Connect to a remote virtual machine."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "connect|remote <addr> <port>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Attempts to connect to a debuggee running at the specified IP address and\n" +
+                       "port.\n" +
+                       "\n" +
+                       "Respects the MaxConnectionAttempts and ConnectionAttemptInterval settings.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State != State.Exited)
+            {
+                Log.Error("An inferior process is already being debugged");
+                return;
+            }
+
+            var ip = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+            if (ip == null)
+            {
+                Log.Error("No IP address given");
+                return;
+            }
+
+            IPAddress addr;
+
+            if (!IPAddress.TryParse(ip, out addr))
+            {
+                Log.Error("Invalid IP address");
+                return;
+            }
+
+            var rest = new string(args.Skip(ip.Length).ToArray()).Trim();
+
+            if (rest.Length == 0)
+            {
+                Log.Error("No port number given");
+                return;
+            }
+
+            int port;
+
+            if (!int.TryParse(rest, out port) || port <= 0)
+            {
+                Log.Error("Invalid port number");
+                return;
+            }
+
+            Log.Info("Connecting to '{0}:{1}'...", addr, port);
+
+            Debugger.Connect(addr, port);
+        }
+    }
+}
diff --git a/src/Commands/ContinueCommand.cs b/src/Commands/ContinueCommand.cs
new file mode 100644
index 0000000..5be350e
--- /dev/null
+++ b/src/Commands/ContinueCommand.cs
@@ -0,0 +1,63 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ContinueCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "continue", "resume" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Continue execution of the inferior."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "continue|resume"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Continues execution of the inferior process after it has been paused by a\n" +
+                       "breakpoint, catchpoint, unhandled exception, etc.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State == State.Exited)
+                Log.Error("No inferior process");
+            else
+                Debugger.Continue();
+        }
+    }
+}
diff --git a/src/Commands/DatabaseCommand.cs b/src/Commands/DatabaseCommand.cs
new file mode 100644
index 0000000..792dfee
--- /dev/null
+++ b/src/Commands/DatabaseCommand.cs
@@ -0,0 +1,185 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class DatabaseCommand : MultiCommand
+    {
+        sealed class DatabaseLoadCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "load", "read" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Read the database for the current inferior."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "database|db load|read <file>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Loads the debugger state from the given file.\n" +
+                           "\n" +
+                           "Note that this resets the active debugger state before applying the\n" +
+                           "data read from the given file.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (args.Length == 0)
+                {
+                    Log.Error("No file path given");
+                    return;
+                }
+
+                if (!File.Exists(args))
+                {
+                    Log.Error("File '{0}' does not exist", args);
+                    return;
+                }
+
+                FileInfo file;
+
+                try
+                {
+                    file = new FileInfo(args);
+                }
+                catch (Exception ex)
+                {
+                    Log.Error("Could not open file '{0}':", args);
+                    Log.Error(ex.ToString());
+
+                    return;
+                }
+
+                Debugger.Read(file);
+
+                Log.Info("Debugger state initialized from '{0}'", args);
+            }
+        }
+
+        sealed class DatabaseSaveCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "save", "write" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Save the database for the current inferior."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "database|db save|write <file>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Saves the active debugger state to the given file.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (args.Length == 0)
+                {
+                    Log.Error("No file path given");
+                    return;
+                }
+
+                FileInfo file;
+
+                try
+                {
+                    file = new FileInfo(args);
+                }
+                catch (Exception ex)
+                {
+                    Log.Error("Could not open file '{0}':", args);
+                    Log.Error(ex.ToString());
+
+                    return;
+                }
+
+                Debugger.Write(file);
+
+                Log.Info("Debugger state saved to '{0}'", args);
+            }
+        }
+
+        public DatabaseCommand()
+        {
+            AddCommand<DatabaseLoadCommand>();
+            AddCommand<DatabaseSaveCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "database", "db" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Store and load debugger state."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Stores and loads the debugger state.\n" +
+                       "\n" +
+                       "This is useful for retaining things like breakpoints and environment\n" +
+                       "variables across debugger runs.\n" +
+                       "\n" +
+                       "The following state is saved/loaded:\n" +
+                       "\n" +
+                       "* Working directory.\n" +
+                       "* Program arguments.\n" +
+                       "* Environment variables.\n" +
+                       "* Watches.\n" +
+                       "* Breakpoints and catchpoints.\n" +
+                       "* Session options.\n" +
+                       "* Evaluation options.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/DecompileCommand.cs b/src/Commands/DecompileCommand.cs
new file mode 100644
index 0000000..8aa0005
--- /dev/null
+++ b/src/Commands/DecompileCommand.cs
@@ -0,0 +1,62 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class DecompileCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "decompile" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Decompile the current stack frame."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "decompile"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Decompiles the IL code in the active stack frame and attempts to highlight\n" +
+                       "the line closest to the current IL offset.\n" +
+                       "\n" +
+                       "Currently unimplemented.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            Log.Error("Decompilation is not yet implemented.");
+        }
+    }
+}
diff --git a/src/Commands/DirectoryCommand.cs b/src/Commands/DirectoryCommand.cs
new file mode 100644
index 0000000..0f09571
--- /dev/null
+++ b/src/Commands/DirectoryCommand.cs
@@ -0,0 +1,73 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class DirectoryCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "directory", "cd", "cwd" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Get or set the current working directory."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "directory|cd|cwd [path]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Without any argument, prints the current working directory for inferior\n" +
+                       "processes launched locally. If an argument is given, the working directory\n" +
+                       "is set to that argument.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (args.Length == 0)
+                Log.Info("Working directory: '{0}'", Debugger.WorkingDirectory);
+            else if (Directory.Exists(args))
+            {
+                var old = Debugger.WorkingDirectory;
+
+                Debugger.WorkingDirectory = args;
+
+                Log.Info("Working directory set to '{0}' (was '{1}')", args, old);
+            }
+            else
+                Log.Error("Directory '{0}' does not exist", args);
+        }
+    }
+}
diff --git a/src/Commands/DisassembleCommand.cs b/src/Commands/DisassembleCommand.cs
new file mode 100644
index 0000000..5d62bde
--- /dev/null
+++ b/src/Commands/DisassembleCommand.cs
@@ -0,0 +1,114 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class DisassembleCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "disassemble", "disasm", "dasm" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Disassemble the current stack frame."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "disassemble|disasm|dasm [lower] [upper]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Disassembles the current stack frame and highlights the line corresponding\n" +
+                       "to the current IL offset.\n" +
+                       "\n" +
+                       "If arguments are given, they specify how many lines to print before and\n" +
+                       "after the line corresponding to the current IL offset.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            var f = Debugger.ActiveFrame;
+
+            if (f == null)
+            {
+                Log.Error("No active stack frame");
+                return;
+            }
+
+            var lower = -10;
+            var upper = 20;
+
+            var lowerStr = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+            if (lowerStr != null)
+            {
+                if (!int.TryParse(lowerStr, out lower))
+                {
+                    Log.Error("Invalid lower bound value");
+                    return;
+                }
+
+                lower = -lower;
+
+                var upperStr = new string(args.Skip(lowerStr.Length).ToArray()).Trim();
+
+                if (upperStr.Length != 0)
+                {
+                    if (!int.TryParse(upperStr, out upper))
+                    {
+                        Log.Error("Invalid upper bound value");
+                        return;
+                    }
+
+                    upper += System.Math.Abs(lower) + 1;
+                }
+            }
+
+            var asm = f.Disassemble(lower, upper);
+
+            foreach (var line in asm)
+            {
+                if (line.IsOutOfRange)
+                    continue;
+
+                var str = string.Format("0x{0:X8}    {1}", line.Address, line.Code);
+
+                if (line.Address == f.Address)
+                    Log.Emphasis(str);
+                else
+                    Log.Info(str);
+            }
+        }
+    }
+}
diff --git a/src/Commands/EnvironmentCommand.cs b/src/Commands/EnvironmentCommand.cs
new file mode 100644
index 0000000..d5d8f56
--- /dev/null
+++ b/src/Commands/EnvironmentCommand.cs
@@ -0,0 +1,305 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class EnvironmentCommand : MultiCommand
+    {
+        sealed class EnvironmentClearCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "clear" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Clear all environment variables."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment clear"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Clears all environment variables.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                Debugger.EnvironmentVariables.Clear();
+
+                Log.Info("All environment variables cleared");
+            }
+        }
+
+        sealed class EnvironmentDeleteCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "delete", "remove" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Delete an environment variable."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment delete|remove <name>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Deletes the given environment variable, if it exists.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                string val;
+
+                if (!Debugger.EnvironmentVariables.TryGetValue(args, out val))
+                {
+                    Log.Error("Environment variable '{0}' not found", args);
+                    return;
+                }
+
+                Debugger.EnvironmentVariables.Remove(args);
+
+                Log.Info("Environment variable '{0}' (with value '{1}') deleted", args, val);
+            }
+        }
+
+        sealed class EnvironmentGetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "get" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Get the value of an environment variable."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment get <name>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Gets the value of the given environment variable.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var name = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+                if (name == null)
+                {
+                    Log.Error("No environment variable name given");
+                    return;
+                }
+
+                string val;
+
+                if (Debugger.EnvironmentVariables.TryGetValue(name, out val))
+                    Log.Info("'{0}' = '{1}'", name, val);
+                else
+                    Log.Error("Environment variable '{0}' is not set", name);
+            }
+        }
+
+        sealed class EnvironmentInheritCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "inherit" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Inherit all environment variables from the debugger."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment inherit"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Inherits all environment variables from the debugger host environment.\n" +
+                           "\n" +
+                           "This simply grabs all environment variables on the machine where the\n" +
+                           "debugger is running and inserts them into the debuggee environment.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                foreach (DictionaryEntry pair in Environment.GetEnvironmentVariables())
+                    Debugger.EnvironmentVariables[(string)pair.Key] = (string)pair.Value;
+            }
+        }
+
+        sealed class EnvironmentListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all environment variables and their values."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all environment variables and their values.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (Debugger.EnvironmentVariables.Count == 0)
+                {
+                    Log.Info("No environment variables");
+                    return;
+                }
+
+                foreach (var pair in Debugger.EnvironmentVariables)
+                    Log.Info("'{0}' = '{1}'", pair.Key, pair.Value);
+            }
+        }
+
+        sealed class EnvironmentSetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "set" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Set the value of an environment variable."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "environment set <name> <value>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the given environment variable to the given value.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var name = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+                if (name == null)
+                {
+                    Log.Error("No environment variable name given");
+                    return;
+                }
+
+                var rest = new string(args.Skip(name.Length).ToArray()).Trim();
+
+                if (rest.Length == 0)
+                {
+                    Log.Error("No environment variable value given");
+                    return;
+                }
+
+                Debugger.EnvironmentVariables[name] = rest;
+            }
+        }
+
+        public EnvironmentCommand()
+        {
+            AddCommand<EnvironmentClearCommand>();
+            AddCommand<EnvironmentDeleteCommand>();
+            AddCommand<EnvironmentGetCommand>();
+            AddCommand<EnvironmentInheritCommand>();
+            AddCommand<EnvironmentListCommand>();
+            AddCommand<EnvironmentSetCommand>();
+
+            Forward<EnvironmentListCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "environment" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Manipulate inferior environment variables."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Manipulates environment variables.\n" +
+                       "\n" +
+                       "Environment variables set with these commands are passed on to inferior\n" +
+                       "processes launched locally.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/FrameCommand.cs b/src/Commands/FrameCommand.cs
new file mode 100644
index 0000000..116cb89
--- /dev/null
+++ b/src/Commands/FrameCommand.cs
@@ -0,0 +1,439 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+using Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class FrameCommand : MultiCommand
+    {
+        sealed class FrameArgumentsCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "arguments", "args" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Show arguments in the current stack frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame arguments|args"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all arguments and their values for the current frame.\n" +
+                           "\n" +
+                           "The 'this' reference is included if available.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    // Not really an error since it's a perfectly
+                    // valid state for e.g. the finalizer thread.
+                    if (Debugger.State != State.Exited)
+                        Log.Info("Backtrace for this thread is unavailable");
+                    else
+                        Log.Error("No active stack frame");
+
+                    return;
+                }
+
+                var vals = new[] { f.GetThisReference() }.Concat(f.GetParameters()).Where(x => x != null);
+
+                if (!vals.Any())
+                {
+                    Log.Info("No arguments");
+                    return;
+                }
+
+                foreach (var val in vals)
+                {
+                    val.WaitHandle.WaitOne();
+
+                    var strErr = Utilities.StringizeValue(val);
+
+                    if (strErr.Item2)
+                        Log.Error("{0}<error>{1} {2} = {3}", Color.DarkRed, Color.Reset,
+                                  val.Name, strErr.Item1);
+                    else
+                        Log.Info("{0}{1}{2} {3} = {4}", Color.DarkGreen, val.TypeName,
+                                 Color.Reset, val.Name, strErr.Item1);
+                }
+            }
+        }
+
+        sealed class FrameDownCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "down" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Go down the call stack by one frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame down"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the active frame to the frame below the current frame.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var bt = Debugger.ActiveBacktrace;
+
+                if (bt == null)
+                {
+                    Log.Error("No active backtrace");
+                    return;
+                }
+
+                if (bt.FrameCount == 0)
+                {
+                    Log.Info("Backtrace for this thread is unavailable");
+                    return;
+                }
+
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    Log.Error("No active stack frame");
+                    return;
+                }
+
+                var f2 = bt.GetFrame(f.Index - 1);
+
+                if (f2 == f)
+                {
+                    Log.Error("Cannot go further down the stack");
+                    return;
+                }
+
+                Debugger.ActiveFrame = f2;
+
+                Log.Emphasis(Utilities.StringizeFrame(f2, true));
+            }
+        }
+
+        sealed class FrameGetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "get" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Show the current stack frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame get"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Shows the currently active frame, along with its source location and\n" +
+                           "and source line.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    // Not really an error since it's a perfectly
+                    // valid state for e.g. the finalizer thread.
+                    if (Debugger.State != State.Exited)
+                        Log.Info("Backtrace for this thread is unavailable");
+                    else
+                        Log.Error("No active stack frame");
+
+                    return;
+                }
+
+                Log.Emphasis(Utilities.StringizeFrame(f, true));
+            }
+        }
+
+        sealed class FrameLocalsCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "locals", "variables", "vars" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Show local variables in the current stack frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame locals|variables|vars"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all local variables and their values in the current frame.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    // Not really an error since it's a perfectly
+                    // valid state for e.g. the finalizer thread.
+                    if (Debugger.State != State.Exited)
+                        Log.Info("Backtrace for this thread is unavailable");
+                    else
+                        Log.Error("No active stack frame");
+
+                    return;
+                }
+
+                var vals = f.GetLocalVariables();
+
+                if (vals.Length == 0)
+                {
+                    Log.Info("No locals");
+                    return;
+                }
+
+                foreach (var val in vals)
+                {
+                    val.WaitHandle.WaitOne();
+
+                    var strErr = Utilities.StringizeValue(val);
+
+                    if (strErr.Item2)
+                        Log.Error("{0}<error>{1} {2} = {3}", Color.DarkRed, Color.Reset,
+                                  val.Name, strErr.Item1);
+                    else
+                        Log.Info("{0}{1}{2} {3} = {4}", Color.DarkGreen, val.TypeName,
+                                 Color.Reset, val.Name, strErr.Item1);
+                }
+            }
+        }
+
+        sealed class FrameSetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "set" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Switch to specific stack frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame set <num>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the current frame to the given number." +
+                           "\n" +
+                           "The numbers shown in a 'backtrace' can be passed to this command.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var bt = Debugger.ActiveBacktrace;
+
+                if (bt == null)
+                {
+                    Log.Error("No active backtrace");
+                    return;
+                }
+
+                if (bt.FrameCount == 0)
+                {
+                    // Not really an error since it's a perfectly
+                    // valid state for e.g. the finalizer thread.
+                    if (Debugger.State != State.Exited)
+                        Log.Info("Backtrace for this thread is unavailable");
+                    else
+                        Log.Error("No active stack frame");
+
+                    return;
+                }
+
+                int num;
+
+                if (!int.TryParse(args, out num))
+                {
+                    Log.Error("Invalid frame number");
+                    return;
+                }
+
+                if (num < 0 || num > bt.FrameCount - 1)
+                {
+                    Log.Error("Frame number is out of bounds (0 .. {0})", bt.FrameCount - 1);
+                    return;
+                }
+
+                var f = bt.GetFrame(num);
+
+                Debugger.ActiveFrame = f;
+
+                Log.Emphasis(Utilities.StringizeFrame(f, true));
+            }
+        }
+
+        sealed class FrameUpCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "up" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Go up the call stack by one frame."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "frame up"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the active frame to the frame above the current frame.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var bt = Debugger.ActiveBacktrace;
+
+                if (bt == null)
+                {
+                    Log.Error("No active backtrace");
+                    return;
+                }
+
+                if (bt.FrameCount == 0)
+                {
+                    Log.Info("Backtrace for this thread is unavailable");
+                    return;
+                }
+
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    Log.Error("No active stack frame");
+                    return;
+                }
+
+                var f2 = bt.GetFrame(f.Index + 1);
+
+                if (f2 == f)
+                {
+                    Log.Error("Cannot go further up the stack");
+                    return;
+                }
+
+                Debugger.ActiveFrame = f2;
+
+                Log.Emphasis(Utilities.StringizeFrame(f2, true));
+            }
+        }
+
+        public FrameCommand()
+        {
+            AddCommand<FrameArgumentsCommand>();
+            AddCommand<FrameDownCommand>();
+            AddCommand<FrameGetCommand>();
+            AddCommand<FrameLocalsCommand>();
+            AddCommand<FrameSetCommand>();
+            AddCommand<FrameUpCommand>();
+
+            Forward<FrameGetCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "frame" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Get, set, and inspect stack frames."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Interacts with the call stack.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/HelpCommand.cs b/src/Commands/HelpCommand.cs
new file mode 100644
index 0000000..f700892
--- /dev/null
+++ b/src/Commands/HelpCommand.cs
@@ -0,0 +1,118 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class HelpCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "help" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Describe available commands."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "help [cmd] [sub-cmd] [...]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Prints detailed help for the given (sub-)command.\n" +
+                       "\n" +
+                       "If the command is a multi-command, its sub-commands are printed in addition\n" +
+                       "to its detailed help text.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            var names = args.Split(' ').Where(x => x != string.Empty).ToArray();
+            var fullName = string.Empty;
+            Command cmd = CommandLine.Root;
+
+            foreach (var name in names)
+            {
+                var mcmd = cmd as MultiCommand;
+
+                if (mcmd == null)
+                    break;
+
+                var scmd = mcmd.GetCommand(name);
+
+                if (scmd == null)
+                {
+                    if (cmd is RootCommand)
+                        Log.Error("'{0}' is not a known command", name);
+                    else
+                        Log.Error("No sub-command '{0}' under '{1}'", name, fullName);
+
+                    return;
+                }
+                else
+                {
+                    cmd = scmd;
+
+                    if (fullName != string.Empty)
+                        fullName += ' ';
+
+                    fullName += name;
+                }
+            }
+
+            if (!(cmd is RootCommand))
+            {
+                Log.Info(string.Empty);
+                Log.Emphasis("  {0}", cmd.Syntax);
+            }
+
+            Log.Info(string.Empty);
+            Log.Info(cmd.Help);
+            Log.Info(string.Empty);
+
+            var mcmd2 = cmd as MultiCommand;
+
+            if (mcmd2 != null)
+            {
+
+                foreach (var sub in mcmd2.Commands.Select(x => x.Item2).Distinct())
+                {
+                    Log.Emphasis("  {0}", string.Join("|", sub.Names));
+                    Log.Info("    {0}", sub.Summary);
+                    Log.Info(string.Empty);
+                }
+            }
+        }
+    }
+}
diff --git a/src/Commands/KillCommand.cs b/src/Commands/KillCommand.cs
new file mode 100644
index 0000000..f24c29a
--- /dev/null
+++ b/src/Commands/KillCommand.cs
@@ -0,0 +1,65 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class KillCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "kill" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Kill the inferior process."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "kill|stop"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Kills the current inferior process, regardless of its state.\n" +
+                       "\n" +
+                       "For remote debugging sessions, this also makes the remote virtual machine\n" +
+                       "exit immediately.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State == State.Exited)
+                Log.Error("No inferior process");
+            else
+                Debugger.Kill();
+        }
+    }
+}
diff --git a/src/Commands/ListenCommand.cs b/src/Commands/ListenCommand.cs
new file mode 100644
index 0000000..72e18be
--- /dev/null
+++ b/src/Commands/ListenCommand.cs
@@ -0,0 +1,101 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+using System.Net;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ListenCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "listen", "wait" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Listen for a remote virtual machine."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "listen|wait <addr> <port>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Listens for a remote debuggee connection on the given IP address and port.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State != State.Exited)
+            {
+                Log.Error("An inferior process is already being debugged");
+                return;
+            }
+
+            var ip = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+            if (ip == null)
+            {
+                Log.Error("No IP address given");
+                return;
+            }
+
+            IPAddress addr;
+
+            if (!IPAddress.TryParse(ip, out addr))
+            {
+                Log.Error("Invalid IP address");
+                return;
+            }
+
+            var rest = new string(args.Skip(ip.Length).ToArray()).Trim();
+
+            if (rest.Length == 0)
+            {
+                Log.Error("No port number given");
+                return;
+            }
+
+            int port;
+
+            if (!int.TryParse(rest, out port) || port <= 0)
+            {
+                Log.Error("Invalid port number");
+                return;
+            }
+
+            Debugger.Listen(addr, port);
+
+            Log.Info("Listening on '{0}:{1}'...", addr, port);
+        }
+    }
+}
diff --git a/src/Commands/PluginCommand.cs b/src/Commands/PluginCommand.cs
new file mode 100644
index 0000000..9afb97b
--- /dev/null
+++ b/src/Commands/PluginCommand.cs
@@ -0,0 +1,66 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.IO;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class PluginCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "plugin", "addin" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Load a plugin assembly."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "plugin|addin <file>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Attempts to load the given plugin assembly. All types that are tagged with\n" +
+                       "the '[Command]' attribute will be instantiated and made available.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (!File.Exists(args))
+            {
+                Log.Error("Plugin assembly '{0}' does not exist");
+                return;
+            }
+
+            CommandLine.Root.AddCommands(Plugins.Load(args));
+        }
+    }
+}
diff --git a/src/Commands/PrintCommand.cs b/src/Commands/PrintCommand.cs
new file mode 100644
index 0000000..a045b53
--- /dev/null
+++ b/src/Commands/PrintCommand.cs
@@ -0,0 +1,92 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class PrintCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "print", "evaluate" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Print (evaluate) a given expression."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "print|evaluate <expr>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Evaluates the given expression in the context of the active stack frame.\n" +
+                       "The expression has access to any local variables and method arguments that\n" +
+                       "are in scope.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            var f = Debugger.ActiveFrame;
+
+            if (f == null)
+            {
+                Log.Error("No active stack frame");
+                return;
+            }
+
+            if (args.Length == 0)
+            {
+                Log.Error("No expression given");
+                return;
+            }
+
+            if (!f.ValidateExpression(args))
+            {
+                Log.Error("Expression '{0}' is invalid", args);
+                return;
+            }
+
+            var val = f.GetExpressionValue(args, Debugger.Options.EvaluationOptions);
+            val.WaitHandle.WaitOne();
+
+            var strErr = Utilities.StringizeValue(val);
+
+            if (strErr.Item2)
+            {
+                Log.Error(strErr.Item1);
+                return;
+            }
+
+            Log.Info("{0}{1}{2} it = {3}", Color.DarkGreen, val.TypeName, Color.Reset, strErr.Item1);
+        }
+    }
+}
diff --git a/src/Commands/QuitCommand.cs b/src/Commands/QuitCommand.cs
new file mode 100644
index 0000000..3852448
--- /dev/null
+++ b/src/Commands/QuitCommand.cs
@@ -0,0 +1,67 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class QuitCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "quit", "bye", "exit" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Exit the debugger."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "quit|bye|exit [!]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Exits the debugger. If the '!' argument is given, any active inferior\n" +
+                       "process will be killed; otherwise, the command will refuse to quit if an\n" +
+                       "inferior process is active.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State != State.Exited && !args.StartsWith("!"))
+            {
+                Log.Error("An inferior process is active");
+                return;
+            }
+
+            CommandLine.Stop = true;
+        }
+    }
+}
diff --git a/src/Commands/ResetCommand.cs b/src/Commands/ResetCommand.cs
new file mode 100644
index 0000000..4bad1fd
--- /dev/null
+++ b/src/Commands/ResetCommand.cs
@@ -0,0 +1,72 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ResetCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "reset" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Reset inferior environment variables, arguments, etc."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "reset"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Resets the following debugger state to defaults:\n" +
+                       "\n" +
+                       "* Working directory.\n" +
+                       "* Program arguments.\n" +
+                       "* Environment variables.\n" +
+                       "* Watches.\n" +
+                       "* Breakpoints and catchpoints.\n" +
+                       "* Session options.\n" +
+                       "* Evaluation options.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            Debugger.ResetState();
+            Debugger.ResetOptions();
+
+            Configuration.Apply();
+
+            Log.Info("All debugger state reset");
+        }
+    }
+}
diff --git a/src/Commands/RootCommand.cs b/src/Commands/RootCommand.cs
new file mode 100644
index 0000000..0c32c13
--- /dev/null
+++ b/src/Commands/RootCommand.cs
@@ -0,0 +1,99 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class RootCommand : MultiCommand
+    {
+        public RootCommand()
+        {
+            AddCommandWithName<PrintCommand>("p");
+            AddCommand<AttachCommand>();
+            AddCommandWithName<BreakpointCommand>("b");
+            AddCommand<BacktraceCommand>();
+            AddCommand<BreakpointCommand>();
+            AddCommandWithName<ContinueCommand>("c");
+            AddCommand<CatchpointCommand>();
+            AddCommand<ConfigCommand>();
+            AddCommandWithName<RunCommand>("r");
+            AddCommand<ConnectCommand>();
+            AddCommand<ContinueCommand>();
+            AddCommand<DatabaseCommand>();
+            AddCommand<DecompileCommand>();
+            AddCommand<DirectoryCommand>();
+            AddCommand<DisassembleCommand>();
+            AddCommand<EnvironmentCommand>();
+            AddCommand<FrameCommand>();
+            AddCommand<HelpCommand>();
+            AddCommand<KillCommand>();
+            AddCommand<ListenCommand>();
+            AddCommand<PluginCommand>();
+            AddCommandWithName<PrintCommand>("p");
+            AddCommand<PrintCommand>();
+            AddCommand<QuitCommand>();
+            AddCommandWithName<RunCommand>("r");
+            AddCommand<ResetCommand>();
+            AddCommand<RunCommand>();
+            AddCommandWithName<StepCommand>("s");
+            AddCommand<SourceCommand>();
+            AddCommand<StepCommand>();
+            AddCommand<ThreadCommand>();
+            AddCommand<WatchCommand>();
+        }
+
+        public void AddCommands(IEnumerable<Command> commands)
+        {
+            foreach (var cmd in commands)
+                AddCommand(cmd);
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "root" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "N/A"; }
+        }
+
+        public override string Syntax
+        {
+            get { return "N/A"; }
+        }
+
+        public override string Help
+        {
+            get { return "All commands (and sub-commands) can be abbreviated.\n" +
+                         "For example, 'exe' means 'execute' and 'co s' means 'config set'."; }
+        }
+
+        protected override void ProcessFallback(string args, bool invalidSubCommand)
+        {
+            Log.Error("'{0}' is not a known command", args);
+        }
+    }
+}
diff --git a/src/Commands/RunCommand.cs b/src/Commands/RunCommand.cs
new file mode 100644
index 0000000..95d7316
--- /dev/null
+++ b/src/Commands/RunCommand.cs
@@ -0,0 +1,100 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class RunCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "run", "execute", "launch" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Run a virtual machine locally."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "run|execute|launch <path>"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Runs an inferior process locally. The argument must be the path to the\n" +
+                       "executable file (i.e. '.exe').\n" +
+                       "\n" +
+                       "The following debugger state is taken into account:\n" +
+                       "\n" +
+                       "* Working directory.\n" +
+                       "* Program arguments.\n" +
+                       "* Environment variables.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            if (Debugger.State != State.Exited)
+            {
+                Log.Error("An inferior process is already being debugged");
+                return;
+            }
+
+            if (args.Length == 0)
+            {
+                Log.Error("No program path given");
+                return;
+            }
+
+            if (!File.Exists(args))
+            {
+                Log.Error("Program executable '{0}' does not exist", args);
+                return;
+            }
+
+            FileInfo file;
+
+            try
+            {
+                file = new FileInfo(args);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not open file '{0}':", args);
+                Log.Error(ex.ToString());
+
+                return;
+            }
+
+            Debugger.Run(file);
+        }
+    }
+}
diff --git a/src/Commands/SourceCommand.cs b/src/Commands/SourceCommand.cs
new file mode 100644
index 0000000..2f19bf7
--- /dev/null
+++ b/src/Commands/SourceCommand.cs
@@ -0,0 +1,160 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class SourceCommand : Command
+    {
+        public override string[] Names
+        {
+            get { return new[] { "source", "src" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Show the source for the current stack frame."; }
+        }
+
+        public override string Syntax
+        {
+            get { return "source|src [lower] [upper]"; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Prints the source for the current stack frame and highlights the current\n" +
+                       "line.\n" +
+                       "\n" +
+                       "If arguments are given, they specify how many lines to print before and\n" +
+                       "after the current line.";
+            }
+        }
+
+        public override void Process(string args)
+        {
+            var f = Debugger.ActiveFrame;
+
+            if (f == null)
+            {
+                Log.Error("No active stack frame");
+                return;
+            }
+
+            var lower = 10;
+            var upper = 10;
+
+            var lowerStr = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+            if (lowerStr != null)
+            {
+                if (!int.TryParse(lowerStr, out lower))
+                {
+                    Log.Error("Invalid lower bound value");
+                    return;
+                }
+
+                lower = System.Math.Abs(lower);
+
+                var upperStr = new string(args.Skip(lowerStr.Length).ToArray()).Trim();
+
+                if (upperStr.Length != 0)
+                {
+                    if (!int.TryParse(upperStr, out upper))
+                    {
+                        Log.Error("Invalid upper bound value");
+                        return;
+                    }
+                }
+            }
+
+            var loc = f.SourceLocation;
+            var file = loc.FileName;
+            var line = loc.Line;
+
+            if (file != null && line != -1)
+            {
+                if (!File.Exists(file))
+                {
+                    Log.Error("Source file '{0}' not found", file);
+                    return;
+                }
+
+                StreamReader reader;
+
+                try
+                {
+                    reader = File.OpenText(file);
+                }
+                catch (Exception ex)
+                {
+                    Log.Error("Could not open source file '{0}'", file);
+                    Log.Error(ex.ToString());
+
+                    return;
+                }
+
+                try
+                {
+                    var exec = Debugger.CurrentExecutable;
+
+                    if (exec != null && File.GetLastWriteTime(file) > exec.LastWriteTime)
+                        Log.Notice("Source file '{0}' is newer than the debuggee executable", file);
+
+                    var cur = 0;
+
+                    while (!reader.EndOfStream)
+                    {
+                        var str = reader.ReadLine();
+
+                        var i = line - cur;
+                        var j = cur - line;
+
+                        if (i > 0 && i < lower + 2 || j >= 0 && j < upper)
+                        {
+                            if (cur == line - 1)
+                                Log.Emphasis(str);
+                            else
+                                Log.Info(str);
+                        }
+
+                        cur++;
+                    }
+                }
+                finally
+                {
+                    reader.Dispose();
+                }
+            }
+            else
+                Log.Error("No source information available");
+        }
+    }
+}
diff --git a/src/Commands/StepCommand.cs b/src/Commands/StepCommand.cs
new file mode 100644
index 0000000..d2087bb
--- /dev/null
+++ b/src/Commands/StepCommand.cs
@@ -0,0 +1,307 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class StepCommand : MultiCommand
+    {
+        sealed class StepOverCommand : MultiCommand
+        {
+            sealed class StepOverLineCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "line" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Step over a line."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "step over line"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Steps over a line.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    if (Debugger.State == State.Suspended)
+                        Debugger.StepOverLine();
+                    else
+                        Log.Error("No suspended inferior process");
+                }
+            }
+
+            sealed class StepOverInstructionCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "instruction", "insn" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Step over an instruction."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "step over instruction|insn"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Steps over an instruction.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    if (Debugger.State == State.Suspended)
+                        Debugger.StepOverInstruction();
+                    else
+                        Log.Error("No suspended inferior process");
+                }
+            }
+
+            public StepOverCommand()
+            {
+                AddCommand<StepOverLineCommand>();
+                AddCommand<StepOverInstructionCommand>();
+
+                Forward<StepOverLineCommand>();
+            }
+
+            public override string[] Names
+            {
+                get { return new[] { "over" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Step over a line or an instruction."; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Steps over a line or an instruction.\n" +
+                           "\n" +
+                           "This executes the line or instruction that's next in the source code or\n" +
+                           "IL stream. If the line or instruction contains a call, the debugger\n" +
+                           "will treat it as a single unit, as opposed to 'step into'.";
+                }
+            }
+
+            public override string Parent
+            {
+                get { return "step"; }
+            }
+        }
+
+        sealed class StepIntoCommand : MultiCommand
+        {
+            sealed class StepIntoLineCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "line" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Step into a line."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "step into line"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Steps into a line.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    if (Debugger.State == State.Suspended)
+                        Debugger.StepIntoLine();
+                    else
+                        Log.Error("No suspended inferior process");
+                }
+            }
+
+            sealed class StepIntoInstructionCommand : Command
+            {
+                public override string[] Names
+                {
+                    get { return new[] { "instruction", "insn" }; }
+                }
+
+                public override string Summary
+                {
+                    get { return "Step into an instruction."; }
+                }
+
+                public override string Syntax
+                {
+                    get { return "step into instruction|insn"; }
+                }
+
+                public override string Help
+                {
+                    get
+                    {
+                        return "Steps into an instruction.";
+                    }
+                }
+
+                public override void Process(string args)
+                {
+                    if (Debugger.State == State.Suspended)
+                        Debugger.StepIntoInstruction();
+                    else
+                        Log.Error("No suspended inferior process");
+                }
+            }
+
+            public StepIntoCommand()
+            {
+                AddCommand<StepIntoLineCommand>();
+                AddCommand<StepIntoInstructionCommand>();
+
+                Forward<StepIntoLineCommand>();
+            }
+
+            public override string[] Names
+            {
+                get { return new[] { "into" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Step into a line or an instruction."; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Steps into a line or an instruction.\n" +
+                           "\n" +
+                           "This executes the line or instruction that's next in the source code or\n" +
+                           "IL stream. If the line or instruction contains a call, the debugger\n" +
+                           "will pause at the first line or instruction inside the callee.";
+                }
+            }
+
+            public override string Parent
+            {
+                get { return "step"; }
+            }
+        }
+
+        sealed class StepOutCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "out" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Step out of the current method."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "step out"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Steps out of the current method.\n" +
+                           "\n" +
+                           "This resumes the debuggee until it exits the method it's currently in.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                if (Debugger.State == State.Suspended)
+                    Debugger.StepOutOfMethod();
+                else
+                    Log.Error("No suspended inferior process");
+            }
+        }
+
+        public StepCommand()
+        {
+            AddCommandWithName<StepOverCommand>("o");
+            AddCommand<StepOverCommand>();
+            AddCommand<StepIntoCommand>();
+            AddCommand<StepOutCommand>();
+
+            Forward<StepOverCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "step" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Single-step through lines/instructions/methods."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Single-steps through lines, instructions, and methods.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/ThreadCommand.cs b/src/Commands/ThreadCommand.cs
new file mode 100644
index 0000000..20993a3
--- /dev/null
+++ b/src/Commands/ThreadCommand.cs
@@ -0,0 +1,295 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class ThreadCommand : MultiCommand
+    {
+        sealed class ThreadBacktraceCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "backtrace", "bt" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Print backtraces for all threads."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "thread backtrace|bt"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Prints a backtrace for all program threads.\n" +
+                           "\n" +
+                           "Functionally equivalent to applying 'backtrace' to all threads while\n" +
+                           "switching through them with 'thread set'.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var p = Debugger.ActiveProcess;
+
+                if (p == null)
+                {
+                    Log.Error("No active inferior process");
+                    return;
+                }
+
+                var threads = p.GetThreads();
+
+                for (var i = 0; i < threads.Length; i++)
+                {
+                    var t = threads[i];
+                    var str = Utilities.StringizeThread(t, false);
+
+                    if (t == Debugger.ActiveThread)
+                        Log.Emphasis(str);
+                    else
+                        Log.Info(str);
+
+                    var bt = t.Backtrace;
+
+                    if (bt.FrameCount != 0)
+                    {
+                        for (var j = 0; j < bt.FrameCount; j++)
+                        {
+                            var f = bt.GetFrame(j);
+                            var fstr = Utilities.StringizeFrame(f, true);
+
+                            if (f == Debugger.ActiveFrame)
+                                Log.Emphasis(fstr);
+                            else
+                                Log.Info(fstr);
+                        }
+                    }
+                    else
+                        Log.Info("Backtrace for this thread is unavailable");
+
+                    if (i < threads.Length - 1)
+                        Log.Info(string.Empty);
+                }
+            }
+        }
+
+        sealed class ThreadGetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "get" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Show the currently active thread."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "thread get"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Gets the currently active thread.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var t = Debugger.ActiveThread;
+
+                if (t == null)
+                {
+                    Log.Error("No active thread");
+                    return;
+                }
+
+                var str = Utilities.StringizeThread(t, true);
+
+                if (t == Debugger.ActiveThread)
+                    Log.Emphasis(str);
+                else
+                    Log.Info(str);
+            }
+        }
+
+        sealed class ThreadListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all program threads."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "thread list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all program threads, along with their IDs and names.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var p = Debugger.ActiveProcess;
+
+                if (p == null)
+                {
+                    Log.Error("No active inferior process");
+                    return;
+                }
+
+                var threads = p.GetThreads();
+
+                for (var i = 0; i < threads.Length; i++)
+                {
+                    var t = threads[i];
+                    var str = Utilities.StringizeThread(t, true);
+
+                    if (t == Debugger.ActiveThread)
+                        Log.Emphasis(str);
+                    else
+                        Log.Info(str);
+
+                    if (i < threads.Length - 1)
+                        Log.Info(string.Empty);
+                }
+            }
+        }
+
+        sealed class ThreadSetCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "set" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Set currently active thread."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "thread set <id>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Sets the currently active thread to the given thread ID.\n" +
+                           "\n" +
+                           "All following commands that somehow interact with the program's call\n" +
+                           "stack will happen on the specified thread.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var p = Debugger.ActiveProcess;
+
+                if (p == null)
+                {
+                    Log.Error("No active inferior process");
+                    return;
+                }
+
+                int num;
+
+                if (!int.TryParse(args, out num) || num < 0)
+                {
+                    Log.Error("Invalid thread ID");
+                    return;
+                }
+
+                var threads = p.GetThreads();
+
+                foreach (var t in threads)
+                {
+                    if (t.Id == num)
+                    {
+                        t.SetActive();
+                        Debugger.ActiveFrame = null;
+
+                        Log.Emphasis(Utilities.StringizeThread(t, true));
+
+                        return;
+                    }
+                }
+
+                Log.Error("Thread ID '{0}' not found", num);
+            }
+        }
+
+        public ThreadCommand()
+        {
+            AddCommand<ThreadBacktraceCommand>();
+            AddCommand<ThreadGetCommand>();
+            AddCommand<ThreadListCommand>();
+            AddCommand<ThreadSetCommand>();
+
+            Forward<ThreadGetCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "thread" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Get, set, and inspect program threads."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Interacts with program threads.";
+            }
+        }
+    }
+}
diff --git a/src/Commands/WatchCommand.cs b/src/Commands/WatchCommand.cs
new file mode 100644
index 0000000..c6e2258
--- /dev/null
+++ b/src/Commands/WatchCommand.cs
@@ -0,0 +1,256 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client.Commands
+{
+    sealed class WatchCommand : MultiCommand
+    {
+        sealed class WatchAddCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "add" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Add a watch expression."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "watch add <expr>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Adds a watch with the given expression.\n" +
+                           "\n" +
+                           "Watches and their values can be shown with the 'watch list' command.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var id = Debugger.GetWatchId();
+
+                Debugger.Watches.Add(id, args);
+
+                Log.Info("Added watch '{0}' with expression '{1}'", id, args);
+            }
+        }
+
+        sealed class WatchClearCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "clear" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Delete all watches."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "watch clear"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Clears all active watches.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                Debugger.Watches.Clear();
+
+                Log.Info("All watches cleared");
+            }
+        }
+
+        sealed class WatchDeleteCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "delete", "remove" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "Delete a watch by ID."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "watch delete|remove <id>"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Deletes the watch with the specified ID, if it exists.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                long num;
+
+                if (!long.TryParse(args, out num))
+                {
+                    Log.Error("Invalid watch ID");
+                    return;
+                }
+
+                string expr;
+
+                if (!Debugger.Watches.TryGetValue(num, out expr))
+                {
+                    Log.Error("Watch '{0}' not found", num);
+                    return;
+                }
+
+                Debugger.Watches.Remove(num);
+
+                Log.Info("Watch '{0}' (with expression '{1}') deleted", num, expr);
+            }
+        }
+
+        sealed class WatchListCommand : Command
+        {
+            public override string[] Names
+            {
+                get { return new[] { "list" }; }
+            }
+
+            public override string Summary
+            {
+                get { return "List all set watches and their IDs."; }
+            }
+
+            public override string Syntax
+            {
+                get { return "watch list"; }
+            }
+
+            public override string Help
+            {
+                get
+                {
+                    return "Lists all watches, along with their IDs, expressions, and values.";
+                }
+            }
+
+            public override void Process(string args)
+            {
+                var f = Debugger.ActiveFrame;
+
+                if (f == null)
+                {
+                    Log.Error("No active stack frame");
+                    return;
+                }
+
+                if (Debugger.Watches.Count == 0)
+                {
+                    Log.Info("No watches");
+                    return;
+                }
+
+                foreach (var pair in Debugger.Watches)
+                {
+                    ObjectValue obj = null;
+                    string value;
+                    bool error;
+
+                    if (!f.ValidateExpression(pair.Value))
+                    {
+                        value = "Expression is invalid";
+                        error = true;
+                    }
+                    else
+                    {
+                        obj = f.GetExpressionValue(pair.Value, Debugger.Options.EvaluationOptions);
+                        obj.WaitHandle.WaitOne();
+
+                        var strErr = Utilities.StringizeValue(obj);
+
+                        value = strErr.Item1;
+                        error = strErr.Item2;
+                    }
+
+                    var prefix = string.Format("#{0} '{1}': ", pair.Key, pair.Value);
+
+                    if (error)
+                        Log.Error("{0}{1}", prefix, value);
+                    else
+                        Log.Info("{0}{1}{2}{3} it = {4}", prefix, Color.DarkGreen, obj.TypeName, Color.Reset, value);
+                }
+            }
+        }
+
+        public WatchCommand()
+        {
+            AddCommand<WatchAddCommand>();
+            AddCommand<WatchClearCommand>();
+            AddCommand<WatchDeleteCommand>();
+            AddCommand<WatchListCommand>();
+
+            Forward<WatchListCommand>();
+        }
+
+        public override string[] Names
+        {
+            get { return new[] { "watch" }; }
+        }
+
+        public override string Summary
+        {
+            get { return "Add, delete, and show watches."; }
+        }
+
+        public override string Help
+        {
+            get
+            {
+                return "Manipulates watches.\n" +
+                       "\n" +
+                       "Watches are simply expressions that can be saved and queried at any point\n" +
+                       "when the debuggee is paused. They are useful for easily observing global\n" +
+                       "program state.";
+            }
+        }
+    }
+}
diff --git a/src/Configuration.cs b/src/Configuration.cs
new file mode 100644
index 0000000..37134f7
--- /dev/null
+++ b/src/Configuration.cs
@@ -0,0 +1,199 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.IO;
+using System.Reflection;
+using System.Runtime.Serialization.Formatters.Binary;
+using Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client
+{
+    [Serializable]
+    public sealed class Configuration
+    {
+        public bool AllowMethodEvaluation { get; set; }
+
+        public bool AllowTargetInvoke { get; set; }
+
+        public bool AllowToStringCalls { get; set; }
+
+        public bool ChunkRawStrings { get; set; }
+
+        public int ConnectionAttemptInterval { get; set; }
+
+        public bool DebugLogging { get; set; }
+
+        public bool DisableColors { get; set; }
+
+        public bool EllipsizeStrings { get; set; }
+
+        public int EllipsizeThreshold { get; set; }
+
+        public bool EnableControlC { get; set; }
+
+        public int EvaluationTimeout { get; set; }
+
+        public string ExceptionIdentifier { get; set; }
+
+        public bool FlattenHierarchy { get; set; }
+
+        public bool HexadecimalIntegers { get; set; }
+
+        public string InputPrompt { get; set; }
+
+        public bool LogInternalErrors { get; set; }
+
+        public bool LogRuntimeSpew { get; set; }
+
+        public int MaxConnectionAttempts { get; set; }
+
+        public int MemberEvaluationTimeout { get; set; }
+
+        public string RuntimePrefix { get; set; }
+
+        public bool StepOverPropertiesAndOperators { get; set; }
+
+        public static Configuration Current { get; private set; }
+
+        static Configuration()
+        {
+            Current = new Configuration();
+        }
+
+        static string GetFilePath()
+        {
+            var cfg = Environment.GetEnvironmentVariable("SDB_CFG");
+
+            if (cfg != null)
+                return cfg == string.Empty ? null : cfg;
+
+            var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+            return Path.Combine(home, ".sdb.cfg");
+        }
+
+        public static void Write()
+        {
+            var file = GetFilePath();
+
+            if (file == null)
+                return;
+
+            try
+            {
+                using (var stream = new FileStream(file, FileMode.Create, FileAccess.Write))
+                    new BinaryFormatter().Serialize(stream, Current);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not write configuration file '{0}':", file);
+                Log.Error(ex.ToString());
+            }
+        }
+
+        public static bool Read()
+        {
+            var file = GetFilePath();
+
+            if (file == null)
+                return false;
+
+            try
+            {
+                using (var stream = new FileStream(file, FileMode.Open, FileAccess.Read))
+                    Current = (Configuration)new BinaryFormatter().Deserialize(stream);
+            }
+            catch (Exception ex)
+            {
+                // If it's an FNFE, chances are the file just
+                // hasn't been written yet.
+                if (!(ex is FileNotFoundException))
+                {
+                    Log.Error("Could not read configuration file '{0}':", file);
+                    Log.Error(ex.ToString());
+                }
+
+                return false;
+            }
+
+            return true;
+        }
+
+        public static void Defaults()
+        {
+            // Cute hack to set all properties to their default values.
+            foreach (var prop in typeof(Configuration).GetProperties(BindingFlags.Public |
+                                                                     BindingFlags.Instance))
+                prop.SetValue(Configuration.Current, null);
+
+            Current.AllowMethodEvaluation = true;
+            Current.AllowTargetInvoke = true;
+            Current.AllowToStringCalls = true;
+            Current.ConnectionAttemptInterval = 500;
+            Current.EllipsizeStrings = true;
+            Current.EllipsizeThreshold = 100;
+            Current.EnableControlC = true;
+            Current.EvaluationTimeout = 1000;
+            Current.ExceptionIdentifier = "$exception";
+            Current.FlattenHierarchy = true;
+            Current.InputPrompt = "(sdb)";
+            Current.MaxConnectionAttempts = 1;
+            Current.MemberEvaluationTimeout = 5000;
+            Current.RuntimePrefix = "/usr";
+            Current.StepOverPropertiesAndOperators = true;
+        }
+
+        public static void Apply()
+        {
+            // We can only apply a limited set of options here since some
+            // are set at session creation time.
+
+            var opt = Debugger.Options;
+
+            opt.StepOverPropertiesAndOperators = Current.StepOverPropertiesAndOperators;
+
+            var eval = opt.EvaluationOptions;
+
+            eval.AllowMethodEvaluation = Current.AllowMethodEvaluation;
+            eval.AllowTargetInvoke = Current.AllowTargetInvoke;
+            eval.AllowToStringCalls = Current.AllowToStringCalls;
+            eval.ChunkRawStrings = Current.ChunkRawStrings;
+            eval.CurrentExceptionTag = Current.ExceptionIdentifier;
+            eval.EllipsizeStrings = Current.EllipsizeStrings;
+            eval.EllipsizedLength = Current.EllipsizeThreshold;
+            eval.EvaluationTimeout = Current.EvaluationTimeout;
+            eval.FlattenHierarchy = Current.FlattenHierarchy;
+            eval.IntegerDisplayFormat = Current.HexadecimalIntegers ?
+                                        IntegerDisplayFormat.Hexadecimal :
+                                        IntegerDisplayFormat.Decimal;
+            eval.MemberEvaluationTimeout = Current.MemberEvaluationTimeout;
+
+            if (Current.EnableControlC)
+                CommandLine.SetControlCHandler();
+            else
+                CommandLine.UnsetControlCHandler();
+        }
+    }
+}
diff --git a/src/CustomLogger.cs b/src/CustomLogger.cs
new file mode 100644
index 0000000..8e38dc5
--- /dev/null
+++ b/src/CustomLogger.cs
@@ -0,0 +1,50 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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 Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client
+{
+    sealed class CustomLogger : ICustomLogger
+    {
+        public void LogError(string message, Exception ex)
+        {
+            Log.Error(message);
+
+            if (ex != null)
+                Log.Error(ex.ToString());
+        }
+
+        public void LogAndShowException(string message, Exception ex)
+        {
+            LogError(message, ex);
+        }
+
+        public void LogMessage(string format, params object[] args)
+        {
+            Log.Info(format, args);
+        }
+    }
+}
diff --git a/src/Debugger.cs b/src/Debugger.cs
new file mode 100644
index 0000000..721274b
--- /dev/null
+++ b/src/Debugger.cs
@@ -0,0 +1,702 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Runtime.Serialization.Formatters.Binary;
+using System.Threading;
+using Mono.Debugging.Client;
+using Mono.Debugging.Soft;
+
+namespace Mono.Debugger.Client
+{
+    public static class Debugger
+    {
+        static readonly object _lock = new object();
+
+        static Debugger()
+        {
+            EnsureCreated();
+            ResetOptions();
+            ResetState();
+
+            _debuggeeKilled = true;
+
+            DebuggerLoggingService.CustomLogger = new CustomLogger();
+        }
+
+        static SoftDebuggerSession _session;
+
+        public static FileInfo CurrentExecutable { get; private set; }
+
+        public static IPAddress CurrentAddress { get; private set; }
+
+        public static int CurrentPort { get; private set; }
+
+        public static DebuggerSessionOptions Options { get; private set; }
+
+        public static string WorkingDirectory { get; set; }
+
+        public static string Arguments { get; set; }
+
+        public static Dictionary<string, string> EnvironmentVariables { get; private set; }
+
+        public static SortedDictionary<long, string> Watches { get; private set; }
+
+        public static SortedDictionary<long, BreakEvent> Breakpoints { get; private set; }
+
+        public static BreakpointStore BreakEvents { get; private set; }
+
+        public static bool DebuggeeKilled
+        {
+            get { return _debuggeeKilled; }
+            set { _debuggeeKilled = value; }
+        }
+
+        static volatile bool _debuggeeKilled;
+
+        static long _nextWatchId;
+
+        static long _nextBreakpointId;
+
+        static volatile bool _showResumeMessage;
+
+        public static State State
+        {
+            get
+            {
+                lock (_lock)
+                {
+                    if (_session == null || _session.HasExited || !_session.IsConnected)
+                        return State.Exited;
+
+                    return _session.IsRunning ? State.Running : State.Suspended;
+                }
+            }
+        }
+
+        static volatile SessionKind _kind;
+
+        public static SessionKind Kind
+        {
+            get { return _kind; }
+            set { _kind = value; }
+        }
+
+        static ProcessInfo _activeProcess;
+
+        public static ProcessInfo ActiveProcess
+        {
+            get { return _activeProcess; }
+        }
+
+        public static ThreadInfo ActiveThread
+        {
+            get
+            {
+                lock (_lock)
+                    return _session == null ? null : _session.ActiveThread;
+            }
+        }
+
+        public static Backtrace ActiveBacktrace
+        {
+            get
+            {
+                var thr = ActiveThread;
+
+                return thr == null ? null : thr.Backtrace;
+            }
+        }
+
+        static StackFrame _activeFrame;
+
+        public static StackFrame ActiveFrame
+        {
+            get
+            {
+                var f = _activeFrame;
+
+                if (f != null)
+                    return f;
+
+                var bt = ActiveBacktrace;
+
+                if (bt != null)
+                    return _activeFrame = bt.GetFrame(0);
+
+                return null;
+            }
+            set { _activeFrame = value; }
+        }
+
+        public static ExceptionInfo ActiveException
+        {
+            get
+            {
+                var bt = ActiveBacktrace;
+
+                return bt == null ? null : bt.GetFrame(0).GetException();
+            }
+        }
+
+        static void PrintException(string prefix, ExceptionInfo ex)
+        {
+            // HACK: Until we get a `WaitHandle` property on the
+            // `Mono.Debugging.Client.ExceptionInfo` type...
+            ex.Message.Discard();
+
+            while (ex.Message == "Loading...")
+                Thread.Sleep(10);
+
+            Log.Error("{0}{1}: {2}", prefix, ex.Type, ex.Message);
+        }
+
+        static void PrintException(ExceptionInfo ex)
+        {
+            PrintException(string.Empty, ex);
+
+            var prefix = "> ";
+            var inner = ex;
+
+            while ((inner = inner.InnerException) != null)
+            {
+                PrintException(prefix, inner);
+
+                prefix = "--" + prefix;
+            }
+        }
+
+        static string StringizeTarget()
+        {
+            if (CurrentExecutable != null)
+                return CurrentExecutable.Name;
+
+            if (CurrentAddress != null)
+                return string.Format("{0}:{1}", CurrentAddress, CurrentPort);
+
+            return "<none>";
+        }
+
+        static void EnsureCreated()
+        {
+            lock (_lock)
+            {
+                if (_session != null)
+                    return;
+
+                _session = new SoftDebuggerSession();
+                _session.Breakpoints = BreakEvents;
+
+                _session.ExceptionHandler = ex =>
+                {
+                    if (Configuration.Current.LogInternalErrors)
+                    {
+                        Log.Error("Internal debugger error:", ex.GetType());
+                        Log.Error(ex.ToString());
+                    }
+
+                    return true;
+                };
+
+                _session.LogWriter = (isStdErr, text) =>
+                {
+                    if (Configuration.Current.LogRuntimeSpew)
+                        Log.NoticeSameLine("[Mono] {0}", text); // The string already has a line feed.
+                };
+
+                _session.OutputWriter = (isStdErr, text) =>
+                {
+                    lock (Log.Lock)
+                    {
+                        if (isStdErr)
+                            Console.Error.Write(text);
+                        else
+                            Console.Write(text);
+                    }
+                };
+
+                _session.TypeResolverHandler += (identifier, location) =>
+                {
+                    // I honestly have no idea how correct this is. I suspect you
+                    // could probably break it in some corner cases. It does make
+                    // something like `p Android.Runtime.JNIEnv.Handle` work,
+                    // though, which would otherwise have required `global::` to
+                    // be explicitly prepended.
+
+                    if (identifier == "__EXCEPTION_OBJECT__")
+                        return null;
+
+                    foreach (var loc in ActiveFrame.GetAllLocals())
+                        if (loc.Name == identifier)
+                            return null;
+
+                    return identifier;
+                };
+
+                _session.TargetEvent += (sender, e) =>
+                {
+                    Log.Debug("Event: '{0}'", e.Type);
+                };
+
+                _session.TargetStarted += (sender, e) =>
+                {
+                    _activeFrame = null;
+
+                    if (_showResumeMessage)
+                        Log.Notice("Inferior process '{0}' ('{1}') resumed",
+                                   ActiveProcess.Id, StringizeTarget());
+                };
+
+                _session.TargetReady += (sender, e) =>
+                {
+                    _showResumeMessage = true;
+                    _activeProcess = _session.GetProcesses().SingleOrDefault();
+
+                    // The inferior process has launched, so we can safely
+                    // set our `SIGINT` handler without it interfering with
+                    // the inferior.
+                    CommandLine.SetControlCHandler();
+
+                    Log.Notice("Inferior process '{0}' ('{1}') started",
+                               ActiveProcess.Id, StringizeTarget());
+                };
+
+                _session.TargetStopped += (sender, e) =>
+                {
+                    Log.Notice("Inferior process '{0}' ('{1}') suspended",
+                               ActiveProcess.Id, StringizeTarget());
+                    Log.Emphasis(Utilities.StringizeFrame(ActiveFrame, true));
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetInterrupted += (sender, e) =>
+                {
+                    Log.Notice("Inferior process '{0}' ('{1}') interrupted",
+                               ActiveProcess.Id, StringizeTarget());
+                    Log.Emphasis(Utilities.StringizeFrame(ActiveFrame, true));
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetHitBreakpoint += (sender, e) =>
+                {
+                    var bp = e.BreakEvent as Breakpoint;
+                    var fbp = e.BreakEvent as FunctionBreakpoint;
+
+                    if (fbp != null)
+                        Log.Notice("Hit method breakpoint on '{0}'", fbp.FunctionName);
+                    else
+                    {
+                        var cond = bp.ConditionExpression != null ?
+                                   string.Format(" (condition '{0}' met)", bp.ConditionExpression) :
+                                   string.Empty;
+
+                        Log.Notice("Hit breakpoint at '{0}:{1}'{2}", bp.FileName, bp.Line, cond);
+                    }
+
+                    Log.Emphasis(Utilities.StringizeFrame(ActiveFrame, true));
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetExited += (sender, e) =>
+                {
+                    var p = ActiveProcess;
+
+                    // Can happen when a remote connection attempt fails.
+                    if (p == null)
+                    {
+                        if (_kind == SessionKind.Listening)
+                            Log.Notice("Listening socket closed");
+                        else if (_kind == SessionKind.Connected)
+                            Log.Notice("Connection attempt terminated");
+                        else
+                            Log.Notice("Failed to connect to '{0}'", StringizeTarget());
+                    }
+                    else
+                        Log.Notice("Inferior process '{0}' ('{1}') exited", ActiveProcess.Id, StringizeTarget());
+
+                    // Make sure we clean everything up on a normal exit.
+                    Kill();
+
+                    _debuggeeKilled = true;
+                    _kind = SessionKind.Disconnected;
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetExceptionThrown += (sender, e) =>
+                {
+                    var ex = ActiveException;
+
+                    Log.Notice("Trapped first-chance exception of type '{0}'", ex.Type);
+                    Log.Emphasis(Utilities.StringizeFrame(ActiveFrame, true));
+
+                    PrintException(ex);
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetUnhandledException += (sender, e) =>
+                {
+                    var ex = ActiveException;
+
+                    Log.Notice("Trapped unhandled exception of type '{0}'", ex.Type);
+                    Log.Emphasis(Utilities.StringizeFrame(ActiveFrame, true));
+
+                    PrintException(ex);
+
+                    CommandLine.ResumeEvent.Set();
+                };
+
+                _session.TargetThreadStarted += (sender, e) =>
+                {
+                    Log.Notice("Inferior thread '{0}' ('{1}') started",
+                               e.Thread.Id, e.Thread.Name);
+                };
+
+                _session.TargetThreadStopped += (sender, e) =>
+                {
+                    Log.Notice("Inferior thread '{0}' ('{1}') exited",
+                               e.Thread.Id, e.Thread.Name);
+                };
+            }
+        }
+
+        public static void Run(FileInfo file)
+        {
+            lock (_lock)
+            {
+                EnsureCreated();
+
+                CurrentExecutable = file;
+                CurrentAddress = null;
+                CurrentPort = -1;
+
+                _showResumeMessage = false;
+                _debuggeeKilled = false;
+                _kind = SessionKind.Launched;
+
+                var info = new SoftDebuggerStartInfo(Configuration.Current.RuntimePrefix,
+                                                     EnvironmentVariables)
+                {
+                    Command = file.FullName,
+                    Arguments = Arguments,
+                    WorkingDirectory = WorkingDirectory,
+                    StartArgs =
+                    {
+                        MaxConnectionAttempts = Configuration.Current.MaxConnectionAttempts,
+                        TimeBetweenConnectionAttempts = Configuration.Current.ConnectionAttemptInterval
+                    }
+                };
+
+                // We need to ignore `SIGINT` while we start the inferior
+                // process so that it inherits that signal disposition.
+                CommandLine.UnsetControlCHandler();
+
+                _session.Run(info, Options);
+
+                CommandLine.InferiorExecuting = true;
+            }
+        }
+
+        public static void Connect(IPAddress address, int port)
+        {
+            lock (_lock)
+            {
+                EnsureCreated();
+
+                CurrentExecutable = null;
+                CurrentAddress = address;
+                CurrentPort = port;
+
+                _showResumeMessage = false;
+                _debuggeeKilled = false;
+                _kind = SessionKind.Connected;
+
+                var args = new SoftDebuggerConnectArgs(string.Empty, address, port)
+                {
+                    MaxConnectionAttempts = Configuration.Current.MaxConnectionAttempts,
+                    TimeBetweenConnectionAttempts = Configuration.Current.ConnectionAttemptInterval
+                };
+
+                _session.Run(new SoftDebuggerStartInfo(args), Options);
+
+                CommandLine.InferiorExecuting = true;
+            }
+        }
+
+        public static void Listen(IPAddress address, int port)
+        {
+            lock (_lock)
+            {
+                EnsureCreated();
+
+                CurrentExecutable = null;
+                CurrentAddress = address;
+                CurrentPort = port;
+
+                _showResumeMessage = false;
+                _debuggeeKilled = false;
+                _kind = SessionKind.Listening;
+
+                var args = new SoftDebuggerListenArgs(string.Empty, address, port);
+
+                _session.Run(new SoftDebuggerStartInfo(args), Options);
+
+                CommandLine.InferiorExecuting = true;
+            }
+        }
+
+        public static void Pause()
+        {
+            lock (_lock)
+                if (_session != null && _session.IsRunning)
+                    _session.Stop();
+        }
+
+        public static void Continue()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.Continue();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static void Kill()
+        {
+            lock (_lock)
+            {
+                if (_session == null)
+                    return;
+
+                CommandLine.InferiorExecuting = true;
+
+                if (!_session.HasExited)
+                    _session.Exit();
+
+                _session.Dispose();
+                _session = null;
+            }
+        }
+
+        public static void StepOverLine()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.StepLine();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static void StepOverInstruction()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.StepInstruction();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static void StepIntoLine()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.NextLine();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static void StepIntoInstruction()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.NextInstruction();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static void StepOutOfMethod()
+        {
+            lock (_lock)
+            {
+                if (_session != null && !_session.IsRunning && !_session.HasExited)
+                {
+                    _session.Finish();
+
+                    CommandLine.InferiorExecuting = true;
+                }
+            }
+        }
+
+        public static long GetWatchId()
+        {
+            return _nextWatchId++;
+        }
+
+        public static long GetBreakpointId()
+        {
+            return _nextBreakpointId++;
+        }
+
+        public static void ResetState()
+        {
+            // No need to lock on this data.
+            WorkingDirectory = Environment.CurrentDirectory;
+            Arguments = string.Empty;
+            EnvironmentVariables = new Dictionary<string, string>();
+            Watches = new SortedDictionary<long, string>();
+            _nextWatchId = 0;
+            Breakpoints = new SortedDictionary<long, BreakEvent>();
+            BreakEvents = new BreakpointStore();
+
+            // Make sure breakpoints/catchpoints take effect.
+            lock (_lock)
+                if (_session != null)
+                    _session.Breakpoints = BreakEvents;
+        }
+
+        public static void ResetOptions()
+        {
+            // No need to lock on this data.
+
+            Options = new DebuggerSessionOptions
+            {
+                EvaluationOptions = EvaluationOptions.DefaultOptions
+            };
+
+            Options.EvaluationOptions.UseExternalTypeResolver = true;
+        }
+
+        [Serializable]
+        sealed class DebuggerState
+        {
+            public string WorkingDirectory { get; set; }
+
+            public string Arguments { get; set; }
+
+            public Dictionary<string, string> EnvironmentVariables { get; set; }
+
+            public SortedDictionary<long, string> Watches { get; set; }
+
+            public long NextWatchId { get; set; }
+
+            public Dictionary<long, Tuple<BreakEvent, bool>> Breakpoints { get; set; }
+
+            public long NextBreakpointId { get; set; }
+
+            public ReadOnlyCollection<Catchpoint> Catchpoints { get; set; }
+        }
+
+        public static void Write(FileInfo file)
+        {
+            var state = new DebuggerState
+            {
+                WorkingDirectory = WorkingDirectory,
+                Arguments = Arguments,
+                EnvironmentVariables = EnvironmentVariables,
+                Watches = Watches,
+                NextWatchId = _nextWatchId,
+                Breakpoints = Breakpoints.Select(x => Tuple.Create(x.Key, x.Value, BreakEvents.Contains(x.Value)))
+                                         .ToDictionary(x => x.Item1, x => Tuple.Create(x.Item2, x.Item3)),
+                NextBreakpointId = _nextBreakpointId,
+                Catchpoints = BreakEvents.GetCatchpoints()
+            };
+
+            try
+            {
+                using (var stream = file.Open(FileMode.Create, FileAccess.Write))
+                    new BinaryFormatter().Serialize(stream, state);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not write database file '{0}':", file);
+                Log.Error(ex.ToString());
+            }
+        }
+
+        public static void Read(FileInfo file)
+        {
+            DebuggerState state;
+
+            try
+            {
+                using (var stream = file.Open(FileMode.Open, FileAccess.Read))
+                    state = (DebuggerState)new BinaryFormatter().Deserialize(stream);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not read database file '{0}':", file);
+                Log.Error(ex.ToString());
+
+                return;
+            }
+
+            ResetState();
+
+            WorkingDirectory = state.WorkingDirectory;
+            Arguments = state.Arguments;
+            EnvironmentVariables = state.EnvironmentVariables;
+            Watches = state.Watches;
+
+            foreach (var kvp in state.Breakpoints)
+            {
+                Breakpoints.Add(kvp.Key, kvp.Value.Item1);
+
+                if (kvp.Value.Item2)
+                    BreakEvents.Add(kvp.Value.Item1);
+            }
+
+            foreach (var cp in state.Catchpoints)
+                BreakEvents.Add(cp);
+        }
+    }
+}
diff --git a/src/LibEdit.cs b/src/LibEdit.cs
new file mode 100644
index 0000000..1479ffb
--- /dev/null
+++ b/src/LibEdit.cs
@@ -0,0 +1,40 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Runtime.InteropServices;
+
+namespace Mono.Debugger.Client
+{
+    static class LibEdit
+    {
+        [DllImport("libedit", EntryPoint = "rl_initialize")]
+        public static extern int Initialize();
+
+        [DllImport("libedit", EntryPoint = "readline")]
+        public static extern string ReadLine(string prompt);
+
+        [DllImport("libedit", EntryPoint = "add_history")]
+        public static extern void AddHistory(string line);
+    }
+}
diff --git a/src/Log.cs b/src/Log.cs
new file mode 100644
index 0000000..6e05fe8
--- /dev/null
+++ b/src/Log.cs
@@ -0,0 +1,105 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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;
+
+namespace Mono.Debugger.Client
+{
+    public static class Log
+    {
+        static readonly bool _debug = Environment.GetEnvironmentVariable("SDB_DEBUG") == "enable";
+
+        public static object Lock { get; private set; }
+
+        static Log()
+        {
+            Lock = new object();
+        }
+
+        static void Output(bool nl, string color, string format, object[] args)
+        {
+            var str = color + (args.Length == 0 ? format : string.Format(format, args)) + Color.Reset;
+
+            lock (Lock)
+            {
+                if (nl)
+                    Console.WriteLine(str);
+                else
+                    Console.Write(str);
+            }
+        }
+
+        public static void InfoSameLine(string format, params object[] args)
+        {
+            Output(false, string.Empty, format, args);
+        }
+
+        public static void Info(string format, params object[] args)
+        {
+            Output(true, string.Empty, format, args);
+        }
+
+        public static void NoticeSameLine(string format, params object[] args)
+        {
+            Output(false, Color.DarkCyan, format, args);
+        }
+
+        public static void Notice(string format, params object[] args)
+        {
+            Output(true, Color.DarkCyan, format, args);
+        }
+
+        public static void EmphasisSameLine(string format, params object[] args)
+        {
+            Output(false, Color.DarkGreen, format, args);
+        }
+
+        public static void Emphasis(string format, params object[] args)
+        {
+            Output(true, Color.DarkGreen, format, args);
+        }
+
+        public static void ErrorSameLine(string format, params object[] args)
+        {
+            Output(false, Color.DarkRed, format, args);
+        }
+
+        public static void Error(string format, params object[] args)
+        {
+            Output(true, Color.DarkRed, format, args);
+        }
+
+        public static void DebugSameLine(string format, params object[] args)
+        {
+            if (_debug || Configuration.Current.DebugLogging)
+                Output(false, Color.DarkYellow, format, args);
+        }
+
+        public static void Debug(string format, params object[] args)
+        {
+            if (_debug || Configuration.Current.DebugLogging)
+                Output(true, Color.DarkYellow, format, args);
+        }
+    }
+}
diff --git a/src/MultiCommand.cs b/src/MultiCommand.cs
new file mode 100644
index 0000000..a04f492
--- /dev/null
+++ b/src/MultiCommand.cs
@@ -0,0 +1,159 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.Linq;
+
+namespace Mono.Debugger.Client
+{
+    public abstract class MultiCommand : Command
+    {
+        readonly Dictionary<Type, Command> _instantiations = new Dictionary<Type, Command>();
+
+        readonly List<Tuple<string, Command>> _allCommands = new List<Tuple<string, Command>>();
+
+        readonly List<Tuple<string, Command>> _commands = new List<Tuple<string, Command>>();
+
+        Command _forwardTarget;
+
+        public IEnumerable<Tuple<string, Command>> Commands
+        {
+            get { return _commands; }
+        }
+
+        public virtual string Parent
+        {
+            get { return null; }
+        }
+
+        public override string Syntax
+        {
+            get
+            {
+                var names = string.Join("|", Names);
+                var subs = string.Join("|", _commands.Select(x => x.Item2).Distinct().Select(x => x.Names[0]));
+
+                return string.Format("{0}{1} {2} ...", Parent != null ? Parent + " " : string.Empty, names, subs);
+            }
+        }
+
+        Command Instantiate(Type type)
+        {
+            Command cmd;
+
+            if (!_instantiations.TryGetValue(type, out cmd))
+            {
+                cmd = (Command)Activator.CreateInstance(type);
+
+                _instantiations.Add(type, cmd);
+            }
+
+            return cmd;
+        }
+
+        protected void AddCommand(Command command)
+        {
+            foreach (var name in command.Names)
+            {
+                var tup = Tuple.Create(name, command);
+
+                _allCommands.Add(tup);
+                _commands.Add(tup);
+            }
+        }
+
+        protected void AddCommand(Type type)
+        {
+            AddCommand(Instantiate(type));
+        }
+
+        protected void AddCommand<T>()
+            where T : Command
+        {
+            AddCommand(typeof(T));
+        }
+
+        protected void AddCommandWithName(Command command, string name)
+        {
+            _allCommands.Add(Tuple.Create(name, command));
+        }
+
+        protected void AddCommandWithName(Type type, string name)
+        {
+            AddCommandWithName(Instantiate(type), name);
+        }
+
+        protected void AddCommandWithName<T>(string name)
+            where T : Command
+        {
+            AddCommandWithName(typeof(T), name);
+        }
+
+        protected void Forward<T>()
+        {
+            _forwardTarget = Instantiate(typeof(T));
+        }
+
+        public Command GetCommand(string name)
+        {
+            foreach (var cmd in _allCommands)
+                if (cmd.Item1.StartsWith(name, StringComparison.InvariantCultureIgnoreCase))
+                    return cmd.Item2;
+
+            return null;
+        }
+
+        public override sealed void Process(string args)
+        {
+            var name = args.Split(' ').Where(x => x != string.Empty).FirstOrDefault();
+
+            if (name != null)
+            {
+                var cmd = GetCommand(name);
+
+                if (cmd != null)
+                {
+                    cmd.Process(new string(args.Skip(name.Length).ToArray()).Trim());
+                    return;
+                }
+            }
+
+            ProcessFallback(args, name != null);
+        }
+
+        protected virtual void ProcessFallback(string args, bool invalidSubCommand)
+        {
+            if (invalidSubCommand)
+                Log.Error("Invalid sub-command given to '{0}'", Names[0]);
+            else
+            {
+                if (_forwardTarget != null)
+                    _forwardTarget.Process(args);
+                else
+                    Log.Error("No '{0}' sub-command specified", Names[0]);
+            }
+        }
+    }
+}
diff --git a/src/Plugins.cs b/src/Plugins.cs
new file mode 100644
index 0000000..d1944b6
--- /dev/null
+++ b/src/Plugins.cs
@@ -0,0 +1,122 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using System.IO;
+using System.Linq;
+using System.Reflection;
+
+namespace Mono.Debugger.Client
+{
+    static class Plugins
+    {
+        static string GetFolderPath()
+        {
+            var home = Environment.GetFolderPath(Environment.SpecialFolder.UserProfile);
+
+            return Path.Combine(home, ".sdb");
+        }
+
+        public static IEnumerable<Command> Load(string file)
+        {
+            Log.Debug("Attempting to load plugin '{0}'", file);
+
+            Assembly asm;
+
+            try
+            {
+                asm = Assembly.LoadFile(file);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not load plugin '{0}':", file);
+                Log.Error(ex.ToString());
+
+                yield break;
+            }
+
+            Type[] types;
+
+            try
+            {
+                types = asm.GetTypes();
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not load types from plugin '{0}':", file);
+                Log.Error(ex.ToString());
+
+                yield break;
+            }
+
+            foreach (var type in types)
+            {
+                Command cmd = null;
+
+                try
+                {
+                    if (type.IsDefined(typeof(CommandAttribute), false))
+                        cmd = (Command)Activator.CreateInstance(type);
+                }
+                catch (Exception ex)
+                {
+                    Log.Error("Could not load command type '{0}':", type);
+                    Log.Error(ex.ToString());
+                }
+
+                if (cmd != null)
+                    yield return cmd;
+            }
+        }
+
+        public static IEnumerable<Command> LoadDirectory(string path)
+        {
+            try
+            {
+                return Directory.EnumerateFiles(path, "*.dll").Select(x => Load(x)).SelectMany(x => x);
+            }
+            catch (Exception ex)
+            {
+                Log.Error("Could not iterate plugin directory '{0}':", path);
+                Log.Error(ex.ToString());
+            }
+
+            return Enumerable.Empty<Command>();
+        }
+
+        public static IEnumerable<Command> LoadDefault()
+        {
+            var env = Environment.GetEnvironmentVariable("SDB_PATH") ?? string.Empty;
+            var paths = new[] { GetFolderPath() }.Concat(env.Split(Path.PathSeparator).Where(x => x != string.Empty));
+            var list = new List<Command>();
+
+            foreach (var p in paths)
+                if (Directory.Exists(p))
+                    list.AddRange(LoadDirectory(p));
+
+            return list;
+        }
+    }
+}
diff --git a/src/Program.cs b/src/Program.cs
new file mode 100644
index 0000000..dfe9a71
--- /dev/null
+++ b/src/Program.cs
@@ -0,0 +1,110 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.Collections.Generic;
+using Mono.Options;
+using Mono.Unix.Native;
+
+namespace Mono.Debugger.Client
+{
+    static class Program
+    {
+        static void Help(OptionSet set)
+        {
+            Console.WriteLine("This is the Mono soft debugger.");
+            Console.WriteLine();
+            Console.WriteLine("Usage:");
+            Console.WriteLine();
+            Console.WriteLine("  sdb [options]");
+            Console.WriteLine("  sdb [options] \"run prog.exe\"");
+            Console.WriteLine("  sdb [options] \"args --foo --bar baz\" \"run prog.exe\"");
+            Console.WriteLine();
+            Console.WriteLine("Options:");
+            Console.WriteLine();
+
+            set.WriteOptionDescriptions(Console.Out);
+
+            Console.WriteLine();
+            Console.WriteLine("All non-option arguments are treated as commands that are executed");
+            Console.WriteLine("at startup. Files are executed before individual commands.");
+        }
+
+        static int Main(string[] args)
+        {
+            var version = false;
+            var help = false;
+            var batch = false;
+            var rc = true;
+
+            var files = new List<string>();
+
+            var p = new OptionSet()
+            {
+                {"v|version", "Show version information and exit.", v => version = v != null},
+                {"h|help", "Show this help message and exit.", v => help = v != null},
+                {"b|batch", "Exit after running commands.", v => batch = v != null},
+                {"n|norc", "Don't run commands in '~/.sdb.rc'.", v => rc = v == null},
+                {"f|file=", "Execute commands in the given file at startup.", f => files.Add(f)}
+            };
+
+            List<string> cmds;
+
+            try
+            {
+                cmds = p.Parse(args);
+            }
+            catch (OptionException ex)
+            {
+                Console.WriteLine(ex.Message);
+                return 1;
+            }
+
+            var ver = typeof(Program).Assembly.GetName().Version;
+
+            if (version)
+            {
+                Console.WriteLine("Mono soft debugger (sdb) {0}", ver);
+                return 0;
+            }
+
+            if (help)
+            {
+                Help(p);
+                return 0;
+            }
+
+            // This ugly hack is necessary because we don't want the
+            // debuggee to die if the user hits Ctrl-C. We set our signal
+            // disposition to `SIG_IGN` so that the debuggee inherits
+            // this and doesn't die.
+            if (!Utilities.IsWindows && !batch)
+                Stdlib.SetSignalAction(Signum.SIGINT, SignalAction.Ignore);
+
+            CommandLine.Run(ver, batch, rc, cmds, files);
+
+            return 0;
+        }
+    }
+}
diff --git a/src/SessionKind.cs b/src/SessionKind.cs
new file mode 100644
index 0000000..ae8b648
--- /dev/null
+++ b/src/SessionKind.cs
@@ -0,0 +1,34 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.
+//
+
+namespace Mono.Debugger.Client
+{
+    public enum SessionKind : byte
+    {
+        Disconnected,
+        Launched,
+        Listening,
+        Connected,
+    }
+}
diff --git a/src/State.cs b/src/State.cs
new file mode 100644
index 0000000..08fc405
--- /dev/null
+++ b/src/State.cs
@@ -0,0 +1,33 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.
+//
+
+namespace Mono.Debugger.Client
+{
+    public enum State : byte
+    {
+        Running,
+        Suspended,
+        Exited
+    }
+}
diff --git a/src/Utilities.cs b/src/Utilities.cs
new file mode 100644
index 0000000..85179c9
--- /dev/null
+++ b/src/Utilities.cs
@@ -0,0 +1,131 @@
+//
+// The MIT License (MIT)
+//
+// Copyright (c) 2013 Alex Rønne Petersen
+//
+// 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.IO;
+using Mono.Debugging.Client;
+
+namespace Mono.Debugger.Client
+{
+    public static class Utilities
+    {
+        public static bool IsWindows
+        {
+            get { return (int)Environment.OSVersion.Platform < (int)PlatformID.Unix; }
+        }
+
+        public static void Discard<T>(this T value)
+        {
+        }
+
+        public static string StringizeFrame(StackFrame frame, bool includeIndex)
+        {
+            var loc = string.Empty;
+            string src = null;
+
+            if (frame.SourceLocation.FileName != null)
+            {
+                loc = " at " + frame.SourceLocation.FileName;
+
+                if (frame.SourceLocation.Line != -1)
+                {
+                    loc += ":" + frame.SourceLocation.Line;
+
+                    StreamReader reader = null;
+
+                    try
+                    {
+                        reader = File.OpenText(frame.SourceLocation.FileName);
+
+                        var cur = 1;
+
+                        while (!reader.EndOfStream)
+                        {
+                            var str = reader.ReadLine();
+
+                            if (cur == frame.SourceLocation.Line)
+                            {
+                                src = str;
+                                break;
+                            }
+
+                            cur++;
+                        }
+                    }
+                    catch (Exception)
+                    {
+                    }
+                    finally
+                    {
+                        if (reader != null)
+                            reader.Dispose();
+                    }
+                }
+            }
+
+            var idx = includeIndex ? string.Format("#{0} ", frame.Index) : string.Empty;
+            var srcStr = src != null ? Environment.NewLine + src : string.Empty;
+
+            return string.Format("{0}[0x{1:X8}] {2}{3}{4}", idx, frame.Address,
+                                 frame.SourceLocation.MethodName, loc, srcStr);
+        }
+
+        public static string StringizeThread(ThreadInfo thread, bool includeFrame)
+        {
+            var f = includeFrame ? thread.Backtrace.GetFrame(0) : null;
+
+            var fstr = f == null ? string.Empty : Environment.NewLine + StringizeFrame(f, false);
+            var tstr = string.Format("Thread #{0} '{1}'", thread.Id, thread.Name);
+
+            return string.Format("{0}{1}", tstr, fstr);
+        }
+
+        public static Tuple<string, bool> StringizeValue(ObjectValue value)
+        {
+            string str;
+            bool err;
+
+            if (value.IsError)
+            {
+                str = value.DisplayValue;
+                err = true;
+            }
+            else if (value.IsUnknown)
+            {
+                str = "Result is unrepresentable";
+                err = true;
+            }
+            else
+            {
+                str = value.DisplayValue;
+                err = false;
+            }
+
+            if (Configuration.Current.DebugLogging)
+                str += string.Format(" ({0})", value.Flags);
+
+            return Tuple.Create(str, err);
+        }
+    }
+}

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/pkg-mono/packages/sdb.git



More information about the Pkg-mono-svn-commits mailing list