void CmdWhere::onClientImpl(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() > 1) { help(client); return; } Array st = fetchStackTrace(client); if (st.empty()) { client.info("(no stacktrace to display or in global scope)"); client.info("if you hit serialization limit, consider do " "\"set sa off\" and then get the stack without args"); return; } // so list command can default to current frame client.moveToFrame(client.getFrame(), false); if (client.argCount() == 0) { int i = 0; for (ArrayIter iter(st); iter; ++iter) { client.printFrame(i, iter.second().toArray()); ++i; if (!client.isApiMode() && i % DebuggerClient::ScrollBlockSize == 0 && client.ask("There are %zd more frames. Continue? [Y/n]", st.size() - i) == 'n') { break; } } } else { string snum = client.argValue(1); int num = atoi(snum.c_str()); if (snum[0] == '-') { snum = snum.substr(1); } if (!DebuggerClient::IsValidNumber(snum)) { client.error("The argument, if specified, has to be numeric."); return; } if (num > 0) { for (int i = 0; i < num && i < st.size(); i++) { client.printFrame(i, st[i].toArray()); } } else if (num < 0) { for (int i = st.size() + num; i < st.size(); i++) { client.printFrame(i, st[i].toArray()); } } else { client.error("0 was specified for the number of frames"); client.tutorial( "The optional argument is the number of frames to print out. " "Use a positive number to print out innermost frames. Use a negative " "number to print out outermost frames." ); } } }
bool CmdMachine::processList(DebuggerClient &client, bool output /* = true */) { m_body = "list"; CmdMachinePtr res = client.xend<CmdMachine>(this); client.updateSandboxes(res->m_sandboxes); if (!output) return true; if (res->m_sandboxes.empty()) { client.info("(no sandbox was found)"); client.tutorial( "Please hit the sandbox from browser at least once. Then run " "'[m]achine [l]ist' again." ); } else { for (int i = 0; i < (int)res->m_sandboxes.size(); i++) { client.print(" %d\t%s", i + 1, res->m_sandboxes[i]->desc().c_str()); } client.tutorial( "Use '[m]achine [a]ttach {index}' to attach to one sandbox. For " "example, 'm a 1'. If desired sandbox is not on the list, please " "hit the sandbox from browser once. Then run '[m]achine [l]ist' " "again." ); } return true; }
bool CmdMachine::AttachSandbox(DebuggerClient &client, DSandboxInfoPtr sandbox, bool force /* = false */) { if (client.isLocal()) { client.error("Local script doesn't have sandbox to attach to."); return false; } CmdMachine cmd; cmd.m_body = "attach"; cmd.m_sandboxes.push_back(sandbox); cmd.m_force = force; client.info("Attaching to %s and pre-loading, please wait...", sandbox->desc().c_str()); CmdMachinePtr cmdMachine = client.xend<CmdMachine>(&cmd); if (cmdMachine->m_succeed) { client.playMacro("startup"); } else { // Note: it would be nice to give them more info about the process we think // is debugging this sandbox: what machine it's on, what it's pid is, etc. // Unfortunately, we don't have any of that data. We'd need a protocol // change to have the client give us more info when it attaches. client.error( "Failed to attach to the sandbox. Maybe another client is debugging, \n" "or a client failed to detach cleanly.\n" "You can attach to another sandbox, or exit the other attached client, \n" "or force this client to take over the sandbox with: \n" "\n" "\t[m]achine [a]ttach [f]orce %s %s" "\n", sandbox->m_user.c_str(), sandbox->m_name.c_str()); } return cmdMachine->m_succeed; }
void CmdVariable::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; String text; if (client.argCount() == 1) { text = client.argValue(1); } else if (client.argCount() != 0) { help(client); return; } if (client.isStackTraceAsync()) { m_type = KindOfVariableAsync; } m_frame = client.getFrame(); auto cmd = client.xend<CmdVariable>(this); if (cmd->m_variables.empty()) { client.info("(no variable was defined)"); } else { PrintVariables(client, cmd->m_variables, cmd->m_global ? -1 : m_frame, text, cmd->m_version); } }
bool CmdMachine::AttachSandbox(DebuggerClient &client, DSandboxInfoPtr sandbox, bool force /* = false */) { if (client.isLocal()) { client.error("Local script doesn't have sandbox to attach to."); return false; } CmdMachine cmd; cmd.m_body = "attach"; cmd.m_sandboxes.push_back(sandbox); cmd.m_force = force; client.info("Attaching to %s and pre-loading, please wait...", sandbox->desc().c_str()); CmdMachinePtr cmdMachine = client.xend<CmdMachine>(&cmd); if (cmdMachine->m_succeed) { client.playMacro("startup"); } else { client.error("failed to attach to sandbox, maybe another client is " "debugging, \nattach to another sandbox, exit the " "attached hphpd client, or try \n" "[m]achine [a]ttach [f]orce [%s] [%s]", sandbox->m_user.c_str(), sandbox->m_name.c_str()); } return cmdMachine->m_succeed; }
void CmdInternalTesting::onClientImpl(DebuggerClient &client) { TRACE(2, "CmdInternalTesting::onClientImpl\n"); if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() == 0) { help(client); return; } client.info("Executing internal test..."); m_arg = client.argValue(1); if (client.arg(1, "badcmdtypesend")) { // Give the cmd a bad type and send it over. This should cause the proxy to // disconnect from us. m_type = KindOfInternalTestingBad; client.sendToServer(this); // Spin here and wait for the client to be marked as stopped // before going back to the event loop. This will give the local // proxy time to recgonize the bad cmd, terminate, and wait for // the client to stop. This will ensure that we always exit on the // same path on both proxy and client threads, and remove any // spurious output form ths test case. while (!client.internalTestingIsClientStopped()) { sleep(1); } throw DebuggerConsoleExitException(); // Expect no response } else if (client.arg(1, "badcmdtypereceive")) { client.xend<CmdInternalTesting>(this); return; } else if (client.arg(1, "shortcmdsend")) { m_arg = "shortcmd"; // Force send to drop a field. client.sendToServer(this); // See note above about this wait. while (!client.internalTestingIsClientStopped()) { sleep(1); } throw DebuggerConsoleExitException(); // Expect no response } else if (client.arg(1, "shortcmdreceive")) { client.xend<CmdInternalTesting>(this); return; } else if (client.arg(1, "segfaultClient")) { int *px = nullptr; *px = 42; } else if (client.arg(1, "segfaultServer")) { client.xend<CmdInternalTesting>(this); return; } help(client); }
void CmdZend::onClientImpl(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() == 0) { const std::string &code = client.getCode(); if (!code.empty()) { const std::string zendExe = client.getZendExecutable(); client.info("Executing last PHP block with \"%s\"...", zendExe.c_str()); string out; Process::Exec(zendExe.c_str(), nullptr, code.c_str(), out, &out, true); client.print(out); return; } } help(client); }
void CmdGlobal::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; String text; if (client.argCount() == 1) { text = client.argValue(1); } else if (client.argCount() != 0) { help(client); return; } auto cmd = client.xend<CmdGlobal>(this); if (cmd->m_globals.empty()) { client.info("(no global variable was found)"); } else { CmdVariable::PrintVariables(client, cmd->m_globals, -1, text, cmd->m_version); } }
void CmdVariable::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; String text; if (client.argCount() == 1) { text = client.argValue(1); } else if (client.argCount() != 0) { help(client); return; } m_frame = client.getFrame(); CmdVariablePtr cmd = client.xend<CmdVariable>(this); if (cmd->m_variables.empty()) { client.info("(no variable was defined)"); } else { m_variables = cmd->m_variables; PrintVariables(client, cmd->m_variables, cmd->m_global, text); } }
void CmdVariable::PrintVariables(DebuggerClient &client, CArrRef variables, bool global, CStrRef text) { bool system = true; int i = 0; bool found = false; for (ArrayIter iter(variables); iter; ++iter) { String name = iter.first().toString(); String value = DebuggerClient::FormatVariable(iter.second(), 200); if (!text.empty()) { String fullvalue = DebuggerClient::FormatVariable(iter.second(), -1); if (name.find(text, 0, false) >= 0 || fullvalue.find(text, 0, false) >= 0) { client.print("%s = %s", name.data(), value.data()); found = true; } } else { if (global && system) { client.print("$%s = %s", name.data(), value.data()); } else { client.output("$%s = %s", name.data(), value.data()); } // we knew this is the last system global if (global && name == "http_response_header") { client.output("%s", ""); system = false; } ++i; if (i % DebuggerClient::ScrollBlockSize == 0 && client.ask("There are %zd more variables. Continue? [Y/n]", variables.size() - i) == 'n') { break; } } } if (!text.empty() && !found) { client.info("(unable to find specified text in any variables)"); } }
// Sends this list command to the server to retrieve the source to be listed // and then displays the source on the client. The client's current line // is then updated to point to the last listed line. // Returns false if the server was unable to return source for this command. bool CmdList::listFileRange(DebuggerClient &client, int lineFocus0, int charFocus0, int lineFocus1, int charFocus1) { if (m_line1 <= 0) m_line1 = 1; if (m_line2 <= 0) m_line2 = 1; if (m_line1 > m_line2) { int32_t tmp = m_line1; m_line1 = m_line2; m_line2 = tmp; } auto res = client.xend<CmdList>(this); if (res->m_code.isString()) { if (!client.code(res->m_code.toString(), m_line1, m_line2, lineFocus0, charFocus0, lineFocus1, charFocus1)) { client.info("No more lines in %s to display.", m_file.c_str()); } client.setListLocation(m_file, m_line2, false); return true; } return false; }
void CmdPrint::processClear(DebuggerClient &client) { DebuggerClient::WatchPtrVec &watches = client.getWatches(); if (watches.empty()) { client.error("There is no watch expression to clear."); client.tutorial( "Use '[p]rint [a]lways ...' to set new watch expressions. " "Use '[p]rint ?|[h]elp' to read how to set them. " ); return; } if (client.arg(2, "all")) { watches.clear(); client.info("All watch expressions are cleared."); return; } string snum = client.argValue(2); if (!DebuggerClient::IsValidNumber(snum)) { client.error("'[p]rint [c]lear' needs an {index} argument."); client.tutorial( "You will have to run '[p]rint [l]ist' first to see a list of valid " "numbers or indices to specify." ); return; } int num = atoi(snum.c_str()) - 1; if (num < 0 || num >= (int)watches.size()) { client.error("\"%s\" is not a valid index. Choose one from this list:", snum.c_str()); processList(client); return; } watches.erase(watches.begin() + num); }
void CmdWhere::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() > 2) { help(client); return; } int argBase = 1; if ((client.argCount() > 0) && client.arg(argBase, "async")) { // We use a different command type for an async stack trace, so we // can both send and receive different data and still keep the // existing Where command unchanged. This ensures that old clients // can still get a stack trace from a newer server, and vice // versa. m_type = KindOfWhereAsync; argBase++; client.info("Fetching async stacktrace..."); } Array st = fetchStackTrace(client); if (st.empty()) { if (m_type != KindOfWhereAsync) { client.info("(no stacktrace to display or in global scope)"); client.info("If you hit the serialization limit, try " "\"set sa off\" to get the stack without args"); } else { client.info("(no async stacktrace to display)"); } return; } // so list command can default to current frame client.moveToFrame(client.getFrame(), false); if (client.argCount() < argBase) { int i = 0; for (ArrayIter iter(st); iter; ++iter) { client.printFrame(i, iter.second().toArray()); ++i; if (i % DebuggerClient::ScrollBlockSize == 0 && client.ask("There are %zd more frames. Continue? [Y/n]", st.size() - i) == 'n') { break; } } } else { std::string snum = client.argValue(argBase); int num = atoi(snum.c_str()); if (snum[0] == '-') { snum = snum.substr(1); } if (!DebuggerClient::IsValidNumber(snum)) { client.error("The argument, if specified, has to be numeric."); return; } if (num > 0) { for (int i = 0; i < num && i < st.size(); i++) { client.printFrame(i, st[i].toArray()); } } else if (num < 0) { for (int i = st.size() + num; i < st.size(); i++) { client.printFrame(i, st[i].toArray()); } } else { client.error("0 was specified for the number of frames"); client.tutorial( "The optional argument is the number of frames to print out. " "Use a positive number to print out innermost frames. Use a negative " "number to print out outermost frames." ); } } }
void CmdVariable::PrintVariables(DebuggerClient &client, CArrRef variables, int frame, const String& text, int version) { bool global = frame == -1; // I.e. we were called from CmdGlobal, or the //client's current frame is the global frame, according to OnServer bool system = true; int i = 0; bool found = false; for (ArrayIter iter(variables); iter; ++iter) { String name = iter.first().toString(); String value; if (version == 2) { // Using the new protocol, so variables contain only names. // Fetch the value separately. CmdVariable cmd; cmd.m_frame = frame; cmd.m_variables = null_array; cmd.m_varName = name; cmd.m_filter = text; cmd.m_version = 2; auto rcmd = client.xend<CmdVariable>(&cmd); if (!rcmd->m_variables.empty()) { value = DebuggerClient::FormatVariable(rcmd->m_variables[name], 200); found = true; } else if (text.empty()) { // Not missing because filtered out, assume the value is too large. value = s_omitted; found = true; } else { if (name.find(text, 0, false) >= 0) { // Server should have matched it. // Assume missing because value is too large. value = s_omitted; found = true; } else { // The variable was filtered out on the server, using text. // Or it was just too large. Either way we let skip over it. continue; } } } else { value = DebuggerClient::FormatVariable(iter.second(), 200); } if (version == 0 && !text.empty()) { if (name.find(text, 0, false) >= 0) { client.print("%s = %s", name.data(), value.data()); found = true; } else { String fullvalue = DebuggerClient::FormatVariable(value, -1); if (fullvalue.find(text, 0, false) >= 0) { client.print("%s = %s", name.data(), value.data()); found = true; } } } else { if (global && system) { client.print("$%s = %s", name.data(), value.data()); } else { client.output("$%s = %s", name.data(), value.data()); } // we know s_http_response_header is the last system global if (global && name == s_http_response_header) { client.output("%s", ""); system = false; } ++i; if (i % DebuggerClient::ScrollBlockSize == 0 && client.ask("There are %zd more variables. Continue? [Y/n]", variables.size() - i) == 'n') { break; } } } if (!text.empty() && !found) { client.info("(unable to find specified text in any variables)"); } }
void CmdThread::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() > 1) { help(client); return; } if (client.argCount() == 0) { m_body = "info"; auto res = client.xend<CmdThread>(this); client.print(res->m_out); } else if (client.arg(1, "list")) { processList(client); } else if (client.arg(1, "normal")) { m_body = "normal"; client.sendToServer(this); client.info("Thread is running in normal mode now. Other threads will " "interleave when they hit breakpoints as well."); } else if (client.arg(1, "sticky")) { m_body = "sticky"; client.sendToServer(this); client.info("Thread is running in sticky mode now. All other threads " "will wait until this thread finishes, when they hit " "breakpoints."); } else if (client.arg(1, "exclusive")) { m_body = "exclusive"; client.sendToServer(this); client.info("Thread is running in exclusive mode now. All other threads " "will not break, even when they hit breakpoints."); } else { std::string snum = client.argValue(1); if (!DebuggerClient::IsValidNumber(snum)) { client.error("'[t]hread {index}' needs a numeric argument."); client.tutorial( "You will have to run '[t]hread [l]ist' first to see a list of valid " "numbers or indices to specify. Thread 1 is always your current " "thread. If that's the only thread on the list, you do not have " "another thread at break to switch to." ); return; } int num = atoi(snum.c_str()); DThreadInfoPtr thread = client.getThread(num); if (!thread) { processList(client, false); thread = client.getThread(num); if (!thread) { client.error("\"%s\" is not a valid thread index. Choose one from " "this list:", snum.c_str()); processList(client); return; } } if (thread->m_id == client.getCurrentThreadId()) { client.info("This is your current thread already."); return; } m_body = "switch"; m_threads.push_back(thread); client.sendToServer(this); throw DebuggerConsoleExitException(); } }
// Checks the command arguments, report errors and returning as appropriate. // Then communicates with the server to retrieve source information. Also // retrieves and updates location information stored in the client. void CmdList::onClient(DebuggerClient &client) { if (DebuggerCommand::displayedHelp(client)) return; if (client.argCount() > 1) { help(client); return; } int line = 0; m_line1 = m_line2 = 0; if (client.argCount() == 1) { std::string arg = client.argValue(1); if (DebuggerClient::IsValidNumber(arg)) { line = atoi(arg.c_str()); if (line <= 0) { client.error("A line number has to be a positive integer."); help(client); return; } m_line1 = line - DebuggerClient::CodeBlockSize/2; m_line2 = m_line1 + DebuggerClient::CodeBlockSize; } else if (arg.find("::") != std::string::npos) { if (!listFunctionOrClass(client)) { client.error("Unable to read specified method."); } return; } else { size_t pos = arg.find(':'); if (pos != std::string::npos) { m_file = arg.substr(0, pos); if (m_file.empty()) { client.error("File name cannot be empty."); help(client); return; } arg = arg.substr(pos + 1); } pos = arg.find('-'); if (pos != std::string::npos) { std::string line1 = arg.substr(0, pos); std::string line2 = arg.substr(pos + 1); if (!DebuggerClient::IsValidNumber(line1) || !DebuggerClient::IsValidNumber(line2)) { if (m_file.empty()) { m_file = arg; m_line1 = 1; m_line2 = DebuggerClient::CodeBlockSize; } else { client.error("Line numbers have to be integers."); help(client); return; } } else { m_line1 = atoi(line1.c_str()); m_line2 = atoi(line2.c_str()); if (line1.empty()) { m_line1 = m_line2 - DebuggerClient::CodeBlockSize; } if (line2.empty()) { m_line2 = m_line1 + DebuggerClient::CodeBlockSize; } if (m_line1 <= 0 || m_line2 <= 0) { client.error("Line numbers have to be positive integers."); help(client); return; } } } else { if (!DebuggerClient::IsValidNumber(arg)) { if (m_file.empty()) { if (client.argCount() == 1 && listFunctionOrClass(client)) { return; } m_file = arg; m_line1 = 1; m_line2 = DebuggerClient::CodeBlockSize; } else { client.error("A line number has to be an integer."); help(client); return; } } else { int line = atoi(arg.c_str()); if (line <= 0) { client.error("A line number has to be a positive integer."); help(client); return; } m_line1 = line - DebuggerClient::CodeBlockSize/2; m_line2 = m_line1 + DebuggerClient::CodeBlockSize; } } } } int charFocus0 = 0; int lineFocus1 = 0; int charFocus1 = 0; if (m_file.empty()) { getListLocation(client, line, charFocus0, lineFocus1, charFocus1); if (m_file.empty()) { listEvalCode(client); return; } } else if (m_file[0] == '/') { struct stat sb; stat(m_file.c_str(), &sb); if ((sb.st_mode & S_IFMT) == S_IFDIR) { client.setSourceRoot(m_file); client.info("PHP source root directory is set to %s", m_file.c_str()); return; } } if (!listFileRange(client, line, charFocus0, lineFocus1, charFocus1)) { client.error( "Unable to read specified function, class or source file location."); } }
void CmdVariable::PrintVariables(DebuggerClient &client, const Array& variables, int frame, const String& text, int version) { bool global = frame == -1; // I.e. we were called from CmdGlobal, or the //client's current frame is the global frame, according to OnServer bool system = true; int i = 0; bool found = false; always_assert(version == 2); for (ArrayIter iter(variables); iter; ++iter) { auto const name = iter.first().toString(); String value; // Using the new protocol, so variables contain only names. Fetch the value // separately. CmdVariable cmd(client.isStackTraceAsync() ? KindOfVariableAsync : KindOfVariable); cmd.m_frame = frame; cmd.m_variables.reset(); cmd.m_varName = name; cmd.m_filter = text; cmd.m_formatMaxLen = 200; cmd.m_version = 2; auto rcmd = client.xend<CmdVariable>(&cmd); if (!rcmd->m_variables.empty()) { assert(rcmd->m_variables[name].isString()); value = rcmd->m_variables[name].toString(); found = true; } else if (text.empty()) { // Not missing because filtered out, assume the value is too large. value = s_omitted; found = true; } else if (name.find(text, 0, false) >= 0) { // Server should have matched it. Assume missing because value is too // large. value = s_omitted; found = true; } else { // The variable was filtered out on the server, using text. Or it was // just too large. Either way we skip over it. continue; } if (global && system) { client.print("$%s = %s", name.data(), value.data()); } else { client.output("$%s = %s", name.data(), value.data()); } // We know s_HTTP_RAW_POST_DATA is the last system global. if (global && name == s_HTTP_RAW_POST_DATA) { client.output("%s", ""); system = false; } ++i; if (i % DebuggerClient::ScrollBlockSize == 0 && client.ask("There are %zd more variables. Continue? [Y/n]", variables.size() - i) == 'n') { break; } } if (!text.empty() && !found) { client.info("(unable to find specified text in any variables)"); } }
void CmdInterrupt::onClient(DebuggerClient &client) { client.setCurrentLocation(m_threadId, m_bpi); if (!client.getDebuggerClientSmallStep()) { // Adjust line and char if it's not small stepping if (m_bpi->m_line1 == m_bpi->m_line2) { m_bpi->m_char1 = 1; m_bpi->m_char2 = 100; } } client.setMatchedBreakPoints(m_matched); switch (m_interrupt) { case SessionStarted: if (!m_program.empty()) { client.info("Program %s loaded. Type '[r]un' or '[c]ontinue' to go.", m_program.c_str()); m_bpi->m_file = m_program; } break; case SessionEnded: if (!m_program.empty()) { client.info("Program %s exited normally.", m_program.c_str()); } break; case RequestStarted: if (!m_program.empty()) { client.info("Web request %s started.", m_program.c_str()); } break; case RequestEnded: if (!m_program.empty()) { client.info("Web request %s ended.", m_program.c_str()); } break; case PSPEnded: if (!m_program.empty()) { client.info("Post-Send Processing for %s was ended.", m_program.c_str()); } break; case HardBreakPoint: case BreakPointReached: case ExceptionThrown: { bool found = false; bool toggled = false; auto *bps = client.getBreakPoints(); for (unsigned int i = 0; i < m_matched.size(); i++) { BreakPointInfoPtr bpm = m_matched[i]; BreakPointInfoPtr bp; int index = 0; for (; index < (int)bps->size(); index++) { if (bpm->same((*bps)[index])) { bp = (*bps)[index]; break; } } if (bp) { found = true; if (bp->m_state == BreakPointInfo::Once) { bp->m_state = BreakPointInfo::Disabled; toggled = true; } if (m_interrupt == BreakPointReached || m_interrupt == HardBreakPoint) { client.info("Breakpoint %d reached %s", bp->index(), m_bpi->site().c_str()); client.shortCode(m_bpi); } else { if (m_bpi->m_exceptionClass == BreakPointInfo::ErrorClassName) { client.info("Breakpoint %d reached: An error occurred %s", bp->index(), m_bpi->site().c_str()); client.shortCode(m_bpi); client.error("Error Message: %s", m_bpi->m_exceptionObject.c_str()); } else { client.info("Breakpoint %d reached: Throwing %s %s", bp->index(), m_bpi->m_exceptionClass.c_str(), m_bpi->site().c_str()); client.shortCode(m_bpi); if (client.getLogFileHandler()) { client.output(m_bpi->m_exceptionObject); } } } if (!bpm->m_output.empty()) { client.print(bpm->m_output); } } } if (toggled) { CmdBreak::SendClientBreakpointListToServer(client); } if (!found) { if (m_interrupt == HardBreakPoint) { // for HardBreakPoint, default the frame to the caller client.setFrame(1); } client.info("Break %s", m_bpi->site().c_str()); client.shortCode(m_bpi); } break; } } if (!m_errorMsg.empty()) { client.error(m_errorMsg); } // watches switch (m_interrupt) { case SessionStarted: case RequestStarted: break; default: { DebuggerClient::WatchPtrVec &watches = client.getWatches(); for (int i = 0; i < (int)watches.size(); i++) { if (i > 0) client.output("%s", ""); client.info("Watch %d: %s =", i + 1, watches[i]->second.c_str()); Variant v = CmdPrint().processWatch(client, watches[i]->first, watches[i]->second); client.output(CmdPrint::FormatResult(watches[i]->first, v)); } } } }