Skip to content

Commit a76df6e

Browse files
committed
Support cvd start --daemon=false
Implements foreground mode for `cvd start`. When `--daemon=false` is requested, it continues to launch the device via `cvd_internal_start --daemon=true` but subsequently attaches the log monitor view (`cvd monitor`). Upon user interruption (Ctrl-C), it gracefully shuts down the instance group. Assisted-by: Jetski:Gemini-Next Bug: b/519304405 TAG=agy CONV=34ddc7b8-a695-4937-be99-b796087dd987
1 parent 775e6d6 commit a76df6e

5 files changed

Lines changed: 179 additions & 67 deletions

File tree

base/cvd/cuttlefish/host/commands/cvd/cli/commands/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,7 @@ cf_cc_library(
405405
"//cuttlefish/host/commands/cvd/cli:utils",
406406
"//cuttlefish/host/commands/cvd/cli/commands:command_handler",
407407
"//cuttlefish/host/commands/cvd/cli/commands:host_tool_target",
408+
"//cuttlefish/host/commands/cvd/cli/commands/monitor",
408409
"//cuttlefish/host/commands/cvd/cli/selector",
409410
"//cuttlefish/host/commands/cvd/fetch:substitute",
410411
"//cuttlefish/host/commands/cvd/instances",

base/cvd/cuttlefish/host/commands/cvd/cli/commands/monitor/monitor.cc

Lines changed: 33 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -18,8 +18,9 @@
1818

1919
#include <errno.h>
2020
#include <fcntl.h>
21-
#include <poll.h>
21+
#include <sys/eventfd.h>
2222
#include <sys/inotify.h>
23+
#include <sys/poll.h>
2324
#include <sys/types.h>
2425
#include <unistd.h>
2526

@@ -28,6 +29,7 @@
2829
#include <memory>
2930
#include <string>
3031
#include <thread>
32+
#include <vector>
3133

3234
#include "absl/strings/str_cat.h"
3335

@@ -40,16 +42,8 @@
4042
#include "cuttlefish/result/result.h"
4143

4244
namespace cuttlefish {
43-
4445
namespace {
4546

46-
void ClearLastNLines(int n) {
47-
if (n > 0) {
48-
// Move cursor up N lines and clear to end of screen
49-
std::cout << AnsiCursorUp(n) << kAnsiClearScreenAfterCursor << std::flush;
50-
}
51-
}
52-
5347
void UpdateFileAndWatch(const SharedFD& inotify_fd, const std::string& path,
5448
SharedFD& fd, int& watch) {
5549
if (!fd->IsOpen()) {
@@ -62,7 +56,17 @@ void UpdateFileAndWatch(const SharedFD& inotify_fd, const std::string& path,
6256

6357
} // namespace
6458

59+
void ClearLastNLines(int n) {
60+
if (n > 0) {
61+
// Move cursor up N lines and clear to end of screen
62+
std::cout << AnsiCursorUp(n) << kAnsiClearScreenAfterCursor << std::flush;
63+
}
64+
}
6565
Result<void> MonitorLogs(const LocalInstance& instance) {
66+
return MonitorLogs(instance, SharedFD());
67+
}
68+
69+
Result<void> MonitorLogs(const LocalInstance& instance, SharedFD stop_eventfd) {
6670
CF_EXPECT(isatty(0), "The monitor command requires an interactive terminal.");
6771

6872
const std::string kernel_log =
@@ -122,19 +126,34 @@ Result<void> MonitorLogs(const LocalInstance& instance) {
122126
}
123127
last_draw_time = std::chrono::steady_clock::now();
124128

129+
// Setup poll_fds
130+
std::vector<PollSharedFd> poll_fds = {
131+
PollSharedFd{.fd = inotify_fd, .events = POLLIN, .revents = 0},
132+
};
133+
if (stop_eventfd->IsOpen()) {
134+
poll_fds.push_back(
135+
PollSharedFd{.fd = stop_eventfd, .events = POLLIN, .revents = 0});
136+
}
137+
125138
// Block until file changes occur. If any watch failed or is missing, use
126139
// a 1-second fallback timeout to awake and retry.
127-
// NOLINTNEXTLINE(misc-include-cleaner)
128-
PollSharedFd poll_fd = {inotify_fd, POLLIN, 0};
129140
int timeout_ms = -1;
130141
if (dir_watch == -1 || kernel_watch == -1 || launcher_watch == -1 ||
131142
logcat_watch == -1) {
132143
timeout_ms = 1000;
133144
}
134145

135-
// NOLINTNEXTLINE(misc-include-cleaner)
136-
if (SharedFD::Poll(&poll_fd, 1, timeout_ms) > 0 &&
137-
(poll_fd.revents & POLLIN)) {
146+
int poll_res = SharedFD::Poll(poll_fds, timeout_ms);
147+
148+
if (stop_eventfd->IsOpen() && (poll_fds[1].revents & POLLIN)) {
149+
// Stop requested via eventfd
150+
eventfd_t val;
151+
stop_eventfd->EventfdRead(&val);
152+
ClearLastNLines(total_lines_drawn);
153+
return {};
154+
}
155+
156+
if (poll_res > 0 && (poll_fds[0].revents & POLLIN)) {
138157
// Exhaustively drain all available events from the non-blocking
139158
// descriptor to coalesce rapid file modifications.
140159
char buf[4096]

base/cvd/cuttlefish/host/commands/cvd/cli/commands/monitor/monitor.h

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,18 @@
1616

1717
#pragma once
1818

19+
#include "cuttlefish/common/libs/fs/shared_fd.h"
1920
#include "cuttlefish/host/commands/cvd/instances/local_instance.h"
2021
#include "cuttlefish/result/result.h"
2122

2223
namespace cuttlefish {
2324

25+
void ClearLastNLines(int n);
26+
2427
Result<void> MonitorLogs(const LocalInstance& instance);
2528

29+
// The monitor will stop and return if `stop_eventfd` becomes readable (receives
30+
// an event).
31+
Result<void> MonitorLogs(const LocalInstance& instance, SharedFD stop_eventfd);
32+
2633
} // namespace cuttlefish

base/cvd/cuttlefish/host/commands/cvd/cli/commands/start.cpp

Lines changed: 134 additions & 52 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,8 @@
2424
#include <unistd.h>
2525

2626
#include <algorithm>
27+
#include <chrono>
28+
#include <cstdio>
2729
#include <cstdlib>
2830
#include <cstring>
2931
#include <iostream>
@@ -32,26 +34,29 @@
3234
#include <optional>
3335
#include <set>
3436
#include <string>
37+
#include <thread>
3538
#include <utility>
3639
#include <vector>
3740

38-
#include <fmt/core.h>
39-
#include <fmt/format.h>
4041
#include "absl/log/log.h"
4142
#include "absl/strings/str_join.h"
4243
#include "absl/strings/str_split.h"
44+
#include "fmt/core.h"
45+
#include "fmt/format.h"
4346

47+
#include "cuttlefish/common/libs/fs/shared_buf.h"
4448
#include "cuttlefish/common/libs/fs/shared_fd.h"
4549
#include "cuttlefish/common/libs/utils/contains.h"
4650
#include "cuttlefish/common/libs/utils/files.h"
47-
#include "cuttlefish/flag_parser/flag.h"
48-
#include "cuttlefish/flag_parser/gflags_compat.h"
4951
#include "cuttlefish/common/libs/utils/json.h"
5052
#include "cuttlefish/common/libs/utils/subprocess.h"
5153
#include "cuttlefish/common/libs/utils/users.h"
54+
#include "cuttlefish/flag_parser/flag.h"
55+
#include "cuttlefish/flag_parser/gflags_compat.h"
5256
#include "cuttlefish/host/commands/cvd/cli/command_request.h"
5357
#include "cuttlefish/host/commands/cvd/cli/commands/command_handler.h"
5458
#include "cuttlefish/host/commands/cvd/cli/commands/host_tool_target.h"
59+
#include "cuttlefish/host/commands/cvd/cli/commands/monitor/monitor.h"
5560
#include "cuttlefish/host/commands/cvd/cli/help_format.h"
5661
#include "cuttlefish/host/commands/cvd/cli/selector/selector.h"
5762
#include "cuttlefish/host/commands/cvd/cli/types.h"
@@ -279,7 +284,8 @@ Result<Command> ConstructCvdNonHelpCommand(const std::string& bin_file,
279284
const LocalInstanceGroup& group,
280285
const cvd_common::Args& args,
281286
const cvd_common::Envs& envs,
282-
const CommandRequest& request) {
287+
const CommandRequest& request,
288+
SharedFD output_fd = SharedFD()) {
283289
auto bin_path = group.HostArtifactsPath();
284290
CF_EXPECTF(PotentiallyHostArtifactsPath(bin_path),
285291
"ANDROID_HOST_OUT, \"{}\" is not a tool directory", bin_path);
@@ -292,10 +298,17 @@ Result<Command> ConstructCvdNonHelpCommand(const std::string& bin_file,
292298
.working_dir = CurrentDirectory(),
293299
.command_name = bin_file};
294300
Command non_help_command = CF_EXPECT(ConstructCommand(construct_cmd_param));
295-
// Print everything to stderr, cvd needs to print JSON to stdout which
296-
// would be unparseable with the subcommand's output.
297-
non_help_command.RedirectStdIO(Subprocess::StdIOChannel::kStdOut,
298-
Subprocess::StdIOChannel::kStdErr);
301+
if (output_fd->IsOpen()) {
302+
non_help_command.RedirectStdIO(Subprocess::StdIOChannel::kStdOut,
303+
output_fd);
304+
non_help_command.RedirectStdIO(Subprocess::StdIOChannel::kStdErr,
305+
output_fd);
306+
} else {
307+
// Print everything to stderr, cvd needs to print JSON to stdout which
308+
// would be unparseable with the subcommand's output.
309+
non_help_command.RedirectStdIO(Subprocess::StdIOChannel::kStdOut,
310+
Subprocess::StdIOChannel::kStdErr);
311+
}
299312
return non_help_command;
300313
}
301314

@@ -317,18 +330,6 @@ CvdStartCommandHandler::CvdStartCommandHandler(
317330
InstanceManager& instance_manager)
318331
: instance_manager_(instance_manager) {}
319332

320-
static Result<void> ConsumeDaemonModeFlag(cvd_common::Args& args) {
321-
bool daemon = true;
322-
Flag flag =
323-
GflagsCompatFlag("daemon", daemon)
324-
.AddValidator([&daemon]() -> Result<void> {
325-
CF_EXPECT(!!daemon, "`cvd start` must always run in daemon mode.");
326-
return {};
327-
});
328-
CF_EXPECT(ConsumeFlags({flag}, args));
329-
return {};
330-
}
331-
332333
static bool HasUnsafeFlagsForBypass(const std::vector<std::string>& args) {
333334
std::vector<std::string> args_copy = args;
334335
bool daemon = true;
@@ -370,8 +371,18 @@ Result<void> CvdStartCommandHandler::Handle(const CommandRequest& request) {
370371
}
371372
}
372373

373-
CF_EXPECT(ConsumeDaemonModeFlag(subcmd_args));
374+
CF_EXPECT(ConsumeFlags(BuildOwnFlags(), subcmd_args));
375+
bool is_daemon = own_flags_.daemon.value_or(true);
374376
subcmd_args.push_back("--daemon=true");
377+
if (own_flags_.report_anonymous_usage_stats) {
378+
subcmd_args.push_back("--report_anonymous_usage_stats=" +
379+
*own_flags_.report_anonymous_usage_stats);
380+
} else if (!is_daemon) {
381+
// If the user did not specify the anonymous usage stats flag, default it
382+
// to 'n' to ensure that cvd_internal_start does not block waiting for
383+
// input on stdin.
384+
subcmd_args.push_back("--report_anonymous_usage_stats=n");
385+
}
375386

376387
LocalInstanceGroup group =
377388
CF_EXPECT(selector::SelectGroup(instance_manager_, request),
@@ -388,46 +399,109 @@ Result<void> CvdStartCommandHandler::Handle(const CommandRequest& request) {
388399
CF_EXPECT(UpdateEnvs(envs, group));
389400
const auto bin = CF_EXPECT(FindStartBin(group.HostArtifactsPath()));
390401

391-
CF_EXPECT(ConsumeFlags(BuildOwnFlags(), subcmd_args));
392-
393402
CF_EXPECT(HostPackageSubstitution(group.HostArtifactsPath(),
394403
own_flags_.host_substitutions));
395404

396-
Command command = CF_EXPECT(
397-
ConstructCvdNonHelpCommand(bin, group, subcmd_args, envs, request));
398-
399-
// The instance database needs to be updated if an interrupt is received.
400-
auto handle_res = PushInterruptListener([this, &group](int signal) {
401-
LOG(WARNING) << strsignal(signal) << " signal received, cleanning up";
402-
auto interrupt_res = subprocess_waiter_.Interrupt();
403-
if (!interrupt_res.ok()) {
404-
LOG(ERROR) << "Failed to stop subprocesses: " << interrupt_res.error();
405-
LOG(ERROR) << "Devices may still be executing in the background, run "
406-
"`cvd reset` to ensure a clean state";
407-
}
408-
409-
group.SetAllStates(cvd::INSTANCE_STATE_CANCELLED);
410-
auto update_res = instance_manager_.UpdateInstanceGroup(group);
411-
if (!update_res.ok()) {
412-
LOG(ERROR) << "Failed to update group status: " << update_res.error();
413-
}
414-
// It's technically possible for the group's state to be set to
415-
// "running" before abort has a chance to run, but that can only happen
416-
// if the instances are indeed running, so it's OK.
405+
SharedFD memfd;
406+
SharedFD stop_eventfd;
407+
if (!is_daemon) {
408+
memfd = SharedFD::MemfdCreate("cvd_internal_start_output");
409+
CF_EXPECT(memfd->IsOpen(), "Failed to create memfd for subprocess output: "
410+
<< memfd->StrError());
411+
412+
stop_eventfd = SharedFD::Event();
413+
CF_EXPECT(stop_eventfd->IsOpen(),
414+
"Failed to create eventfd for stopping monitor: "
415+
<< stop_eventfd->StrError());
416+
}
417417

418-
std::abort();
419-
});
420-
auto listener_handle = CF_EXPECT(std::move(handle_res));
418+
Command command = CF_EXPECT(ConstructCvdNonHelpCommand(
419+
bin, group, subcmd_args, envs, request, memfd));
420+
421+
Result<std::unique_ptr<InterruptListenerHandle>> handle_res =
422+
PushInterruptListener([this, &group, stop_eventfd](int signal) {
423+
LOG(WARNING) << strsignal(signal) << " signal received, cleanning up";
424+
if (stop_eventfd->IsOpen()) {
425+
stop_eventfd->EventfdWrite(1);
426+
}
427+
Result<void> interrupt_res = subprocess_waiter_.Interrupt();
428+
if (!interrupt_res.ok()) {
429+
LOG(ERROR) << "Failed to stop subprocesses: "
430+
<< interrupt_res.error();
431+
LOG(ERROR) << "Devices may still be executing in the background, "
432+
"run `cvd reset` to ensure a clean state";
433+
}
434+
435+
group.SetAllStates(cvd::INSTANCE_STATE_CANCELLED);
436+
Result<void> update_res = instance_manager_.UpdateInstanceGroup(group);
437+
if (!update_res.ok()) {
438+
LOG(ERROR) << "Failed to update group status: " << update_res.error();
439+
}
440+
// It's technically possible for the group's state to be set to
441+
// "running" before abort has a chance to run, but that can only happen
442+
// if the instances are indeed running, so it's OK.
443+
444+
std::abort();
445+
});
446+
447+
std::unique_ptr<InterruptListenerHandle> listener_handle =
448+
CF_EXPECT(std::move(handle_res));
421449
group.SetAllStates(cvd::INSTANCE_STATE_STARTING);
422450
group.SetStartTime(CvdServerClock::now());
423451
CF_EXPECT(instance_manager_.UpdateInstanceGroup(group));
424-
CF_EXPECT(
425-
LaunchDeviceInterruptible(std::move(command), group, envs, request));
452+
453+
std::thread monitor_thread;
454+
Result<void> monitor_res;
455+
456+
if (!is_daemon) {
457+
monitor_thread = std::thread([&group, stop_eventfd, &monitor_res]() {
458+
const LocalInstance& first_instance = *group.Instances().begin();
459+
monitor_res = MonitorLogs(first_instance, stop_eventfd);
460+
});
461+
}
462+
463+
Result<void> launch_res =
464+
LaunchDeviceInterruptible(std::move(command), group, envs, request);
465+
466+
if (!launch_res.ok()) {
467+
if (!is_daemon) {
468+
if (stop_eventfd->IsOpen()) {
469+
stop_eventfd->EventfdWrite(1);
470+
}
471+
if (monitor_thread.joinable()) {
472+
monitor_thread.join();
473+
}
474+
memfd->LSeek(0, SEEK_SET);
475+
std::string full_output;
476+
ReadAll(memfd, &full_output);
477+
std::cout << full_output << std::flush;
478+
}
479+
return launch_res;
480+
}
481+
426482
group.SetAllStates(cvd::INSTANCE_STATE_RUNNING);
427483
CF_EXPECT(instance_manager_.UpdateInstanceGroup(group));
484+
485+
if (!is_daemon) {
486+
listener_handle.reset();
487+
std::unique_ptr<InterruptListenerHandle> stop_listener =
488+
CF_EXPECT(PushInterruptListener(
489+
[stop_eventfd](int) { stop_eventfd->EventfdWrite(1); }));
490+
491+
if (monitor_thread.joinable()) {
492+
monitor_thread.join();
493+
}
494+
stop_listener.reset();
495+
496+
LOG(INFO) << "Stopping device...";
497+
CF_EXPECT(instance_manager_.StopInstanceGroup(
498+
group, std::chrono::seconds(5), InstanceDirActionOnStop::Keep, {}));
499+
LOG(INFO) << "Device stopped.";
500+
return monitor_res;
501+
}
502+
428503
listener_handle.reset();
429504

430-
// show user a more succinct output
431505
if (isatty(0)) {
432506
std::vector<std::string> instance_names;
433507
for (const auto& instance : group.Instances()) {
@@ -609,7 +683,15 @@ std::vector<Flag> CvdStartCommandHandler::BuildOwnFlags() {
609683
"cuttlefish-base package. The special value \"all\" causes "
610684
"it to replace everything it can, while with an empty it "
611685
"will replace what the android build specifies in the "
612-
"'debian_substitution_marker' file.")};
686+
"'debian_substitution_marker' file."),
687+
GflagsCompatFlag("daemon", own_flags_.daemon)
688+
.Help("Run the start command in the background as a daemon. "
689+
"If false, the command runs in the foreground and monitors "
690+
"logs."),
691+
GflagsCompatFlag("report_anonymous_usage_stats",
692+
own_flags_.report_anonymous_usage_stats)
693+
.Help("Report anonymous usage stats. In foreground mode, "
694+
"defaults to 'n' if not specified.")};
613695
}
614696

615697
std::unique_ptr<CvdCommandHandler> NewCvdStartCommandHandler(

0 commit comments

Comments
 (0)