InteractiveCommands.cpp 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380
  1. /*
  2. * Copyright (c) 2022 Project CHIP Authors
  3. * All rights reserved.
  4. *
  5. * Licensed under the Apache License, Version 2.0 (the "License");
  6. * you may not use this file except in compliance with the License.
  7. * You may obtain a copy of the License at
  8. *
  9. * http://www.apache.org/licenses/LICENSE-2.0
  10. *
  11. * Unless required by applicable law or agreed to in writing, software
  12. * distributed under the License is distributed on an "AS IS" BASIS,
  13. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  14. * See the License for the specific language governing permissions and
  15. * limitations under the License.
  16. *
  17. */
  18. #include "InteractiveCommands.h"
  19. #include <platform/logging/LogV.h>
  20. #include <editline.h>
  21. constexpr const char * kInteractiveModePrompt = ">>> ";
  22. constexpr const char * kInteractiveModeHistoryFilePath = "/tmp/chip_tool_history";
  23. constexpr const char * kInteractiveModeStopCommand = "quit()";
  24. constexpr const char * kCategoryError = "Error";
  25. constexpr const char * kCategoryProgress = "Info";
  26. constexpr const char * kCategoryDetail = "Debug";
  27. namespace {
  28. void ClearLine()
  29. {
  30. printf("\r\x1B[0J"); // Move cursor to the beginning of the line and clear from cursor to end of the screen
  31. }
  32. void ENFORCE_FORMAT(3, 0) LoggingCallback(const char * module, uint8_t category, const char * msg, va_list args)
  33. {
  34. ClearLine();
  35. chip::Logging::Platform::LogV(module, category, msg, args);
  36. ClearLine();
  37. }
  38. class ScopedLock
  39. {
  40. public:
  41. ScopedLock(std::mutex & mutex) : mMutex(mutex) { mMutex.lock(); }
  42. ~ScopedLock() { mMutex.unlock(); }
  43. private:
  44. std::mutex & mMutex;
  45. };
  46. struct InteractiveServerResultLog
  47. {
  48. std::string module;
  49. std::string message;
  50. std::string messageType;
  51. };
  52. struct InteractiveServerResult
  53. {
  54. bool mEnabled = false;
  55. bool mIsAsyncReport = false;
  56. uint16_t mTimeout = 0;
  57. int mStatus = EXIT_SUCCESS;
  58. std::vector<std::string> mResults;
  59. std::vector<InteractiveServerResultLog> mLogs;
  60. // The InteractiveServerResult instance (gInteractiveServerResult) is initially
  61. // accessed on the main thread in InteractiveServerCommand::RunCommand, which is
  62. // when chip-tool starts in 'interactive server' mode.
  63. //
  64. // Then command results are normally sent over the wire onto the main thread too
  65. // when a command is received over WebSocket in InteractiveServerCommand::OnWebSocketMessageReceived
  66. // which for most cases runs a command onto the chip thread and block until
  67. // it is resolved (or until it timeouts).
  68. //
  69. // But in the meantime, when some parts of the command result happens, it is appended
  70. // to the mResults vector onto the chip thread.
  71. //
  72. // For empty commands, which means that the test suite is *waiting* for some events
  73. // (e.g a subscription report), the command results are sent over the chip thread
  74. // (this is the isAsyncReport use case).
  75. //
  76. // Finally, logs can be appended from either the chip thread or the main thread.
  77. //
  78. // This class should be refactored to abstract that properly and reduce the scope of
  79. // of the mutex, but in the meantime, the access to the members of this class are
  80. // protected by a mutex.
  81. std::mutex mMutex;
  82. void Setup(bool isAsyncReport, uint16_t timeout)
  83. {
  84. auto lock = ScopedLock(mMutex);
  85. mEnabled = true;
  86. mIsAsyncReport = isAsyncReport;
  87. mTimeout = timeout;
  88. if (mIsAsyncReport && mTimeout)
  89. {
  90. chip::DeviceLayer::PlatformMgr().ScheduleWork(StartAsyncTimeout, reinterpret_cast<intptr_t>(this));
  91. }
  92. }
  93. void Reset()
  94. {
  95. auto lock = ScopedLock(mMutex);
  96. if (mIsAsyncReport && mTimeout)
  97. {
  98. chip::DeviceLayer::PlatformMgr().ScheduleWork(StopAsyncTimeout, reinterpret_cast<intptr_t>(this));
  99. }
  100. mEnabled = false;
  101. mIsAsyncReport = false;
  102. mTimeout = 0;
  103. mStatus = EXIT_SUCCESS;
  104. mResults.clear();
  105. mLogs.clear();
  106. }
  107. bool IsAsyncReport()
  108. {
  109. auto lock = ScopedLock(mMutex);
  110. return mIsAsyncReport;
  111. }
  112. void MaybeAddLog(const char * module, uint8_t category, const char * base64Message)
  113. {
  114. auto lock = ScopedLock(mMutex);
  115. VerifyOrReturn(mEnabled);
  116. const char * messageType = nullptr;
  117. switch (category)
  118. {
  119. case chip::Logging::kLogCategory_Error:
  120. messageType = kCategoryError;
  121. break;
  122. case chip::Logging::kLogCategory_Progress:
  123. messageType = kCategoryProgress;
  124. break;
  125. case chip::Logging::kLogCategory_Detail:
  126. messageType = kCategoryDetail;
  127. break;
  128. default:
  129. // This should not happen.
  130. chipDie();
  131. break;
  132. }
  133. mLogs.push_back(InteractiveServerResultLog({ module, base64Message, messageType }));
  134. }
  135. void MaybeAddResult(const char * result)
  136. {
  137. auto lock = ScopedLock(mMutex);
  138. VerifyOrReturn(mEnabled);
  139. mResults.push_back(result);
  140. }
  141. std::string AsJsonString()
  142. {
  143. auto lock = ScopedLock(mMutex);
  144. std::stringstream content;
  145. content << "{";
  146. content << " \"results\": [";
  147. if (mResults.size())
  148. {
  149. for (const auto & result : mResults)
  150. {
  151. content << result << ",";
  152. }
  153. // Remove last comma.
  154. content.seekp(-1, std::ios_base::end);
  155. }
  156. if (mStatus != EXIT_SUCCESS)
  157. {
  158. if (mResults.size())
  159. {
  160. content << ",";
  161. }
  162. content << "{ \"error\": \"FAILURE\" }";
  163. }
  164. content << "],";
  165. content << "\"logs\": [";
  166. if (mLogs.size())
  167. {
  168. for (const auto & log : mLogs)
  169. {
  170. content << "{"
  171. " \"module\": \"" +
  172. log.module +
  173. "\","
  174. " \"category\": \"" +
  175. log.messageType +
  176. "\","
  177. " \"message\": \"" +
  178. log.message +
  179. "\""
  180. "},";
  181. }
  182. // Remove last comma.
  183. content.seekp(-1, std::ios_base::end);
  184. }
  185. content << "]";
  186. content << "}";
  187. return content.str();
  188. }
  189. static void StartAsyncTimeout(intptr_t arg)
  190. {
  191. auto self = reinterpret_cast<InteractiveServerResult *>(arg);
  192. auto timeout = chip::System::Clock::Seconds16(self->mTimeout);
  193. chip::DeviceLayer::SystemLayer().StartTimer(timeout, OnAsyncTimeout, self);
  194. }
  195. static void StopAsyncTimeout(intptr_t arg)
  196. {
  197. auto self = reinterpret_cast<InteractiveServerResult *>(arg);
  198. chip::DeviceLayer::SystemLayer().CancelTimer(OnAsyncTimeout, self);
  199. }
  200. static void OnAsyncTimeout(chip::System::Layer *, void * appState)
  201. {
  202. RemoteDataModelLogger::LogErrorAsJSON(CHIP_ERROR_TIMEOUT);
  203. }
  204. };
  205. InteractiveServerResult gInteractiveServerResult;
  206. void ENFORCE_FORMAT(3, 0) InteractiveServerLoggingCallback(const char * module, uint8_t category, const char * msg, va_list args)
  207. {
  208. va_list args_copy;
  209. va_copy(args_copy, args);
  210. chip::Logging::Platform::LogV(module, category, msg, args);
  211. char message[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE];
  212. vsnprintf(message, sizeof(message), msg, args_copy);
  213. va_end(args_copy);
  214. char base64Message[CHIP_CONFIG_LOG_MESSAGE_MAX_SIZE * 2] = {};
  215. chip::Base64Encode(chip::Uint8::from_char(message), static_cast<uint16_t>(strlen(message)), base64Message);
  216. gInteractiveServerResult.MaybeAddLog(module, category, base64Message);
  217. }
  218. char * GetCommand(char * command)
  219. {
  220. if (command != nullptr)
  221. {
  222. free(command);
  223. command = nullptr;
  224. }
  225. command = readline(kInteractiveModePrompt);
  226. // Do not save empty lines
  227. if (command != nullptr && *command)
  228. {
  229. add_history(command);
  230. write_history(kInteractiveModeHistoryFilePath);
  231. }
  232. return command;
  233. }
  234. } // namespace
  235. CHIP_ERROR InteractiveServerCommand::RunCommand()
  236. {
  237. // Logs needs to be redirected in order to refresh the screen appropriately when something
  238. // is dumped to stdout while the user is typing a command.
  239. chip::Logging::SetLogRedirectCallback(InteractiveServerLoggingCallback);
  240. RemoteDataModelLogger::SetDelegate(this);
  241. ReturnErrorOnFailure(mWebSocketServer.Run(mPort, this));
  242. gInteractiveServerResult.Reset();
  243. SetCommandExitStatus(CHIP_NO_ERROR);
  244. return CHIP_NO_ERROR;
  245. }
  246. bool InteractiveServerCommand::OnWebSocketMessageReceived(char * msg)
  247. {
  248. bool isAsyncReport = strlen(msg) == 0;
  249. uint16_t timeout = 0;
  250. if (!isAsyncReport && strlen(msg) <= 5 /* Only look for numeric values <= 65535 */)
  251. {
  252. std::stringstream ss;
  253. ss << msg;
  254. ss >> timeout;
  255. if (!ss.fail())
  256. {
  257. isAsyncReport = true;
  258. }
  259. }
  260. gInteractiveServerResult.Setup(isAsyncReport, timeout);
  261. VerifyOrReturnValue(!isAsyncReport, true);
  262. auto shouldStop = ParseCommand(msg, &gInteractiveServerResult.mStatus);
  263. mWebSocketServer.Send(gInteractiveServerResult.AsJsonString().c_str());
  264. gInteractiveServerResult.Reset();
  265. return shouldStop;
  266. }
  267. CHIP_ERROR InteractiveServerCommand::LogJSON(const char * json)
  268. {
  269. gInteractiveServerResult.MaybeAddResult(json);
  270. if (gInteractiveServerResult.IsAsyncReport())
  271. {
  272. mWebSocketServer.Send(gInteractiveServerResult.AsJsonString().c_str());
  273. gInteractiveServerResult.Reset();
  274. }
  275. return CHIP_NO_ERROR;
  276. }
  277. CHIP_ERROR InteractiveStartCommand::RunCommand()
  278. {
  279. read_history(kInteractiveModeHistoryFilePath);
  280. // Logs needs to be redirected in order to refresh the screen appropriately when something
  281. // is dumped to stdout while the user is typing a command.
  282. chip::Logging::SetLogRedirectCallback(LoggingCallback);
  283. char * command = nullptr;
  284. int status;
  285. while (true)
  286. {
  287. command = GetCommand(command);
  288. if (command != nullptr && !ParseCommand(command, &status))
  289. {
  290. break;
  291. }
  292. }
  293. if (command != nullptr)
  294. {
  295. free(command);
  296. command = nullptr;
  297. }
  298. SetCommandExitStatus(CHIP_NO_ERROR);
  299. return CHIP_NO_ERROR;
  300. }
  301. bool InteractiveCommand::ParseCommand(char * command, int * status)
  302. {
  303. if (strcmp(command, kInteractiveModeStopCommand) == 0)
  304. {
  305. // If scheduling the cleanup fails, there is not much we can do.
  306. // But if something went wrong while the application is leaving it could be because things have
  307. // not been cleaned up properly, so it is still useful to log the failure.
  308. LogErrorOnFailure(chip::DeviceLayer::PlatformMgr().ScheduleWork(ExecuteDeferredCleanups, 0));
  309. return false;
  310. }
  311. ClearLine();
  312. *status = mHandler->RunInteractive(command, GetStorageDirectory());
  313. return true;
  314. }
  315. bool InteractiveCommand::NeedsOperationalAdvertising()
  316. {
  317. return mAdvertiseOperational.ValueOr(true);
  318. }