/* Process record and replay target for GDB, the GNU debugger.

   Copyright (C) 2008-2026 Free Software Foundation, Inc.

   This file is part of GDB.

   This program is free software; you can redistribute it and/or modify
   it under the terms of the GNU General Public License as published by
   the Free Software Foundation; either version 3 of the License, or
   (at your option) any later version.

   This program is distributed in the hope that it will be useful,
   but WITHOUT ANY WARRANTY; without even the implied warranty of
   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
   GNU General Public License for more details.

   You should have received a copy of the GNU General Public License
   along with this program.  If not, see <http://www.gnu.org/licenses/>.  */

#include "cli/cli-cmds.h"
#include "completer.h"
#include "record.h"
#include "observable.h"
#include "inferior.h"
#include "gdbsupport/common-utils.h"
#include "cli/cli-style.h"
#include "cli/cli-utils.h"
#include "disasm.h"
#include "interps.h"
#include "top.h"


/* This is the debug switch for process record.  */
unsigned int record_debug = 0;

/* The number of instructions to print in "record instruction-history".  */
static unsigned int record_insn_history_size = 10;

/* The variable registered as control variable in the "record
   instruction-history" command.  Necessary for extra input
   validation.  */
static unsigned int record_insn_history_size_setshow_var;

/* The number of functions to print in "record function-call-history".  */
static unsigned int record_call_history_size = 10;

/* The variable registered as control variable in the "record
   call-history" command.  Necessary for extra input validation.  */
static unsigned int record_call_history_size_setshow_var;

struct cmd_list_element *record_cmdlist = NULL;
static struct cmd_list_element *record_goto_cmdlist = NULL;
struct cmd_list_element *set_record_cmdlist = NULL;
struct cmd_list_element *show_record_cmdlist = NULL;
struct cmd_list_element *info_record_cmdlist = NULL;

