Module:    environment-tools
Synopsis:  Environment tools
Author:    Andy Armstrong
Copyright:    Original Code is Copyright (c) 1995-2004 Functional Objects, Inc.
              All rights reserved.
License:      Functional Objects Library Public License Version 1.0
Dual-license: GNU Lesser General Public License
Warranty:     Distributed WITHOUT WARRANTY OF ANY KIND

/// Debugger protocols

define open generic find-debugger
    (frame :: <environment-frame>,
     #key startup-option :: <application-startup-option>)
 => (interactor);

define sealed method find-debugger
    (frame :: <environment-frame>,
     #key startup-option :: <application-startup-option> = #"debug")
 => (interactor)
  let zoom
    = select (startup-option)
	#"start", #"debug" => #"zoom-debugging";
	#"interact"	   => #"zoom-interacting";
      end;
  find-debugger-from-environment(frame,
				 project: frame.ensure-frame-project,
				 zoom: zoom)
end method find-debugger;


/// Debugging edition availability

define method low-level-debugging?
    ()
  let edition = release-edition-type();
  edition == #"internal"
    | edition == #"enhanced"
end method low-level-debugging?;

define method profiling-available?
    ()
  let edition = release-edition-type();
  edition == #"internal"
    | edition == #"enhanced"
end method profiling-available?;

define method just-in-time-debugging?
    ()
  let edition = release-edition-type();
  edition == #"internal"
    | edition == #"enhanced"
end method just-in-time-debugging?;

define method remote-debugging?
    ()
  let edition = release-edition-type();
  edition == #"internal"
    | edition == #"enhanced"
end method remote-debugging?;


/// Just-in-time debugging

define method just-in-time-debugging-arguments
    ()
 => (process :: false-or(<string>), id :: false-or(<string>))
  if (just-in-time-debugging?())
    let arguments = as(<deque>, os/application-arguments());
    local method next-argument
	      () => (argument :: <string>, option :: false-or(<string>))
	    let argument = as-lowercase(pop(arguments));
	    let first-char = argument[0];
	    let colon-position = position(argument, ':');
	    if ((first-char == '/' | first-char == '-') & colon-position)
	      values(copy-sequence(argument, start: 1, end: colon-position),
		     copy-sequence(argument, start: colon-position + 1))
	    else
	      values(argument, #f)
	    end
	  end method next-argument;
    let process = #f;
    let id = #f;
    while (~empty?(arguments))
      let (argument, option) = next-argument();
      if (option)
	select (argument by \=)
	  "p", "process" =>
	    process := option;
	  "e", "id" =>
	    id := option;
	  otherwise =>
	    #f;
	end
      end
    end;
    values(process, id)
  end;
end method just-in-time-debugging-arguments;

define method frame-open-just-in-time-project
    (frame :: <environment-frame>, process :: <string>, 
     id :: false-or(<string>))
 => ()
  let process = lookup-process-by-id(process);
  if (process)
    frame-open-just-in-time-project(frame, process, id)
  else
    let message
      = format-to-string("Failed to find process '%s' to debug", process);
    environment-error-message(message, owner: frame)
  end
end method frame-open-just-in-time-project;

define method frame-open-just-in-time-project
    (frame :: <environment-frame>, process :: <process>, 
     id :: false-or(<string>))
 => ()
  let filename = process-executable-file(process);
  let project-filename = frame-choose-project-for-filename(frame, filename);
  if (project-filename)
    let project = coerce-project(project-filename);
    if (project)
      ensure-project-browser-showing-project
	(project,
	 application-process: process,
	 application-id:      id)
    end
  end
end method frame-open-just-in-time-project;


/// Choose project for filename

define variable $choose-project-dialog-width :: <integer> = 350;

define frame <choose-project-dialog> (<dialog-frame>)
  slot %filename :: <file-locator>,
    required-init-keyword: filename:;
  pane project-pane (frame)
    make(<combo-box>,
	 items: map(curry(as, <string>), most-recent-projects()),
	 value: guess-recent-project-for-filename(frame.%filename),
	 min-width: 300,
	 activate-callback: exit-dialog);
  pane browse-button (frame)
    make(<button>,
	 label: "Browse...",
	 activate-callback: 
	   method (gadget)
	     let project = gadget-value(frame.project-pane);
	     let default = project & as(<file-locator>, project);
	     let filename
	       = environment-choose-file
	           (title:  "Open",
		    owner:   frame,
		    default: default,
		    filters: #[#"common-locate-project", #"project", #"lid"]);
	     if (filename)
	       frame.%filename := filename;
	       gadget-text(frame.project-pane) := as(<string>, filename)
	     end
	   end);
  pane debug-project-button (frame)
    make(<radio-button>,
	 id: #"project",
	 label: "Use Dylan project database");
  pane debug-native-button (frame)
    make(<radio-button>,
	 id: #"native",
	 label: "Use native debug information only");
  pane debug-project-pane (frame)
    vertically (spacing: 8)
      frame.debug-project-button;
      horizontally (spacing: 4)
        make(<null-pane>, width: 20, fixed-width?: #t);
	frame.project-pane;
	frame.browse-button;
      end;
    end;
  pane debug-options (frame)
    make(<radio-box>,
	 child: vertically (spacing: 8)
	          frame.debug-project-pane;
	          frame.debug-native-button
                end,
	 value-changed-callback:
	   method (gadget)
	     let value = gadget-value(gadget);
	     let enable? = value == #"project";
	     gadget-enabled?(frame.project-pane)  := enable?;
	     gadget-enabled?(frame.browse-button) := enable?;
	   end);
  layout (frame)
    grouping (format-to-string("Debug '%s'", frame.%filename),
	      max-width: $fill)
      frame.debug-options
    end;
  keyword title: = release-product-name();
  keyword center?: = #t;
end frame <choose-project-dialog>;

define method guess-recent-project-for-filename
    (filename :: <file-locator>) => (guess :: false-or(<string>))
  let projects = most-recent-projects();
  let base = filename.locator-base;
  let filename
    = block (return)
	for (project in projects)
	  if (base == project.locator-base)
	    return(project)
	  end
	end;
	~empty?(projects) & projects[0]
      end;
  filename & as(<string>, filename)
end method guess-recent-project-for-filename;

define method frame-choose-project-for-filename
    (frame :: <environment-frame>, filename :: <file-locator>)
 => (filename :: false-or(<file-locator>))
  let dialog
    = make(<choose-project-dialog>,
	   filename: filename,
	   owner: frame,
	   width: max($choose-project-dialog-width, 350));
  if (start-dialog(dialog))
    let (width, height) = frame-size(dialog);
    $choose-project-dialog-width := width;
    let choice = gadget-value(dialog.debug-options);
    select (choice)
      #"project" => 
	let name = gadget-value(dialog.project-pane);
	name & as(<file-locator>, name);
      #"native" => 
	filename;
    end
  end
end method frame-choose-project-for-filename;


/// Attach application

define variable $application-attach-dialog-width :: <integer> = 450;

define frame <application-attach-dialog> (<dialog-frame>)
  slot %machine :: <machine> = environment-host-machine(),
    init-keyword: machine:;
  pane machine-pane (frame)
    make(<option-box>,
	 items: available-machines(),
	 value: frame.%machine,
	 label-key: machine-hostname,
	 enabled?: remote-debugging?(),
	 value-changed-callback: method (gadget)
				   frame.%machine := gadget-value(gadget);
				   update-dialog-processes(frame)
				 end);
  pane process-pane (frame)
    make(<table-control>,
	 items: keyed-sort(available-processes(machine: frame.%machine),
			   key: process-name),
	 headings:   #["Process", "ID", "File"],
	 widths:     #[120, 50, 400],
	 alignments: #[#"left", #"right", #"left"],
	 generators: vector(process-name,
			    process-id,
			    process-executable-file),
	 callbacks: vector(rcurry(sort-processes, #"process"),
			   rcurry(sort-processes, #"id"),
			   rcurry(sort-processes, #"file")),
	 lines: 15,
	 min-width: 400,
	 activate-callback: method (gadget)
			      exit-dialog(sheet-frame(gadget))
			    end);
  pane new-connection-button (frame)
    make(<button>,
	 label: "Open New Connection...",
	 activate-callback: method (button)
			      let machine 
				= open-remote-connection(owner: frame);
			      if (machine)
				update-dialog-machines(frame, machine: machine);
                                frame.%machine := machine;
                                update-dialog-processes(frame)
			      end
			    end);
  layout (frame)
    grouping ("Attach to a running process:", max-width: $fill)
      vertically (spacing: 8)
        horizontally (spacing: 4)
          make(<label>, label: "Machine:");
          frame.machine-pane
        end;
        frame.process-pane;
	frame.new-connection-button
      end
    end;
  keyword title: = release-product-name();
end frame <application-attach-dialog>;

define method sort-processes
    (gadget :: <table-control>, option :: <symbol>) => ()
  let items = gadget-items(gadget);
  gadget-items(gadget)
    := keyed-sort(items,
		  key: select (option)
			 #"process" => process-name;
			 #"id"      => process-id;
			 #"file"    => process-executable-file
		       end)
end method sort-processes;

define method process-name
    (process :: <process>) => (name :: <string>)
  let filename = process.process-executable-file;
  filename.locator-name
end method process-name;

define method update-dialog-machines
    (frame :: <application-attach-dialog>, #key machine) => ()
  let gadget = frame.machine-pane;
  gadget-items(gadget) := available-machines();
  if (machine) gadget-value(gadget) := machine end;
  update-dialog-processes(frame)
end method update-dialog-machines;

define method update-dialog-processes
    (frame :: <application-attach-dialog>) => ()
  let machine = frame.%machine;
  gadget-items(frame.process-pane) := available-processes(machine: machine)
end method update-dialog-processes;

define function available-machines
    () => (machines :: <sequence>)
  let machines = make(<stretchy-vector>);
  do-machine-connections
    (method (machine :: <machine>)
       add!(machines, machine)
     end);
  machines
end function available-machines;

define function available-processes
    (#key machine = environment-host-machine())
 => (processes :: <sequence>)
  let processes = make(<stretchy-vector>);
  do-active-processes
    (method (process :: <process>)
       if (process-debuggable?(process))
	 add!(processes, process)
       end
     end,
     machine: machine);
  keyed-sort!(processes,
	      key: method (process :: <process>)
		     let filename = process.process-executable-file;
		     filename.locator-name
		   end);
  processes
end function available-processes;

define method frame-attach-application
    (frame :: <environment-frame>, 
     #key process :: false-or(<process>),
          id :: false-or(<string>))
 => ()
  let project = frame.frame-current-project;
  let process
    = process
        | begin
	    let machine
	      = if (project)
		  project.project-debug-machine
		end
	          | environment-host-machine();
	    let dialog
	      = make(<application-attach-dialog>,
		     owner:   frame,
		     machine: machine,
		     width:   max($application-attach-dialog-width, 450));
	    if (start-dialog(dialog))
	      let (width, height) = frame-size(dialog);
	      $application-attach-dialog-width := width;
	      gadget-value(dialog.process-pane)
	    end
	  end;
  if (process)
    if (project)
      attach-live-application(project, process, system-data: id)
    else
      frame-open-just-in-time-project(frame, process, id)
    end
  end
end method frame-attach-application;


/// Remote connections

define variable $new-connection-dialog-width :: <integer> = 250;

define frame <new-connection-dialog> (<dialog-frame>)
  slot %machine :: false-or(<machine>) = #f;
  constant slot %default-machine-address :: <string>,
    init-value: "",
    init-keyword: machine-address:;
  pane address-pane (frame)
    make(<text-field>,
         text: frame.%default-machine-address,
	 min-width: 200,
	 activate-callback: method (gadget)
			      %open-remote-connection(frame)
			    end);
  pane password-pane (frame)
    make(<text-field>,
	 min-width: 200,
	 activate-callback: method (gadget)
			      %open-remote-connection(frame)
			    end);
  layout (frame)
    vertically (spacing: 8)
      make(<label>, label: "Connect to a remote machine:");
      make(<table-layout>,
	   columns: 2,
	   spacing: 4,
	   x-alignment: #[#"right", #"left"],
	   children: vector(make(<label>, label: "IP address:"),
			    frame.address-pane));
      make(<table-layout>,
	   columns: 2,
	   spacing: 4,
	   x-alignment: #[#"right", #"left"],
	   children: vector(make(<label>, label: "Password:"),
			    frame.password-pane));
    end;
  keyword exit-callback: = %open-remote-connection;
  keyword title: = "Open New Connection";
end frame <new-connection-dialog>;

define method open-remote-connection
    (#key owner :: <frame>, default-address = "")
 => (machine :: false-or(<machine>))
  let dialog = make(<new-connection-dialog>, 
                    owner: owner,
                    machine-address: default-address,
		    width: max($new-connection-dialog-width, 250));
  when (start-dialog(dialog))
    let (width, height) = frame-size(dialog);
    $new-connection-dialog-width := width;
    dialog.%machine
  end
end method open-remote-connection;

define method %open-remote-connection
    (dialog :: <new-connection-dialog>) => ()
  let address = gadget-value(dialog.address-pane);
  let password = gadget-value(dialog.password-pane);
  block ()
    let machine = make(<machine>, network-address: address, password: password);
    dialog.%machine := machine;
    exit-dialog(dialog)
  exception (<remote-connection-failed-error>)
    let message
      = format-to-string("Failed to open remote machine address %s", address);
    environment-error-message(message, owner: dialog)
  exception (<remote-connection-password-mismatch-error>)
    let message
      = format-to-string
         ("Invalid password supplied for machine address %s", address);
    environment-error-message(message, owner: dialog)
  end
end method %open-remote-connection;

// Close all connections when the environment shuts down
tune-in($environment-channel,
	method (message :: <environment-stopping-message>)
	  do-machine-connections
	    (close-connection-to-machine,
	     include-local?: #f)
	end,
	message-type: <environment-stopping-message>);


/// Application commands

define open generic frame-start-application (frame :: <environment-frame>) => ();
define open generic frame-attach-application (frame :: <environment-frame>, #key process, id) => ();
define open generic frame-restart-application (frame :: <environment-frame>) => ();
define open generic frame-start-or-resume-application (frame :: <environment-frame>) => ();
define open generic frame-debug-application (frame :: <environment-frame>) => ();
define open generic frame-interact (frame :: <environment-frame>) => ();
define open generic frame-browse-threads (frame :: <environment-frame>) => ();
define open generic frame-pause-application (frame :: <environment-frame>, #key thread, startup-option) => ();
define open generic frame-resume-application (frame :: <environment-frame>) => ();
define open generic frame-stop-application (frame :: <environment-frame>) => ();
define open generic frame-create-thread (frame :: <environment-frame>) => ();

define open generic frame-continue-application (frame :: <environment-frame>) => ();

define open generic find-debugger-from-environment
    (portd :: type-union(<port>, <frame>),
     #key project :: <project-object>,
     thread :: false-or(<thread-object>),
     zoom :: <symbol>)
 => ();

define open generic find-profiler-from-environment
    (portd :: type-union(<port>, <frame>), #key project :: <project-object>)
 => ();

define method frame-start-application
    (frame :: <environment-frame>) => ()
  do-frame-start-application(frame, verify-start-function?: #f);
end method frame-start-application;

define method frame-debug-application
    (frame :: <environment-frame>) => ()
  do-frame-debug-application(frame, startup-option: #"debug");
end method frame-debug-application;

define method frame-profiling?-setter
    (profiling? :: <boolean>, frame :: <environment-frame>)
 => (profiling? :: <boolean>)
  let project = frame.ensure-frame-project;
  if (profiling?)
    start-profiling-application(project)
  else
    stop-profiling-application(project)
  end;
  profiling?
end method frame-profiling?-setter;

define method frame-find-profiler
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  find-profiler-from-environment(frame, project: project)
end method frame-find-profiler;

define method verify-application-start-function
    (frame :: <environment-frame>)
 => (start-function-okay? :: <boolean>)
  let project = frame.ensure-frame-project;
  let name = project.project-start-function-name | "";
  if (~empty?(name) & ~project.project-start-function)
    let message
      = format-to-string("The start function '%s' was not found. Start anyway?",
			 name);
    environment-question(message, owner: frame, style: #"warning")
  else
    #t
  end
end method verify-application-start-function;

define method do-frame-start-application
    (frame :: <environment-frame>,
     #key startup-option :: <application-startup-option> = #"start",
          verify-start-function? = #t)
 => ()
  let project = frame.ensure-frame-project;
  local method do-it ()
	  let filename = project-full-build-filename(project);
	  if (file-exists?(filename))
	    if (~verify-start-function?
		  | verify-application-start-function(frame))
	      with-busy-cursor (frame)
		frame-do-run-application
		  (frame, project, startup-option: startup-option)
	      end
	    end
	  end
	end method do-it;
  if (project-can-be-built?(project) & ~project-compiler-database(project))
    with-project-database (frame, link?: #t)
      do-it()
    end
  else
    with-built-project (frame)
      do-it()
    end
  end
end method do-frame-start-application;

define method do-frame-debug-application
    (frame :: <environment-frame>,
     #key startup-option :: <application-startup-option> = #"debug") => ()
  let project = frame.ensure-frame-project;
  let (tethered?, state) = frame-application-tethered?(frame, project);
  select (tethered? & state)
    #f =>
      do-frame-start-application(frame, startup-option: startup-option);
    #"running" =>
      frame-pause-application(frame, startup-option: startup-option);
    #"stopped" =>
      find-debugger(frame, startup-option: startup-option);
  end select;
end method do-frame-debug-application;

define method frame-interact
    (frame :: <environment-frame>) => ()
  do-frame-debug-application(frame, startup-option: #"interact");
end method frame-interact;

define method frame-browse-threads
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  let application = project.project-application;
  if (application)
    with-busy-cursor (frame)
      browse-object(project, application, page: #"threads");
    end
  else 
    environment-error-message("No tethered application.", owner: frame);
  end if;
end method frame-browse-threads;

define method frame-pause-application
    (frame :: <environment-frame>, 
     #key thread, startup-option)
 => ()
  let project = frame.ensure-frame-project;
  with-busy-cursor (frame)
    stop-application(project, client-data: pair(thread, startup-option))
  end
end method frame-pause-application;

define method frame-resume-application
    (frame :: <environment-frame>) => ()
  with-busy-cursor (frame)
    frame-continue-application(frame)
  end
end method frame-resume-application;

define method frame-continue-application
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;

  continue-application(project)
end method frame-continue-application;

define method frame-stop-application
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  let check? = environment-application-confirm-stop?();
  if (~check?
	| environment-question
	    (format-to-string
	       ("All application state will be lost by stopping it.\n"
		"Are you sure?",
		frame-default-object-name(frame, project)),
	     owner: frame,
	     style: #"warning",
	     exit-style: #"ok-cancel"))
    with-busy-cursor (frame)
      close-application(project, wait-for-termination?: #t)
    end
  end
end method frame-stop-application;

define method frame-restart-application
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  with-busy-cursor (frame)
    let startup-option = project.application-startup-option;
    close-application(project, wait-for-termination?: #t);
    frame-do-run-application
      (frame, project, startup-option: startup-option)
  end
end method frame-restart-application;

define method frame-start-or-resume-application 
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  let (tethered?, state) = frame-application-tethered?(frame, project);
  select (tethered? & state)
    #f => frame-start-application(frame);
    #"stopped" => frame-resume-application(frame);
    #"running" => #f;
  end select;
end method frame-start-or-resume-application;

define variable $create-thread-dialog-width :: <integer> = 250;

define frame <create-thread-dialog> (<dialog-frame>)
  pane thread-title-pane (dialog)
    make(<text-field>,
	 documentation: "Enter the name for the new thread.");
  layout (dialog)
    horizontally (spacing: 4)
      make(<label>, label: "Title:");
      dialog.thread-title-pane;
    end;
  input-focus (dialog)
    dialog.thread-title-pane;
  keyword title: = "New Thread...";
end frame <create-thread-dialog>;

define method frame-create-thread
    (frame :: <environment-frame>) => ()
  let project = frame.ensure-frame-project;
  let dialog = make(<create-thread-dialog>,
		    owner: frame,
		    width: max($create-thread-dialog-width, 250));
  if (start-dialog(dialog))
    let (width, height) = frame-size(dialog);
    $browse-object-dialog-width := width;
    let title = dialog.thread-title-pane.gadget-value;
    create-application-thread(project, title)
  end
end method frame-create-thread;

define command-table *basic-run-command-table* (*global-command-table*)
  menu-item "Start"    = frame-start-application,
    accelerator:   make-keyboard-gesture(#"f8"),
    documentation: "Starts the executable.";
  menu-item "Attach..."    = frame-attach-application,
    documentation: $attach-doc;
  separator;
  menu-item "Debug"    = frame-debug-application,
    accelerator:   make-keyboard-gesture(#"f8", #"shift"),
    documentation: $debug-doc;
  menu-item "Interact" = frame-interact,
    accelerator:   make-keyboard-gesture(#"f8", #"control"),
    documentation: $interact-doc;
  separator;
  menu-item "Pause"    = frame-pause-application,
    documentation: "Pauses the executable.";
  menu-item "Resume"   = frame-resume-application,
    documentation: "Resumes the executable.";
  separator;
  menu-item "Stop"     = frame-stop-application,
    documentation: "Exits the executable.";
  menu-item "Restart"  = frame-restart-application,
    documentation: "Exits the executable then starts it.";
  separator;
  menu-item "New Thread..." = frame-create-thread,
    documentation: "Creates a new thread in the application.";
end command-table *basic-run-command-table*;

define command-table *run-command-table* (*global-command-table*)
  include *basic-run-command-table*;
  separator;
  include *all-breakpoints-command-table*;
end command-table *run-command-table*;

define method enable-application-command-table
    (frame :: <environment-frame>,
     state :: false-or(<application-state>))
  => ()
  let project = frame.ensure-frame-project;
  let application-can-be-debugged? = project-can-be-debugged?(project);

  local method enabled?-setter
	    (enabled? :: <boolean>, command :: <function>)
	  command-enabled?(command, frame)
	    := application-can-be-debugged? & enabled?
	end method enabled?-setter;

  let application-running? = state == #"running";
  let application-stopped? = state == #"stopped";
  let application-started? = application-running? | application-stopped?;
  let application-not-started? = ~application-started?;
  let application-not-running? = ~application-running?;
  let attaching-available? = application-not-started? & just-in-time-debugging?();

  enabled?(frame-start-application)           := application-not-started?;
  enabled?(frame-attach-application)          := attaching-available?;

  enabled?(frame-debug-application)           := #t;
  enabled?(frame-interact)                    := #t;

  enabled?(frame-browse-threads)              := application-started?;
  enabled?(frame-stop-application)            := application-started?;
  enabled?(frame-restart-application)         := application-started?;
  enabled?(frame-create-thread)               := application-started?;
  
  enabled?(frame-pause-application)           := application-running?;
  enabled?(frame-resume-application)          := application-stopped?;
  enabled?(frame-start-or-resume-application) := application-not-running?;
end method enable-application-command-table;