1 //===-- CommandObjectWatchpointCommand.cpp --------------------------------===//
2 //
3 // Part of the LLVM Project, under the Apache License v2.0 with LLVM Exceptions.
4 // See https://llvm.org/LICENSE.txt for license information.
5 // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception
6 //
7 //===----------------------------------------------------------------------===//
8 
9 #include <vector>
10 
11 #include "CommandObjectWatchpoint.h"
12 #include "CommandObjectWatchpointCommand.h"
13 #include "lldb/Breakpoint/StoppointCallbackContext.h"
14 #include "lldb/Breakpoint/Watchpoint.h"
15 #include "lldb/Core/IOHandler.h"
16 #include "lldb/Host/OptionParser.h"
17 #include "lldb/Interpreter/CommandInterpreter.h"
18 #include "lldb/Interpreter/CommandReturnObject.h"
19 #include "lldb/Interpreter/OptionArgParser.h"
20 #include "lldb/Target/Target.h"
21 
22 using namespace lldb;
23 using namespace lldb_private;
24 
25 // FIXME: "script-type" needs to have its contents determined dynamically, so
26 // somebody can add a new scripting language to lldb and have it pickable here
27 // without having to change this enumeration by hand and rebuild lldb proper.
28 static constexpr OptionEnumValueElement g_script_option_enumeration[] = {
29     {
30         eScriptLanguageNone,
31         "command",
32         "Commands are in the lldb command interpreter language",
33     },
34     {
35         eScriptLanguagePython,
36         "python",
37         "Commands are in the Python language.",
38     },
39     {
40         eScriptLanguageLua,
41         "lua",
42         "Commands are in the Lua language.",
43     },
44     {
45         eSortOrderByName,
46         "default-script",
47         "Commands are in the default scripting language.",
48     },
49 };
50 
51 static constexpr OptionEnumValues ScriptOptionEnum() {
52   return OptionEnumValues(g_script_option_enumeration);
53 }
54 
55 #define LLDB_OPTIONS_watchpoint_command_add
56 #include "CommandOptions.inc"
57 
58 class CommandObjectWatchpointCommandAdd : public CommandObjectParsed,
59                                           public IOHandlerDelegateMultiline {
60 public:
61   CommandObjectWatchpointCommandAdd(CommandInterpreter &interpreter)
62       : CommandObjectParsed(interpreter, "add",
63                             "Add a set of LLDB commands to a watchpoint, to be "
64                             "executed whenever the watchpoint is hit.",
65                             nullptr, eCommandRequiresTarget),
66         IOHandlerDelegateMultiline("DONE",
67                                    IOHandlerDelegate::Completion::LLDBCommand),
68         m_options() {
69     SetHelpLong(
70         R"(
71 General information about entering watchpoint commands
72 ------------------------------------------------------
73 
74 )"
75         "This command will prompt for commands to be executed when the specified \
76 watchpoint is hit.  Each command is typed on its own line following the '> ' \
77 prompt until 'DONE' is entered."
78         R"(
79 
80 )"
81         "Syntactic errors may not be detected when initially entered, and many \
82 malformed commands can silently fail when executed.  If your watchpoint commands \
83 do not appear to be executing, double-check the command syntax."
84         R"(
85 
86 )"
87         "Note: You may enter any debugger command exactly as you would at the debugger \
88 prompt.  There is no limit to the number of commands supplied, but do NOT enter \
89 more than one command per line."
90         R"(
91 
92 Special information about PYTHON watchpoint commands
93 ----------------------------------------------------
94 
95 )"
96         "You may enter either one or more lines of Python, including function \
97 definitions or calls to functions that will have been imported by the time \
98 the code executes.  Single line watchpoint commands will be interpreted 'as is' \
99 when the watchpoint is hit.  Multiple lines of Python will be wrapped in a \
100 generated function, and a call to the function will be attached to the watchpoint."
101         R"(
102 
103 This auto-generated function is passed in three arguments:
104 
105     frame:  an lldb.SBFrame object for the frame which hit the watchpoint.
106 
107     wp:     the watchpoint that was hit.
108 
109 )"
110         "When specifying a python function with the --python-function option, you need \
111 to supply the function name prepended by the module name:"
112         R"(
113 
114     --python-function myutils.watchpoint_callback
115 
116 The function itself must have the following prototype:
117 
118 def watchpoint_callback(frame, wp):
119   # Your code goes here
120 
121 )"
122         "The arguments are the same as the arguments passed to generated functions as \
123 described above.  Note that the global variable 'lldb.frame' will NOT be updated when \
124 this function is called, so be sure to use the 'frame' argument. The 'frame' argument \
125 can get you to the thread via frame.GetThread(), the thread can get you to the \
126 process via thread.GetProcess(), and the process can get you back to the target \
127 via process.GetTarget()."
128         R"(
129 
130 )"
131         "Important Note: As Python code gets collected into functions, access to global \
132 variables requires explicit scoping using the 'global' keyword.  Be sure to use correct \
133 Python syntax, including indentation, when entering Python watchpoint commands."
134         R"(
135 
136 Example Python one-line watchpoint command:
137 
138 (lldb) watchpoint command add -s python 1
139 Enter your Python command(s). Type 'DONE' to end.
140 > print "Hit this watchpoint!"
141 > DONE
142 
143 As a convenience, this also works for a short Python one-liner:
144 
145 (lldb) watchpoint command add -s python 1 -o 'import time; print time.asctime()'
146 (lldb) run
147 Launching '.../a.out'  (x86_64)
148 (lldb) Fri Sep 10 12:17:45 2010
149 Process 21778 Stopped
150 * thread #1: tid = 0x2e03, 0x0000000100000de8 a.out`c + 7 at main.c:39, stop reason = watchpoint 1.1, queue = com.apple.main-thread
151   36
152   37   	int c(int val)
153   38   	{
154   39 ->	    return val + 3;
155   40   	}
156   41
157   42   	int main (int argc, char const *argv[])
158 
159 Example multiple line Python watchpoint command, using function definition:
160 
161 (lldb) watchpoint command add -s python 1
162 Enter your Python command(s). Type 'DONE' to end.
163 > def watchpoint_output (wp_no):
164 >     out_string = "Hit watchpoint number " + repr (wp_no)
165 >     print out_string
166 >     return True
167 > watchpoint_output (1)
168 > DONE
169 
170 Example multiple line Python watchpoint command, using 'loose' Python:
171 
172 (lldb) watchpoint command add -s p 1
173 Enter your Python command(s). Type 'DONE' to end.
174 > global wp_count
175 > wp_count = wp_count + 1
176 > print "Hit this watchpoint " + repr(wp_count) + " times!"
177 > DONE
178 
179 )"
180         "In this case, since there is a reference to a global variable, \
181 'wp_count', you will also need to make sure 'wp_count' exists and is \
182 initialized:"
183         R"(
184 
185 (lldb) script
186 >>> wp_count = 0
187 >>> quit()
188 
189 )"
190         "Final Note: A warning that no watchpoint command was generated when there \
191 are no syntax errors may indicate that a function was declared but never called.");
192 
193     CommandArgumentEntry arg;
194     CommandArgumentData wp_id_arg;
195 
196     // Define the first (and only) variant of this arg.
197     wp_id_arg.arg_type = eArgTypeWatchpointID;
198     wp_id_arg.arg_repetition = eArgRepeatPlain;
199 
200     // There is only one variant this argument could be; put it into the
201     // argument entry.
202     arg.push_back(wp_id_arg);
203 
204     // Push the data for the first argument into the m_arguments vector.
205     m_arguments.push_back(arg);
206   }
207 
208   ~CommandObjectWatchpointCommandAdd() override = default;
209 
210   Options *GetOptions() override { return &m_options; }
211 
212   void IOHandlerActivated(IOHandler &io_handler, bool interactive) override {
213     StreamFileSP output_sp(io_handler.GetOutputStreamFileSP());
214     if (output_sp && interactive) {
215       output_sp->PutCString(
216           "Enter your debugger command(s).  Type 'DONE' to end.\n");
217       output_sp->Flush();
218     }
219   }
220 
221   void IOHandlerInputComplete(IOHandler &io_handler,
222                               std::string &line) override {
223     io_handler.SetIsDone(true);
224 
225     // The WatchpointOptions object is owned by the watchpoint or watchpoint
226     // location
227     WatchpointOptions *wp_options =
228         (WatchpointOptions *)io_handler.GetUserData();
229     if (wp_options) {
230       std::unique_ptr<WatchpointOptions::CommandData> data_up(
231           new WatchpointOptions::CommandData());
232       if (data_up) {
233         data_up->user_source.SplitIntoLines(line);
234         auto baton_sp = std::make_shared<WatchpointOptions::CommandBaton>(
235             std::move(data_up));
236         wp_options->SetCallback(WatchpointOptionsCallbackFunction, baton_sp);
237       }
238     }
239   }
240 
241   void CollectDataForWatchpointCommandCallback(WatchpointOptions *wp_options,
242                                                CommandReturnObject &result) {
243     m_interpreter.GetLLDBCommandsFromIOHandler(
244         "> ",        // Prompt
245         *this,       // IOHandlerDelegate
246         wp_options); // Baton for the "io_handler" that will be passed back into
247                      // our IOHandlerDelegate functions
248   }
249 
250   /// Set a one-liner as the callback for the watchpoint.
251   void SetWatchpointCommandCallback(WatchpointOptions *wp_options,
252                                     const char *oneliner) {
253     std::unique_ptr<WatchpointOptions::CommandData> data_up(
254         new WatchpointOptions::CommandData());
255 
256     // It's necessary to set both user_source and script_source to the
257     // oneliner. The former is used to generate callback description (as in
258     // watchpoint command list) while the latter is used for Python to
259     // interpret during the actual callback.
260     data_up->user_source.AppendString(oneliner);
261     data_up->script_source.assign(oneliner);
262     data_up->stop_on_error = m_options.m_stop_on_error;
263 
264     auto baton_sp =
265         std::make_shared<WatchpointOptions::CommandBaton>(std::move(data_up));
266     wp_options->SetCallback(WatchpointOptionsCallbackFunction, baton_sp);
267   }
268 
269   static bool
270   WatchpointOptionsCallbackFunction(void *baton,
271                                     StoppointCallbackContext *context,
272                                     lldb::user_id_t watch_id) {
273     bool ret_value = true;
274     if (baton == nullptr)
275       return true;
276 
277     WatchpointOptions::CommandData *data =
278         (WatchpointOptions::CommandData *)baton;
279     StringList &commands = data->user_source;
280 
281     if (commands.GetSize() > 0) {
282       ExecutionContext exe_ctx(context->exe_ctx_ref);
283       Target *target = exe_ctx.GetTargetPtr();
284       if (target) {
285         Debugger &debugger = target->GetDebugger();
286         CommandReturnObject result(debugger.GetUseColor());
287 
288         // Rig up the results secondary output stream to the debugger's, so the
289         // output will come out synchronously if the debugger is set up that
290         // way.
291         StreamSP output_stream(debugger.GetAsyncOutputStream());
292         StreamSP error_stream(debugger.GetAsyncErrorStream());
293         result.SetImmediateOutputStream(output_stream);
294         result.SetImmediateErrorStream(error_stream);
295 
296         CommandInterpreterRunOptions options;
297         options.SetStopOnContinue(true);
298         options.SetStopOnError(data->stop_on_error);
299         options.SetEchoCommands(false);
300         options.SetPrintResults(true);
301         options.SetPrintErrors(true);
302         options.SetAddToHistory(false);
303 
304         debugger.GetCommandInterpreter().HandleCommands(commands, &exe_ctx,
305                                                         options, result);
306         result.GetImmediateOutputStream()->Flush();
307         result.GetImmediateErrorStream()->Flush();
308       }
309     }
310     return ret_value;
311   }
312 
313   class CommandOptions : public Options {
314   public:
315     CommandOptions()
316         : Options(), m_use_commands(false), m_use_script_language(false),
317           m_script_language(eScriptLanguageNone), m_use_one_liner(false),
318           m_one_liner(), m_function_name() {}
319 
320     ~CommandOptions() override = default;
321 
322     Status SetOptionValue(uint32_t option_idx, llvm::StringRef option_arg,
323                           ExecutionContext *execution_context) override {
324       Status error;
325       const int short_option = m_getopt_table[option_idx].val;
326 
327       switch (short_option) {
328       case 'o':
329         m_use_one_liner = true;
330         m_one_liner = std::string(option_arg);
331         break;
332 
333       case 's':
334         m_script_language = (lldb::ScriptLanguage)OptionArgParser::ToOptionEnum(
335             option_arg, GetDefinitions()[option_idx].enum_values,
336             eScriptLanguageNone, error);
337 
338         switch (m_script_language) {
339         case eScriptLanguagePython:
340         case eScriptLanguageLua:
341           m_use_script_language = true;
342           break;
343         case eScriptLanguageNone:
344         case eScriptLanguageUnknown:
345           m_use_script_language = false;
346           break;
347         }
348         break;
349 
350       case 'e': {
351         bool success = false;
352         m_stop_on_error =
353             OptionArgParser::ToBoolean(option_arg, false, &success);
354         if (!success)
355           error.SetErrorStringWithFormat(
356               "invalid value for stop-on-error: \"%s\"",
357               option_arg.str().c_str());
358       } break;
359 
360       case 'F':
361         m_use_one_liner = false;
362         m_function_name.assign(std::string(option_arg));
363         break;
364 
365       default:
366         llvm_unreachable("Unimplemented option");
367       }
368       return error;
369     }
370 
371     void OptionParsingStarting(ExecutionContext *execution_context) override {
372       m_use_commands = true;
373       m_use_script_language = false;
374       m_script_language = eScriptLanguageNone;
375 
376       m_use_one_liner = false;
377       m_stop_on_error = true;
378       m_one_liner.clear();
379       m_function_name.clear();
380     }
381 
382     llvm::ArrayRef<OptionDefinition> GetDefinitions() override {
383       return llvm::makeArrayRef(g_watchpoint_command_add_options);
384     }
385 
386     // Instance variables to hold the values for command options.
387 
388     bool m_use_commands;
389     bool m_use_script_language;
390     lldb::ScriptLanguage m_script_language;
391 
392     // Instance variables to hold the values for one_liner options.
393     bool m_use_one_liner;
394     std::string m_one_liner;
395     bool m_stop_on_error;
396     std::string m_function_name;
397   };
398 
399 protected:
400   bool DoExecute(Args &command, CommandReturnObject &result) override {
401     Target *target = &GetSelectedTarget();
402 
403     const WatchpointList &watchpoints = target->GetWatchpointList();
404     size_t num_watchpoints = watchpoints.GetSize();
405 
406     if (num_watchpoints == 0) {
407       result.AppendError("No watchpoints exist to have commands added");
408       result.SetStatus(eReturnStatusFailed);
409       return false;
410     }
411 
412     if (!m_options.m_function_name.empty()) {
413       if (!m_options.m_use_script_language) {
414         m_options.m_script_language = GetDebugger().GetScriptLanguage();
415         m_options.m_use_script_language = true;
416       }
417     }
418 
419     std::vector<uint32_t> valid_wp_ids;
420     if (!CommandObjectMultiwordWatchpoint::VerifyWatchpointIDs(target, command,
421                                                                valid_wp_ids)) {
422       result.AppendError("Invalid watchpoints specification.");
423       result.SetStatus(eReturnStatusFailed);
424       return false;
425     }
426 
427     result.SetStatus(eReturnStatusSuccessFinishNoResult);
428     const size_t count = valid_wp_ids.size();
429     for (size_t i = 0; i < count; ++i) {
430       uint32_t cur_wp_id = valid_wp_ids.at(i);
431       if (cur_wp_id != LLDB_INVALID_WATCH_ID) {
432         Watchpoint *wp = target->GetWatchpointList().FindByID(cur_wp_id).get();
433         // Sanity check wp first.
434         if (wp == nullptr)
435           continue;
436 
437         WatchpointOptions *wp_options = wp->GetOptions();
438         // Skip this watchpoint if wp_options is not good.
439         if (wp_options == nullptr)
440           continue;
441 
442         // If we are using script language, get the script interpreter in order
443         // to set or collect command callback.  Otherwise, call the methods
444         // associated with this object.
445         if (m_options.m_use_script_language) {
446           ScriptInterpreter *script_interp = GetDebugger().GetScriptInterpreter(
447               /*can_create=*/true, m_options.m_script_language);
448           // Special handling for one-liner specified inline.
449           if (m_options.m_use_one_liner) {
450             script_interp->SetWatchpointCommandCallback(
451                 wp_options, m_options.m_one_liner.c_str());
452           }
453           // Special handling for using a Python function by name instead of
454           // extending the watchpoint callback data structures, we just
455           // automatize what the user would do manually: make their watchpoint
456           // command be a function call
457           else if (!m_options.m_function_name.empty()) {
458             std::string oneliner(m_options.m_function_name);
459             oneliner += "(frame, wp, internal_dict)";
460             script_interp->SetWatchpointCommandCallback(
461                 wp_options, oneliner.c_str());
462           } else {
463             script_interp->CollectDataForWatchpointCommandCallback(wp_options,
464                                                                    result);
465           }
466         } else {
467           // Special handling for one-liner specified inline.
468           if (m_options.m_use_one_liner)
469             SetWatchpointCommandCallback(wp_options,
470                                          m_options.m_one_liner.c_str());
471           else
472             CollectDataForWatchpointCommandCallback(wp_options, result);
473         }
474       }
475     }
476 
477     return result.Succeeded();
478   }
479 
480 private:
481   CommandOptions m_options;
482 };
483 
484 // CommandObjectWatchpointCommandDelete
485 
486 class CommandObjectWatchpointCommandDelete : public CommandObjectParsed {
487 public:
488   CommandObjectWatchpointCommandDelete(CommandInterpreter &interpreter)
489       : CommandObjectParsed(interpreter, "delete",
490                             "Delete the set of commands from a watchpoint.",
491                             nullptr, eCommandRequiresTarget) {
492     CommandArgumentEntry arg;
493     CommandArgumentData wp_id_arg;
494 
495     // Define the first (and only) variant of this arg.
496     wp_id_arg.arg_type = eArgTypeWatchpointID;
497     wp_id_arg.arg_repetition = eArgRepeatPlain;
498 
499     // There is only one variant this argument could be; put it into the
500     // argument entry.
501     arg.push_back(wp_id_arg);
502 
503     // Push the data for the first argument into the m_arguments vector.
504     m_arguments.push_back(arg);
505   }
506 
507   ~CommandObjectWatchpointCommandDelete() override = default;
508 
509 protected:
510   bool DoExecute(Args &command, CommandReturnObject &result) override {
511     Target *target = &GetSelectedTarget();
512 
513     const WatchpointList &watchpoints = target->GetWatchpointList();
514     size_t num_watchpoints = watchpoints.GetSize();
515 
516     if (num_watchpoints == 0) {
517       result.AppendError("No watchpoints exist to have commands deleted");
518       result.SetStatus(eReturnStatusFailed);
519       return false;
520     }
521 
522     if (command.GetArgumentCount() == 0) {
523       result.AppendError(
524           "No watchpoint specified from which to delete the commands");
525       result.SetStatus(eReturnStatusFailed);
526       return false;
527     }
528 
529     std::vector<uint32_t> valid_wp_ids;
530     if (!CommandObjectMultiwordWatchpoint::VerifyWatchpointIDs(target, command,
531                                                                valid_wp_ids)) {
532       result.AppendError("Invalid watchpoints specification.");
533       result.SetStatus(eReturnStatusFailed);
534       return false;
535     }
536 
537     result.SetStatus(eReturnStatusSuccessFinishNoResult);
538     const size_t count = valid_wp_ids.size();
539     for (size_t i = 0; i < count; ++i) {
540       uint32_t cur_wp_id = valid_wp_ids.at(i);
541       if (cur_wp_id != LLDB_INVALID_WATCH_ID) {
542         Watchpoint *wp = target->GetWatchpointList().FindByID(cur_wp_id).get();
543         if (wp)
544           wp->ClearCallback();
545       } else {
546         result.AppendErrorWithFormat("Invalid watchpoint ID: %u.\n", cur_wp_id);
547         result.SetStatus(eReturnStatusFailed);
548         return false;
549       }
550     }
551     return result.Succeeded();
552   }
553 };
554 
555 // CommandObjectWatchpointCommandList
556 
557 class CommandObjectWatchpointCommandList : public CommandObjectParsed {
558 public:
559   CommandObjectWatchpointCommandList(CommandInterpreter &interpreter)
560       : CommandObjectParsed(interpreter, "list",
561                             "List the script or set of commands to be executed "
562                             "when the watchpoint is hit.",
563                             nullptr, eCommandRequiresTarget) {
564     CommandArgumentEntry arg;
565     CommandArgumentData wp_id_arg;
566 
567     // Define the first (and only) variant of this arg.
568     wp_id_arg.arg_type = eArgTypeWatchpointID;
569     wp_id_arg.arg_repetition = eArgRepeatPlain;
570 
571     // There is only one variant this argument could be; put it into the
572     // argument entry.
573     arg.push_back(wp_id_arg);
574 
575     // Push the data for the first argument into the m_arguments vector.
576     m_arguments.push_back(arg);
577   }
578 
579   ~CommandObjectWatchpointCommandList() override = default;
580 
581 protected:
582   bool DoExecute(Args &command, CommandReturnObject &result) override {
583     Target *target = &GetSelectedTarget();
584 
585     const WatchpointList &watchpoints = target->GetWatchpointList();
586     size_t num_watchpoints = watchpoints.GetSize();
587 
588     if (num_watchpoints == 0) {
589       result.AppendError("No watchpoints exist for which to list commands");
590       result.SetStatus(eReturnStatusFailed);
591       return false;
592     }
593 
594     if (command.GetArgumentCount() == 0) {
595       result.AppendError(
596           "No watchpoint specified for which to list the commands");
597       result.SetStatus(eReturnStatusFailed);
598       return false;
599     }
600 
601     std::vector<uint32_t> valid_wp_ids;
602     if (!CommandObjectMultiwordWatchpoint::VerifyWatchpointIDs(target, command,
603                                                                valid_wp_ids)) {
604       result.AppendError("Invalid watchpoints specification.");
605       result.SetStatus(eReturnStatusFailed);
606       return false;
607     }
608 
609     result.SetStatus(eReturnStatusSuccessFinishNoResult);
610     const size_t count = valid_wp_ids.size();
611     for (size_t i = 0; i < count; ++i) {
612       uint32_t cur_wp_id = valid_wp_ids.at(i);
613       if (cur_wp_id != LLDB_INVALID_WATCH_ID) {
614         Watchpoint *wp = target->GetWatchpointList().FindByID(cur_wp_id).get();
615 
616         if (wp) {
617           const WatchpointOptions *wp_options = wp->GetOptions();
618           if (wp_options) {
619             // Get the callback baton associated with the current watchpoint.
620             const Baton *baton = wp_options->GetBaton();
621             if (baton) {
622               result.GetOutputStream().Printf("Watchpoint %u:\n", cur_wp_id);
623               baton->GetDescription(result.GetOutputStream().AsRawOstream(),
624                                     eDescriptionLevelFull,
625                                     result.GetOutputStream().GetIndentLevel() +
626                                         2);
627             } else {
628               result.AppendMessageWithFormat(
629                   "Watchpoint %u does not have an associated command.\n",
630                   cur_wp_id);
631             }
632           }
633           result.SetStatus(eReturnStatusSuccessFinishResult);
634         } else {
635           result.AppendErrorWithFormat("Invalid watchpoint ID: %u.\n",
636                                        cur_wp_id);
637           result.SetStatus(eReturnStatusFailed);
638         }
639       }
640     }
641 
642     return result.Succeeded();
643   }
644 };
645 
646 // CommandObjectWatchpointCommand
647 
648 CommandObjectWatchpointCommand::CommandObjectWatchpointCommand(
649     CommandInterpreter &interpreter)
650     : CommandObjectMultiword(
651           interpreter, "command",
652           "Commands for adding, removing and examining LLDB commands "
653           "executed when the watchpoint is hit (watchpoint 'commands').",
654           "command <sub-command> [<sub-command-options>] <watchpoint-id>") {
655   CommandObjectSP add_command_object(
656       new CommandObjectWatchpointCommandAdd(interpreter));
657   CommandObjectSP delete_command_object(
658       new CommandObjectWatchpointCommandDelete(interpreter));
659   CommandObjectSP list_command_object(
660       new CommandObjectWatchpointCommandList(interpreter));
661 
662   add_command_object->SetCommandName("watchpoint command add");
663   delete_command_object->SetCommandName("watchpoint command delete");
664   list_command_object->SetCommandName("watchpoint command list");
665 
666   LoadSubCommand("add", add_command_object);
667   LoadSubCommand("delete", delete_command_object);
668   LoadSubCommand("list", list_command_object);
669 }
670 
671 CommandObjectWatchpointCommand::~CommandObjectWatchpointCommand() = default;
672