#define DEBUG(msg, args...)						\
  if (record_debug)							\
    gdb_printf (gdb_stdlog, "record: " msg "\n", ##args)

/* See record.h.  */

struct target_ops *
find_record_target (void)
{
  return current_inferior ()->target_at (record_stratum);
}

/* Check that recording is active.  Throw an error, if it isn't.  */

static struct target_ops *
require_record_target (void)
{
  struct target_ops *t;

  t = find_record_target ();
  if (t == NULL)
    error (_("No recording is currently active.\n"
	     "Use the \"record full\" or \"record btrace\" command first."));

  return t;
}

/* See record.h.  */

void
record_preopen (void)
{
  /* Check if a record target is already running.  */
  if (find_record_target () != NULL)
    error (_("The process is already being recorded.  Use \"%ps\" to "
	     "stop recording first."),
	   styled_string (command_style.style (), "record stop"));
}

/* See record.h.  */

void
record_start (const char *method, const char *format, int from_tty)
{
  if (method == NULL)
    {
      if (format == NULL)
	execute_command_to_string ("record", from_tty, false);
      else
	error (_("Invalid format."));
    }
  else if (streq (method, "full"))
    {
      if (format == NULL)
	execute_command_to_string ("record full", from_tty, false);
      else
	error (_("Invalid format."));
    }
  else if (streq (method, "btrace"))
    {
      if (format == NULL)
	execute_command_to_string ("record btrace", from_tty, false);
      else if (streq (format, "bts"))
	execute_command_to_string ("record btrace bts", from_tty, false);
      else if (streq (format, "pt"))
	execute_command_to_string ("record btrace pt", from_tty, false);
      else
	error (_("Invalid format."));
    }
  else
    error (_("Invalid method."));
}

/* See record.h.  */

void
record_stop (int from_tty)
{
  execute_command_to_string ("record stop", from_tty, false);
}

/* See record.h.  */

int
record_read_memory (struct gdbarch *gdbarch,
		    CORE_ADDR memaddr, gdb_byte *myaddr,
		    ssize_t len)
{
  int ret = target_read_memory (memaddr, myaddr, len);

  if (ret != 0)
    DEBUG ("error reading memory at addr %s len = %ld.\n",
	   paddress (gdbarch, memaddr), (long) len);

  return ret;
}

/* Stop recording.  */

static bool
record_stop (struct target_ops *t)
{
  DEBUG ("stop %s", t->shortname ());

  /* Force the execution direction to forward.  If the user had it
     set to reverse when stopping recording, 'reverse-next' would
     say the direction is reversed, but the inferior would move
     forward, and there'd be no way to reconcile those.  */
  execution_direction = EXEC_FORWARD;

  return t->stop_recording ();
}

/* Unpush the record target.  */

static void
record_unpush (struct target_ops *t)
{
  DEBUG ("unpush %s", t->shortname ());

  current_inferior ()->unpush_target (t);
}

/* See record.h.  */

void
record_disconnect (struct target_ops *t, const char *args, int from_tty)
{
  gdb_assert (t->stratum () == record_stratum);

  DEBUG ("disconnect %s", t->shortname ());

  (void) record_stop (t);
  record_unpush (t);

  target_disconnect (args, from_tty);
}

/* See record.h.  */

void
record_detach (struct target_ops *t, inferior *inf, int from_tty)
{
  gdb_assert (t->stratum () == record_stratum);

  DEBUG ("detach %s", t->shortname ());

  (void) record_stop (t);
  record_unpush (t);

  target_detach (inf, from_tty);
}

/* See record.h.  */

void
record_mourn_inferior (struct target_ops *t)
{
  gdb_assert (t->stratum () == record_stratum);

  DEBUG ("mourn inferior %s", t->shortname ());

  /* It is safer to not stop recording.  Resources will be freed when
     threads are discarded.  */
  record_unpush (t);

  target_mourn_inferior (inferior_ptid);
}

/* See record.h.  */

void
record_kill (struct target_ops *t)
{
  gdb_assert (t->stratum () == record_stratum);

  DEBUG ("kill %s", t->shortname ());

  /* It is safer to not stop recording.  Resources will be freed when
     threads are discarded.  */
  record_unpush (t);

  target_kill ();
}

/* See record.h.  */

int
record_check_stopped_by_breakpoint (const address_space *aspace,
				    CORE_ADDR pc,
				    enum target_stop_reason *reason)
{
  if (breakpoint_inserted_here_p (aspace, pc))
    {
      if (hardware_breakpoint_inserted_here_p (aspace, pc))
	*reason = TARGET_STOPPED_BY_HW_BREAKPOINT;
      else
	*reason = TARGET_STOPPED_BY_SW_BREAKPOINT;
      return 1;
    }

  return 0;
}

/* Implement "show record debug" command.  */

static void
show_record_debug (struct ui_file *file, int from_tty,
		   struct cmd_list_element *c, const char *value)
{
  gdb_printf (file, _("Debugging of process record target is %s.\n"),
	      value);
}

/* Alias for "target record-full".  */

static void
cmd_record_start (const char *args, int from_tty)
{
  /* As 'record' is a prefix command then if the user types 'record blah'
     GDB will search for the 'blah' sub-command and either run that instead
     of calling this function, or throw an error if 'blah' doesn't exist.
     As a result, we only get here if no args are given.  */
  gdb_assert (args == nullptr);
  execute_command ("target record-full", from_tty);
}

/* Truncate the record log from the present point
   of replay until the end.  */

static void
cmd_record_delete (const char *args, int from_tty)
{
  require_record_target ();

  if (!target_record_is_replaying (inferior_ptid))
    {
      gdb_printf (_("Already at end of record list.\n"));
      return;
    }

  if (!target_supports_delete_record ())
    {
      gdb_printf (_("The current record target does not support "
		    "this operation.\n"));
      return;
    }

  if (!from_tty || query (_("Delete the log from this point forward "
			    "and begin to record the running message "
			    "at current PC?")))
    target_delete_record ();
}

/* Implement the "stoprecord" or "record stop" command.  */

static void
cmd_record_stop (const char *args, int from_tty)
{
  struct target_ops *t = require_record_target ();
  bool thread_moved = record_stop (t);
  record_unpush (t);

  gdb_printf (_("Process record is stopped and all execution "
		"logs are deleted.\n"));

  interps_notify_record_changed (current_inferior (), 0, NULL, NULL);

  /* INFERIOR_PTID may have moved when we stopped recording.  */
  if (thread_moved)
    notify_user_selected_context_changed (USER_SELECTED_FRAME);
}


/* The "info record" command.  */

static void
info_record_command (const char *args, int from_tty)
{
  struct target_ops *t;

  t = find_record_target ();
  if (t == NULL)
    {
      gdb_printf (_("No recording is currently active.\n"));
      return;
    }

  gdb_printf (_("Active record target: %s\n"), t->shortname ());
  t->info_record ();
}

/* The "record save" command.  */

static void
cmd_record_save (const char *args, int from_tty)
{
  const char *recfilename;
  char recfilename_buffer[40];

  require_record_target ();

  if (args != NULL && *args != 0)
    recfilename = args;
  else
    {
      /* Default recfile name is "gdb_record.PID".  */
      xsnprintf (recfilename_buffer, sizeof (recfilename_buffer),
		"gdb_record.%d", inferior_ptid.pid ());
      recfilename = recfilename_buffer;
    }

  target_save_record (recfilename);
}

/* See record.h.  */

void
record_goto (const char *arg)
{
  ULONGEST insn;

  if (arg == NULL || *arg == '\0')
    error (_("Command requires an argument (insn number to go to)."));

  insn = parse_and_eval_long (arg);

  require_record_target ();
  target_goto_record (insn);
}

/* "record goto" command.  Argument is an instruction number,
   as given by "info record".

   Rewinds the recording (forward or backward) to the given instruction.  */

static void
cmd_record_goto (const char *arg, int from_tty)
{
  record_goto (arg);
}

/* The "record goto begin" command.  */

static void
cmd_record_goto_begin (const char *arg, int from_tty)
{
  if (arg != NULL && *arg != '\0')
    error (_("Junk after argument: %s."), arg);

  require_record_target ();
  target_goto_record_begin ();
}

/* The "record goto end" command.  */

static void
cmd_record_goto_end (const char *arg, int from_tty)
{
  if (arg != NULL && *arg != '\0')
    error (_("Junk after argument: %s."), arg);

  require_record_target ();
  target_goto_record_end ();
}

/* Read an instruction number from an argument string.  */

static ULONGEST
get_insn_number (const char **arg)
{
  ULONGEST number;
  const char *begin, *end, *pos;

  begin = *arg;
  pos = skip_spaces (begin);

  if (!c_isdigit (*pos))
    error (_("Expected positive number, got: %s."), pos);

  number = strtoulst (pos, &end, 10);

  *arg += (end - begin);

  return number;
}

/* Read a context size from an argument string.  */

static int
get_context_size (const char **arg)
{
  const char *pos;
  char *end;

  pos = skip_spaces (*arg);

  if (!c_isdigit (*pos))
    error (_("Expected positive number, got: %s."), pos);

  long result = strtol (pos, &end, 10);
  *arg = end;
  return result;
}

/* Complain about junk at the end of an argument string.  */

static void
no_chunk (const char *arg)
{
  if (*arg != 0)
    error (_("Junk after argument: %s."), arg);
}

/* Read instruction-history modifiers from an argument string.  */

static gdb_disassembly_flags
get_insn_history_modifiers (const char **arg)
{
  gdb_disassembly_flags modifiers;
  const char *args;

  modifiers = 0;
  args = *arg;

  if (args == NULL)
    return modifiers;

  while (*args == '/')
    {
      ++args;

      if (*args == '\0')
	error (_("Missing modifier."));

      for (; *args; ++args)
	{
	  if (c_isspace (*args))
	    break;

	  if (*args == '/')
	    continue;

	  switch (*args)
	    {
	    case 'a':
	      modifiers |= DISASSEMBLY_OMIT_AUX_INSN;
	      break;
	    case 'm':
	    case 's':
	      modifiers |= DISASSEMBLY_SOURCE;
	      modifiers |= DISASSEMBLY_FILENAME;
	      break;
	    case 'r':
	      modifiers |= DISASSEMBLY_RAW_INSN;
	      break;
	    case 'b':
	      modifiers |= DISASSEMBLY_RAW_BYTES;
	      break;
	    case 'f':
	      modifiers |= DISASSEMBLY_OMIT_FNAME;
	      break;
	    case 'p':
	      modifiers |= DISASSEMBLY_OMIT_PC;
	      break;
	    default:
	      error (_("Invalid modifier: %c."), *args);
	    }
	}

      args = skip_spaces (args);
    }

  /* Update the argument string.  */
  *arg = args;

  return modifiers;
}

/* The "set record instruction-history-size / set record
   function-call-history-size" commands are unsigned, with UINT_MAX
   meaning unlimited.  The target interfaces works with signed int
   though, to indicate direction, so map "unlimited" to INT_MAX, which
   is about the same as unlimited in practice.  If the user does have
   a log that huge, she can fetch it in chunks across several requests,
   but she'll likely have other problems first...  */

static int
command_size_to_target_size (unsigned int size)
{
  gdb_assert (size <= INT_MAX || size == UINT_MAX);

  if (size == UINT_MAX)
    return INT_MAX;
  else
    return size;
}

/* The "record instruction-history" command.  */

static void
cmd_record_insn_history (const char *arg, int from_tty)
{
  require_record_target ();

  gdb_disassembly_flags flags = get_insn_history_modifiers (&arg);

  int size = command_size_to_target_size (record_insn_history_size);

  if (arg == NULL || *arg == 0 || streq (arg, "+"))
    target_insn_history (size, flags);
  else if (streq (arg, "-"))
    target_insn_history (-size, flags);
  else
    {
      ULONGEST begin, end;

      begin = get_insn_number (&arg);

      if (*arg == ',')
	{
	  arg = skip_spaces (++arg);

	  if (*arg == '+')
	    {
	      arg += 1;
	      size = get_context_size (&arg);

	      no_chunk (arg);

	      target_insn_history_from (begin, size, flags);
	    }
	  else if (*arg == '-')
	    {
	      arg += 1;
	      size = get_context_size (&arg);

	      no_chunk (arg);

	      target_insn_history_from (begin, -size, flags);
	    }
	  else
	    {
	      end = get_insn_number (&arg);

	      no_chunk (arg);

	      target_insn_history_range (begin, end, flags);
	    }
	}
      else
	{
	  no_chunk (arg);

	  target_insn_history_from (begin, size, flags);
	}

      dont_repeat ();
    }
}

/* Read function-call-history modifiers from an argument string.  */

static record_print_flags
get_call_history_modifiers (const char **arg)
{
  record_print_flags modifiers = 0;
  const char *args = *arg;

  if (args == NULL)
    return modifiers;

  while (*args == '/')
    {
      ++args;

      if (*args == '\0')
	error (_("Missing modifier."));

      for (; *args; ++args)
	{
	  if (c_isspace (*args))
	    break;

	  if (*args == '/')
	    continue;

	  switch (*args)
	    {
	    case 'l':
	      modifiers |= RECORD_PRINT_SRC_LINE;
	      break;
	    case 'i':
	      modifiers |= RECORD_PRINT_INSN_RANGE;
	      break;
	    case 'c':
	      modifiers |= RECORD_PRINT_INDENT_CALLS;
	      break;
	    case 'a':
	      modifiers |= RECORD_DONT_PRINT_AUX;
	      break;
	    default:
	      error (_("Invalid modifier: %c."), *args);
	    }
	}

      args = skip_spaces (args);
    }

  /* Update the argument string.  */
  *arg = args;

  return modifiers;
}

/* The "record function-call-history" command.  */

static void
cmd_record_call_history (const char *arg, int from_tty)
{
  require_record_target ();

  record_print_flags flags = get_call_history_modifiers (&arg);

  int size = command_size_to_target_size (record_call_history_size);

  if (arg == NULL || *arg == 0 || streq (arg, "+"))
    target_call_history (size, flags);
  else if (streq (arg, "-"))
    target_call_history (-size, flags);
  else
    {
      ULONGEST begin, end;

      begin = get_insn_number (&arg);

      if (*arg == ',')
	{
	  arg = skip_spaces (++arg);

	  if (*arg == '+')
	    {
	      arg += 1;
	      size = get_context_size (&arg);

	      no_chunk (arg);

	      target_call_history_from (begin, size, flags);
	    }
	  else if (*arg == '-')
	    {
	      arg += 1;
	      size = get_context_size (&arg);

	      no_chunk (arg);

	      target_call_history_from (begin, -size, flags);
	    }
	  else
	    {
	      end = get_insn_number (&arg);

	      no_chunk (arg);

	      target_call_history_range (begin, end, flags);
	    }
	}
      else
	{
	  no_chunk (arg);

	  target_call_history_from (begin, size, flags);
	}

      dont_repeat ();
    }
}

/* Helper for "set record instruction-history-size" and "set record
   function-call-history-size" input validation.  COMMAND_VAR is the
   variable registered in the command as control variable.  *SETTING
   is the real setting the command allows changing.  */

static void
validate_history_size (unsigned int *command_var, unsigned int *setting)
{
  if (*command_var != UINT_MAX && *command_var > INT_MAX)
    {
      unsigned int new_value = *command_var;

      /* Restore previous value.  */
      *command_var = *setting;
      error (_("integer %u out of range"), new_value);
    }

  /* Commit new value.  */
  *setting = *command_var;
}

/* Called by do_setshow_command.  We only want values in the
   [0..INT_MAX] range, while the command's machinery accepts
   [0..UINT_MAX].  See command_size_to_target_size.  */

static void
set_record_insn_history_size (const char *args, int from_tty,
			      struct cmd_list_element *c)
{
  validate_history_size (&record_insn_history_size_setshow_var,
			 &record_insn_history_size);
}

/* Called by do_setshow_command.  We only want values in the
   [0..INT_MAX] range, while the command's machinery accepts
   [0..UINT_MAX].  See command_size_to_target_size.  */

static void
set_record_call_history_size (const char *args, int from_tty,
			      struct cmd_list_element *c)
{
  validate_history_size (&record_call_history_size_setshow_var,
			 &record_call_history_size);
}

INIT_GDB_FILE (record)
{
  struct cmd_list_element *c;

  add_setshow_zuinteger_cmd ("record", no_class, &record_debug,
			     _("Set debugging of record/replay feature."),
			     _("Show debugging of record/replay feature."),
			     _("When enabled, debugging output for "
			       "record/replay feature is displayed."),
			     NULL, show_record_debug, &setdebuglist,
			     &showdebuglist);

  add_setshow_uinteger_cmd ("instruction-history-size", no_class,
			    &record_insn_history_size_setshow_var, _("\
Set number of instructions to print in \"record instruction-history\"."), _("\
Show number of instructions to print in \"record instruction-history\"."), _("\
A size of \"unlimited\" means unlimited instructions.  The default is 10."),
			    set_record_insn_history_size, NULL,
			    &set_record_cmdlist, &show_record_cmdlist);

  add_setshow_uinteger_cmd ("function-call-history-size", no_class,
			    &record_call_history_size_setshow_var, _("\
Set number of function to print in \"record function-call-history\"."), _("\
Show number of functions to print in \"record function-call-history\"."), _("\
A size of \"unlimited\" means unlimited lines.  The default is 10."),
			    set_record_call_history_size, NULL,
			    &set_record_cmdlist, &show_record_cmdlist);

  cmd_list_element *record_cmd
    = add_prefix_cmd ("record", class_obscure, cmd_record_start,
		      _("Start recording."),
		      &record_cmdlist, 0, &cmdlist);
  add_com_alias ("rec", record_cmd, class_obscure, 1);

  set_show_commands setshow_record_cmds
    = add_setshow_prefix_cmd ("record", class_support,
			      _("Set record options."),
			      _("Show record options."),
			      &set_record_cmdlist, &show_record_cmdlist,
			      &setlist, &showlist);


  add_alias_cmd ("rec", setshow_record_cmds.set, class_obscure, 1, &setlist);
  add_alias_cmd ("rec", setshow_record_cmds.show, class_obscure, 1, &showlist);

  cmd_list_element *info_record_cmd
    = add_prefix_cmd ("record", class_support, info_record_command,
		      _("Info record options."), &info_record_cmdlist,
		      0, &infolist);
  add_alias_cmd ("rec", info_record_cmd, class_obscure, 1, &infolist);

  c = add_cmd ("save", class_obscure, cmd_record_save,
	       _("Save the execution log to a file.\n\
Usage: record save [FILENAME]\n\
Default filename is 'gdb_record.PROCESS_ID'."),
	       &record_cmdlist);
  set_cmd_completer (c, deprecated_filename_completer);

  cmd_list_element *record_delete_cmd
    =  add_cmd ("delete", class_obscure, cmd_record_delete,
		_("Delete the rest of execution log and start recording it \
anew."),
	    &record_cmdlist);
  add_alias_cmd ("d", record_delete_cmd, class_obscure, 1, &record_cmdlist);
  add_alias_cmd ("del", record_delete_cmd, class_obscure, 1, &record_cmdlist);

  cmd_list_element *record_stop_cmd
    = add_cmd ("stop", class_obscure, cmd_record_stop,
	       _("Stop the record/replay target."),
	       &record_cmdlist);
  add_alias_cmd ("s", record_stop_cmd, class_obscure, 1, &record_cmdlist);

  add_prefix_cmd ("goto", class_obscure, cmd_record_goto, _("\
Restore the program to its state at instruction number N.\n\
Argument is instruction number, as shown by 'info record'."),
		  &record_goto_cmdlist, 1, &record_cmdlist);

  cmd_list_element *record_goto_begin_cmd
    = add_cmd ("begin", class_obscure, cmd_record_goto_begin,
	       _("Go to the beginning of the execution log."),
	       &record_goto_cmdlist);
  add_alias_cmd ("start", record_goto_begin_cmd, class_obscure, 1,
		 &record_goto_cmdlist);

  add_cmd ("end", class_obscure, cmd_record_goto_end,
	   _("Go to the end of the execution log."),
	   &record_goto_cmdlist);

  add_cmd ("instruction-history", class_obscure, cmd_record_insn_history, _("\
Print disassembled instructions stored in the execution log.\n\
With a /m or /s modifier, source lines are included (if available).\n\
With a /r modifier, raw instructions in hex are included.\n\
With a /f modifier, function names are omitted.\n\
With a /p modifier, current position markers are omitted.\n\
With a /a modifier, omits output of auxiliary data, which is enabled\n\
by default.\n\
With no argument, disassembles ten more instructions after the previous\n\
disassembly.\n\
\"record instruction-history -\" disassembles ten instructions before a\n\
previous disassembly.\n\
One argument specifies an instruction number as shown by 'info record', and\n\
ten instructions are disassembled after that instruction.\n\
Two arguments with comma between them specify starting and ending instruction\n\
numbers to disassemble.\n\
If the second argument is preceded by '+' or '-', it specifies the distance\n\
from the first argument.\n\
The number of instructions to disassemble can be defined with\n\
\"set record instruction-history-size\"."),
	   &record_cmdlist);

  add_cmd ("function-call-history", class_obscure, cmd_record_call_history, _("\
Prints the execution history at function granularity.\n\
It prints one line for each sequence of instructions that belong to the same\n\
function.\n\
Without modifiers, it prints the function name.\n\
With a /l modifier, the source file and line number range is included.\n\
With a /i modifier, the instruction number range is included.\n\
With a /c modifier, the output is indented based on the call stack depth.\n\
With a /a modifier, omits output of auxiliary data, which is enabled \
by default.\n\
With no argument, prints ten more lines after the previous ten-line print.\n\
\"record function-call-history -\" prints ten lines before a previous ten-line\n\
print.\n\
One argument specifies a function number as shown by 'info record', and\n\
ten lines are printed after that function.\n\
Two arguments with comma between them specify a range of functions to print.\n\
If the second argument is preceded by '+' or '-', it specifies the distance\n\
from the first argument.\n\
The number of functions to print can be defined with\n\
\"set record function-call-history-size\"."),
	   &record_cmdlist);

  /* Sync command control variables.  */
  record_insn_history_size_setshow_var = record_insn_history_size;
  record_call_history_size_setshow_var = record_call_history_size;
}
