Branch data Line data Source code
1 : : // Copyright 2026 HPActor Contributors
2 : : // Licensed under the Apache License, Version 2.0
3 : :
4 : : #include <hpactor/cli/cli_actor.hpp>
5 : : #include <hpactor/cli/command_context.hpp>
6 : : #include <hpactor/cli/lexer.hpp>
7 : : #include <hpactor/cli/line_editor.hpp>
8 : : #include <hpactor/cli_messages.pb.h>
9 : : #include <hpactor/core/actor_system.hpp>
10 : :
11 : : #include <charconv>
12 : : #include <chrono>
13 : : #include <cstdio>
14 : : #include <cstdlib>
15 : : #include <iostream>
16 : : #include <thread>
17 : :
18 : : namespace hpactor {
19 : : namespace cli {
20 : :
21 : 2 : std::string CliActor::get_history_path(const CliConfig& config) {
22 : 2 : if (!config.history_path.empty())
23 : 1 : return config.history_path;
24 : 1 : const char* home = getenv("HOME");
25 : 1 : if (!home)
26 : 0 : home = "/tmp";
27 : 2 : return std::string(home) + "/.hpactor_history";
28 : : }
29 : :
30 : 0 : CliActor::CliActor(ActorContext* ctx, ActorSystem& system, const CliConfig& config)
31 : 0 : : DaemonActor(ctx, system), system_(system), config_(config),
32 : 0 : line_editor_(LineEditorConfig{get_history_path(config), config.history_max,
33 : : /*multiline=*/false},
34 : 0 : /*root=*/nullptr) {
35 : 0 : formatter_ = OutputFormatter::create(config.default_format);
36 : 0 : pager_ = std::make_unique<Pager>(config.page_size);
37 : 0 : build_command_tree();
38 : 0 : line_editor_.set_root(command_tree_.get());
39 : 0 : line_editor_.load_history();
40 : 0 : }
41 : :
42 : 0 : void CliActor::on_daemon_start() {
43 : 0 : print_greeting();
44 : 0 : }
45 : :
46 : 0 : void CliActor::on_daemon_stop() {
47 : 0 : line_editor_.save_history();
48 : 0 : printf("\n[CLI session ended]\n");
49 : 0 : }
50 : :
51 : 0 : void CliActor::print_greeting() {
52 : 0 : printf("HPActor CLI v1.0 — Type /help for available commands. /quit to "
53 : : "exit.\n\n");
54 : 0 : }
55 : :
56 : : // ---------------------------------------------------------------------------
57 : : // Mailbox polling — block on this dedicated thread until the expected
58 : : // response tag arrives or the timeout expires.
59 : : // ---------------------------------------------------------------------------
60 : :
61 : : std::optional<StreamBuffer>
62 : 0 : CliActor::poll_for_response(TypeTag expected_tag, std::chrono::milliseconds timeout) {
63 : 0 : auto deadline = std::chrono::steady_clock::now() + timeout;
64 : :
65 : 0 : while (std::chrono::steady_clock::now() < deadline) {
66 : 0 : TypedMessage msg;
67 : 0 : if (mailbox()->try_pop(msg)) {
68 : 0 : if (msg.type_id() == expected_tag) {
69 : 0 : return std::move(msg).payload();
70 : : }
71 : : // Discard unexpected messages. CliActor is a system actor —
72 : : // no other actor links to or monitors it, so the only expected
73 : : // traffic is replies to its own requests.
74 : : }
75 : 0 : std::this_thread::sleep_for(std::chrono::milliseconds(1));
76 : 0 : }
77 : 0 : return std::nullopt;
78 : : }
79 : :
80 : : // ---------------------------------------------------------------------------
81 : : // send_and_wait helpers
82 : : // ---------------------------------------------------------------------------
83 : :
84 : : std::optional<InspectStateReply>
85 : 0 : CliActor::send_and_wait_inspect(ActorId target, const InspectStateRequest& req,
86 : : std::chrono::milliseconds timeout) {
87 : 0 : auto actor = system_.get_actor(target);
88 : 0 : if (!actor)
89 : 0 : return std::nullopt;
90 : :
91 : 0 : TypedMessage msg(TypeTag::InspectStateRequestTag, req);
92 : 0 : context()->send(actor->address(), std::move(msg));
93 : :
94 : 0 : auto payload = poll_for_response(TypeTag::InspectStateResponseTag, timeout);
95 : 0 : if (!payload)
96 : 0 : return std::nullopt;
97 : :
98 : 0 : InspectStateReply reply;
99 : 0 : if (!reply.ParseFromArray(payload->data(), static_cast<int>(payload->size()))) {
100 : 0 : return std::nullopt;
101 : : }
102 : 0 : return reply;
103 : 0 : }
104 : :
105 : : std::optional<KillReply>
106 : 0 : CliActor::send_and_wait_kill(ActorId target, const KillRequest& req,
107 : : std::chrono::milliseconds timeout) {
108 : 0 : auto actor = system_.get_actor(target);
109 : 0 : if (!actor)
110 : 0 : return std::nullopt;
111 : :
112 : 0 : TypedMessage msg(TypeTag::KillRequestTag, req);
113 : 0 : context()->send(actor->address(), std::move(msg));
114 : :
115 : 0 : auto payload = poll_for_response(TypeTag::KillResponseTag, timeout);
116 : 0 : if (!payload)
117 : 0 : return std::nullopt;
118 : :
119 : 0 : KillReply reply;
120 : 0 : if (!reply.ParseFromArray(payload->data(), static_cast<int>(payload->size()))) {
121 : 0 : return std::nullopt;
122 : : }
123 : 0 : return reply;
124 : 0 : }
125 : :
126 : : // ---------------------------------------------------------------------------
127 : : // Actor enumeration — iterates the system actor map under lock.
128 : : // ---------------------------------------------------------------------------
129 : :
130 : 0 : std::vector<ActorMeta> CliActor::enumerate_actors(const std::string& filter) {
131 : 0 : std::vector<ActorMeta> result;
132 : 0 : system_.for_each_actor([&](ActorId /*id*/, AbstractActor& actor) {
133 : 0 : if (!filter.empty()) {
134 : 0 : std::string type_name(actor.type_name().data(),
135 : 0 : actor.type_name().size());
136 : 0 : if (type_name.find(filter) == std::string::npos)
137 : 0 : return;
138 : 0 : }
139 : 0 : auto meta = actor.to_metadata();
140 : 0 : result.push_back(std::move(meta));
141 : 0 : });
142 : 0 : return result;
143 : : }
144 : :
145 : : // ---------------------------------------------------------------------------
146 : : // Command tree — registered commands wired to real implementations.
147 : : // ---------------------------------------------------------------------------
148 : :
149 : 0 : static ActorId parse_actor_id(const std::string& s) {
150 : 0 : uint64_t raw = 0;
151 : 0 : int base = 10;
152 : 0 : const char* start = s.data();
153 : 0 : if (s.size() >= 2 && s[0] == '0' && (s[1] == 'x' || s[1] == 'X')) {
154 : 0 : base = 16;
155 : 0 : start = s.data() + 2;
156 : : }
157 : 0 : auto [ptr, ec] = std::from_chars(start, s.data() + s.size(), raw, base);
158 : 0 : if (ec != std::errc{})
159 : 0 : return ActorId{0};
160 : 0 : return ActorId{raw};
161 : : }
162 : :
163 : 0 : void CliActor::build_command_tree() {
164 : 0 : auto root = std::make_unique<CommandNode>("/", "CLI root");
165 : :
166 : : // ── /actor <id> show ──────────────────────────────────────────────
167 : 0 : auto* actor_cmd = root->add_child("actor", "Actor operations");
168 : : auto* actor_id_param =
169 : 0 : actor_cmd->add_child("<id>", "Target actor ID", /*is_param=*/true);
170 : :
171 : : actor_id_param
172 : 0 : ->add_child("show", "Display actor metadata, state, mailbox, and "
173 : : "children")
174 : 0 : ->execute = [this](CommandContext& ctx) -> result<void> {
175 : 0 : auto id_str = ctx.get_param("<id>");
176 : 0 : if (!id_str) {
177 : 0 : ctx.output->error("Missing actor ID (usage: /actor <id> show)");
178 : 0 : return result<void>::make();
179 : : }
180 : 0 : ActorId target_id = parse_actor_id(*id_str);
181 : 0 : if (target_id == ActorId{0}) {
182 : 0 : ctx.output->error("Invalid actor ID: " + *id_str);
183 : 0 : return result<void>::make();
184 : : }
185 : :
186 : 0 : InspectStateRequest req;
187 : 0 : req.set_target_actor_id(target_id.value());
188 : 0 : req.set_include_state(true);
189 : 0 : req.set_include_mailbox(true);
190 : 0 : req.set_include_children(true);
191 : :
192 : 0 : auto reply = send_and_wait_inspect(target_id, req);
193 : 0 : if (!reply) {
194 : 0 : ctx.output->error("No response from actor " + *id_str +
195 : : " (timeout or not found)");
196 : 0 : return result<void>::make();
197 : : }
198 : :
199 : 0 : ctx.output->header("Actor " + *id_str + " — " +
200 : 0 : reply->metadata().actor_type());
201 : :
202 : 0 : std::map<std::string, std::string> kv;
203 : 0 : kv["State"] = reply->metadata().state();
204 : 0 : kv["Incarnation"] = std::to_string(reply->metadata().incarnation());
205 : 0 : kv["Processed"] =
206 : 0 : std::to_string(reply->metadata().messages_processed()) + " msgs";
207 : 0 : kv["Uptime (ms)"] = std::to_string(reply->metadata().uptime_ms());
208 : 0 : kv["Behavior"] = reply->metadata().behavior_name();
209 : :
210 : 0 : if (reply->has_mailbox()) {
211 : 0 : kv["Mailbox depth"] = std::to_string(reply->mailbox().depth());
212 : 0 : kv["Mailbox max"] = std::to_string(reply->mailbox().max_depth());
213 : : }
214 : :
215 : 0 : ctx.output->key_value(kv);
216 : :
217 : 0 : if (!reply->state_blob().empty()) {
218 : 0 : ctx.output->raw("State: " + reply->state_blob());
219 : : }
220 : :
221 : 0 : return result<void>::make();
222 : 0 : };
223 : :
224 : : // ── /actor <id> kill ──────────────────────────────────────────────
225 : 0 : actor_id_param->add_child("kill", "Terminate actor (graceful shutdown)")->execute =
226 : 0 : [this](CommandContext& ctx) -> result<void> {
227 : 0 : auto id_str = ctx.get_param("<id>");
228 : 0 : if (!id_str) {
229 : 0 : ctx.output->error("Missing actor ID (usage: /actor <id> kill)");
230 : 0 : return result<void>::make();
231 : : }
232 : 0 : ActorId target_id = parse_actor_id(*id_str);
233 : 0 : if (target_id == ActorId{0}) {
234 : 0 : ctx.output->error("Invalid actor ID: " + *id_str);
235 : 0 : return result<void>::make();
236 : : }
237 : :
238 : 0 : KillRequest req;
239 : 0 : req.set_target_actor_id(target_id.value());
240 : 0 : req.set_force(false);
241 : :
242 : 0 : auto reply = send_and_wait_kill(target_id, req);
243 : 0 : if (!reply) {
244 : 0 : ctx.output->error("No response from actor " + *id_str +
245 : : " (timeout or not found)");
246 : 0 : return result<void>::make();
247 : : }
248 : :
249 : 0 : if (reply->success()) {
250 : 0 : ctx.output->raw("Actor " + *id_str + " terminated.");
251 : : } else {
252 : 0 : ctx.output->error("Failed to kill actor " + *id_str + ": " +
253 : 0 : reply->error_message());
254 : : }
255 : 0 : return result<void>::make();
256 : 0 : };
257 : :
258 : : // ── /actor list ───────────────────────────────────────────────────
259 : 0 : actor_cmd->add_child("list", "List all actors [--filter <type>]")->execute =
260 : 0 : [this](CommandContext& ctx) -> result<void> {
261 : 0 : std::string filter;
262 : 0 : if (auto f = ctx.get_param("filter"))
263 : 0 : filter = *f;
264 : :
265 : 0 : auto actors = enumerate_actors(filter);
266 : :
267 : 0 : ctx.output->header("Actors (" + std::to_string(actors.size()) + " total)");
268 : :
269 : 0 : std::vector<std::string> cols = {"ID", "Type", "State", "Processed"};
270 : 0 : std::vector<std::vector<std::string>> rows;
271 : 0 : rows.reserve(actors.size());
272 : :
273 : 0 : for (auto& a : actors) {
274 : : char id_buf[32];
275 : 0 : snprintf(id_buf, sizeof(id_buf), "0x%04llX",
276 : 0 : static_cast<unsigned long long>(a.actor_id));
277 : 0 : rows.push_back({id_buf, a.actor_type, a.state,
278 : : std::to_string(a.messages_processed)});
279 : : }
280 : :
281 : 0 : ctx.output->table(cols, rows);
282 : 0 : return result<void>::make();
283 : 0 : };
284 : :
285 : : // ── /system stats ─────────────────────────────────────────────────
286 : 0 : auto* sys = root->add_child("system", "System operations");
287 : :
288 : 0 : sys->add_child("stats", "System-wide statistics")->execute =
289 : 0 : [this](CommandContext& ctx) -> result<void> {
290 : 0 : ctx.output->header("System Statistics");
291 : :
292 : 0 : std::map<std::string, std::string> kv;
293 : 0 : kv["Total actors"] = std::to_string(system_.actor_count());
294 : 0 : if (auto* sched = system_.scheduler()) {
295 : 0 : kv["Scheduler threads"] = std::to_string(sched->worker_count());
296 : : }
297 : 0 : kv["CLI enabled"] = config_.enabled ? "yes" : "no";
298 : 0 : kv["CLI format"] = config_.default_format;
299 : :
300 : 0 : ctx.output->key_value(kv);
301 : 0 : return result<void>::make();
302 : 0 : };
303 : :
304 : 0 : sys->add_child("memory", "Memory subsystem stats")->execute =
305 : 0 : [](CommandContext& ctx) -> result<void> {
306 : 0 : ctx.output->header("System Memory");
307 : 0 : ctx.output->key_value({{"Status", "Memory subsystem active"},
308 : : {"Note", "Use /metrics show for detailed memory "
309 : : "stats"}});
310 : 0 : return result<void>::make();
311 : 0 : };
312 : :
313 : 0 : sys->add_child("list", "List system actors")->execute =
314 : 0 : [this](CommandContext& ctx) -> result<void> {
315 : 0 : ctx.output->header("System Actors");
316 : :
317 : 0 : std::vector<std::string> cols = {"ID", "Type", "State"};
318 : 0 : std::vector<std::vector<std::string>> rows;
319 : :
320 : 0 : system_.for_each_actor([&](ActorId actor_id, AbstractActor& actor) {
321 : : char id_buf[32];
322 : 0 : snprintf(id_buf, sizeof(id_buf), "0x%04llX",
323 : 0 : static_cast<unsigned long long>(actor_id.value()));
324 : 0 : auto meta = actor.to_metadata();
325 : 0 : rows.push_back({id_buf, meta.actor_type, meta.state});
326 : 0 : });
327 : :
328 : 0 : ctx.output->table(cols, rows);
329 : 0 : return result<void>::make();
330 : 0 : };
331 : :
332 : : // ── /system drain ──────────────────────────────────────────────────
333 : 0 : auto* drain = sys->add_child("drain", "Graceful node shutdown");
334 : 0 : drain->execute = [this](CommandContext& ctx) -> result<void> {
335 : 0 : auto shutdown_result = system().shutdown();
336 : 0 : if (shutdown_result.has_value()) {
337 : 0 : ctx.output->raw("Shutdown complete");
338 : : } else {
339 : 0 : ctx.output->error("Shutdown failed");
340 : : }
341 : 0 : return result<void>::make();
342 : 0 : };
343 : :
344 : : // ── /system drain status ───────────────────────────────────────────
345 : 0 : drain->add_child("status", "Show shutdown progress")->execute =
346 : 0 : [this](CommandContext& ctx) -> result<void> {
347 : 0 : ctx.output->raw("Shutdown phase: " + std::to_string(static_cast<int>(
348 : 0 : system().shutdown_phase())));
349 : 0 : ctx.output->raw("Actors live: " + std::to_string(system().actor_count()));
350 : 0 : return result<void>::make();
351 : 0 : };
352 : :
353 : : // ── /system stop <actor_id> ────────────────────────────────────────
354 : 0 : auto* stop = sys->add_child("stop", "Graceful stop of an actor");
355 : 0 : auto* stop_id_param = stop->add_child("<actor_id>", "Actor ID to stop",
356 : : /*is_param=*/true);
357 : 0 : stop_id_param->execute = [this](CommandContext& ctx) -> result<void> {
358 : 0 : auto id_str = ctx.get_param("<actor_id>");
359 : 0 : if (!id_str) {
360 : 0 : ctx.output->error("Missing actor ID (usage: /system stop "
361 : : "<actor_id>)");
362 : 0 : return result<void>::make();
363 : : }
364 : 0 : ActorId target_id = parse_actor_id(*id_str);
365 : 0 : if (target_id == ActorId{0}) {
366 : 0 : ctx.output->error("Invalid actor ID: " + *id_str);
367 : 0 : return result<void>::make();
368 : : }
369 : :
370 : 0 : if (ctx.has_flag("force")) {
371 : 0 : system().set_drain_config(target_id,
372 : : DrainConfig{DrainPolicy::ImmediateStop});
373 : : }
374 : :
375 : 0 : auto actor = system().get_actor(target_id);
376 : 0 : if (!actor) {
377 : 0 : ctx.output->error("Actor not found: " + std::string(*id_str));
378 : 0 : return result<void>::make();
379 : : }
380 : 0 : context()->stop(target_id);
381 : 0 : ctx.output->raw("Drain initiated for actor " + std::string(*id_str));
382 : 0 : return result<void>::make();
383 : 0 : };
384 : :
385 : : // ── /metrics show ─────────────────────────────────────────────────
386 : 0 : auto* metrics = root->add_child("metrics", "Metrics operations");
387 : 0 : metrics->add_child("show", "Show current metrics snapshot")->execute =
388 : 0 : [](CommandContext& ctx) -> result<void> {
389 : 0 : ctx.output->header("Metrics");
390 : 0 : ctx.output->raw("metrics show — not yet implemented");
391 : 0 : return result<void>::make();
392 : 0 : };
393 : :
394 : : // ── /topology show ────────────────────────────────────────────────
395 : 0 : auto* topo = root->add_child("topology", "Topology operations");
396 : 0 : topo->add_child("show", "Show topology tree")->execute =
397 : 0 : [](CommandContext& ctx) -> result<void> {
398 : 0 : ctx.output->header("Topology");
399 : 0 : ctx.output->raw("topology show — not yet implemented");
400 : 0 : return result<void>::make();
401 : 0 : };
402 : :
403 : : // ── /help ─────────────────────────────────────────────────────────
404 : 0 : root->add_child("help", "Show available commands")->execute =
405 : 0 : [this](CommandContext& ctx) -> result<void> {
406 : 0 : ctx.output->header("Available Commands");
407 : 0 : ctx.output->raw(command_tree_->help());
408 : 0 : return result<void>::make();
409 : 0 : };
410 : :
411 : : // ── /quit ─────────────────────────────────────────────────────────
412 : 0 : root->add_child("quit", "Exit the CLI")->execute =
413 : 0 : [this](CommandContext& ctx) -> result<void> {
414 : 0 : ctx.output->raw("Goodbye.");
415 : 0 : running_ = false;
416 : 0 : return result<void>::make();
417 : 0 : };
418 : :
419 : 0 : command_tree_ = std::move(root);
420 : 0 : }
421 : :
422 : : // ---------------------------------------------------------------------------
423 : : // Command execution
424 : : // ---------------------------------------------------------------------------
425 : :
426 : 0 : void CliActor::execute_tokens(const std::vector<Token>& tokens) {
427 : : // Reopen formatter for each command
428 : 0 : formatter_ = OutputFormatter::create(config_.default_format);
429 : :
430 : 0 : CommandContext ctx;
431 : 0 : ctx.system = &system_;
432 : 0 : ctx.cli_actor = this;
433 : 0 : ctx.output = formatter_.get();
434 : 0 : ctx.page_size = config_.page_size;
435 : :
436 : : // Walk the command tree
437 : 0 : CommandNode* node = command_tree_.get();
438 : :
439 : 0 : size_t i = 0;
440 : : // Skip leading "/" keyword
441 : 0 : if (i < tokens.size() && tokens[i].value == "/") {
442 : 0 : ++i;
443 : : }
444 : :
445 : 0 : for (; i < tokens.size(); ++i) {
446 : 0 : auto& tok = tokens[i];
447 : :
448 : 0 : if (tok.type == TokenType::Eof) {
449 : 0 : break;
450 : : }
451 : :
452 : 0 : if (tok.type == TokenType::Flag) {
453 : 0 : ctx.params[tok.value] = "true";
454 : 0 : continue;
455 : : }
456 : :
457 : 0 : if (tok.type == TokenType::FlagWithArg) {
458 : 0 : ctx.params[tok.value] = tok.arg.value_or("true");
459 : 0 : if (tok.value == "format") {
460 : 0 : ctx.format = tok.arg.value_or("pretty");
461 : 0 : formatter_ = OutputFormatter::create(ctx.format);
462 : 0 : ctx.output = formatter_.get();
463 : : }
464 : 0 : continue;
465 : : }
466 : :
467 : : // Try to match as keyword or parameter
468 : 0 : std::string param_value;
469 : 0 : auto* child = node->find_child(tok.value, param_value);
470 : 0 : if (!child) {
471 : 0 : auto suggestion = node->suggest(tok.value);
472 : 0 : std::string err = "Unknown command '" + tok.value + "'";
473 : 0 : if (!suggestion.empty()) {
474 : 0 : err += " — did you mean '" + suggestion + "'?";
475 : : }
476 : 0 : formatter_->error(err);
477 : 0 : printf("%s\n", formatter_->finalize().c_str());
478 : 0 : return;
479 : 0 : }
480 : :
481 : 0 : if (child->is_parameter) {
482 : 0 : ctx.params[child->keyword] = param_value;
483 : : }
484 : 0 : node = child;
485 : 0 : }
486 : :
487 : : // Execute leaf node
488 : 0 : if (node->execute) {
489 : 0 : node->execute(ctx);
490 : : } else {
491 : : // No execute — show help for this node
492 : 0 : if (!node->children.empty()) {
493 : 0 : formatter_->header("Available commands");
494 : 0 : formatter_->raw(node->help());
495 : : }
496 : : }
497 : :
498 : 0 : printf("%s\n", formatter_->finalize().c_str());
499 : 0 : }
500 : :
501 : 0 : bool CliActor::run_once() {
502 : 0 : if (!running_) {
503 : 0 : return false;
504 : : }
505 : :
506 : 0 : std::string line = line_editor_.readline("hpactor> ");
507 : 0 : if (line.empty()) {
508 : 0 : printf("\nGoodbye.\n");
509 : 0 : running_ = false;
510 : 0 : return false;
511 : : }
512 : :
513 : 0 : auto tokens = Lexer::tokenize(line);
514 : 0 : execute_tokens(tokens);
515 : 0 : line_editor_.add_history(line);
516 : 0 : return true;
517 : 0 : }
518 : :
519 : : } // namespace cli
520 : : } // namespace hpactor
|