Gracefully stop the vm

Bug: 381815559
Test: click 'close' in the notification
Change-Id: Ic000bb7e62b5e5ec39b3c89b9cd9a05aee17588b
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
index d167da3..9cf6093 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/DebianServiceImpl.java
@@ -30,6 +30,8 @@
 import com.android.virtualization.terminal.proto.ReportVmActivePortsRequest;
 import com.android.virtualization.terminal.proto.ReportVmActivePortsResponse;
 import com.android.virtualization.terminal.proto.ReportVmIpAddrResponse;
+import com.android.virtualization.terminal.proto.ShutdownQueueOpeningRequest;
+import com.android.virtualization.terminal.proto.ShutdownRequestItem;
 
 import io.grpc.stub.StreamObserver;
 
@@ -41,6 +43,7 @@
     private final PortsStateManager mPortsStateManager;
     private PortsStateManager.Listener mPortsStateListener;
     private final DebianServiceCallback mCallback;
+    private Runnable mShutdownRunnable;
 
     static {
         System.loadLibrary("forwarder_host_jni");
@@ -93,6 +96,28 @@
         responseObserver.onCompleted();
     }
 
+    public boolean shutdownDebian() {
+        if (mShutdownRunnable == null) {
+            Log.d(TAG, "mShutdownRunnable is not ready.");
+            return false;
+        }
+        mShutdownRunnable.run();
+        return true;
+    }
+
+    @Override
+    public void openShutdownRequestQueue(
+            ShutdownQueueOpeningRequest request,
+            StreamObserver<ShutdownRequestItem> responseObserver) {
+        Log.d(TAG, "openShutdownRequestQueue");
+        mShutdownRunnable =
+                () -> {
+                    responseObserver.onNext(ShutdownRequestItem.newBuilder().build());
+                    responseObserver.onCompleted();
+                    mShutdownRunnable = null;
+                };
+    }
+
     @Keep
     private static class ForwarderHostCallback {
         private StreamObserver<ForwardingRequestItem> mResponseObserver;
diff --git a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
index ee2a6b8..6d2c5bd 100644
--- a/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
+++ b/android/TerminalApp/java/com/android/virtualization/terminal/VmLauncherService.java
@@ -54,7 +54,6 @@
 import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.Objects;
-import java.util.Set;
 import java.util.concurrent.ExecutorService;
 import java.util.concurrent.Executors;
 
@@ -144,7 +143,10 @@
     @Override
     public int onStartCommand(Intent intent, int flags, int startId) {
         if (Objects.equals(intent.getAction(), ACTION_STOP_VM_LAUNCHER_SERVICE)) {
-            stopSelf();
+            // If there is no Debian service or it fails to shutdown, just stop the service.
+            if (mDebianService == null || !mDebianService.shutdownDebian()) {
+                stopSelf();
+            }
             return START_NOT_STICKY;
         }
         if (mVirtualMachine != null) {
@@ -293,7 +295,8 @@
 
     public static void stop(Context context) {
         Intent i = getMyIntent(context);
-        context.stopService(i);
+        i.setAction(VmLauncherService.ACTION_STOP_VM_LAUNCHER_SERVICE);
+        context.startService(i);
     }
 
     @Override
diff --git a/build/debian/build.sh b/build/debian/build.sh
index 9104adc..19894c2 100755
--- a/build/debian/build.sh
+++ b/build/debian/build.sh
@@ -192,6 +192,7 @@
 	build_rust_binary_and_copy forwarder_guest
 	build_rust_binary_and_copy forwarder_guest_launcher
 	build_rust_binary_and_copy ip_addr_reporter
+	build_rust_binary_and_copy shutdown_runner
 }
 
 run_fai() {
diff --git a/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF b/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF
new file mode 100644
index 0000000..bfb8afb
--- /dev/null
+++ b/build/debian/fai_config/files/etc/systemd/system/shutdown_runner.service/AVF
@@ -0,0 +1,11 @@
+[Unit]
+After=syslog.target
+After=network.target
+After=virtiofs_internal.service
+[Service]
+ExecStart=/usr/bin/bash -c '/usr/local/bin/shutdown_runner --grpc_port $(cat /mnt/internal/debian_service_port)'
+Type=simple
+User=root
+Group=root
+[Install]
+WantedBy=multi-user.target
diff --git a/build/debian/fai_config/scripts/AVF/10-systemd b/build/debian/fai_config/scripts/AVF/10-systemd
index 94838bc..119bec7 100755
--- a/build/debian/fai_config/scripts/AVF/10-systemd
+++ b/build/debian/fai_config/scripts/AVF/10-systemd
@@ -3,6 +3,7 @@
 chmod +x $target/usr/local/bin/forwarder_guest
 chmod +x $target/usr/local/bin/forwarder_guest_launcher
 chmod +x $target/usr/local/bin/ip_addr_reporter
+chmod +x $target/usr/local/bin/shutdown_runner
 chmod +x $target/usr/local/bin/ttyd
 ln -s /etc/systemd/system/ttyd.service $target/etc/systemd/system/multi-user.target.wants/ttyd.service
 ln -s /etc/systemd/system/ip_addr_reporter.service $target/etc/systemd/system/multi-user.target.wants/ip_addr_reporter.service
@@ -10,5 +11,6 @@
 ln -s /etc/systemd/system/forwarder_guest_launcher.service $target/etc/systemd/system/multi-user.target.wants/forwarder_guest_launcher.service
 ln -s /etc/systemd/system/virtiofs_internal.service $target/etc/systemd/system/multi-user.target.wants/virtiofs_internal.service
 ln -s /etc/systemd/system/backup_mount.service $target/etc/systemd/system/multi-user.target.wants/backup_mount.service
+ln -s /etc/systemd/system/shutdown_runner.service $target/etc/systemd/system/multi-user.target.wants/shutdown_runner.service
 
 sed -i 's/#LLMNR=yes/LLMNR=no/' $target/etc/systemd/resolved.conf
diff --git a/guest/shutdown_runner/.gitignore b/guest/shutdown_runner/.gitignore
new file mode 100644
index 0000000..ea8c4bf
--- /dev/null
+++ b/guest/shutdown_runner/.gitignore
@@ -0,0 +1 @@
+/target
diff --git a/guest/shutdown_runner/Cargo.toml b/guest/shutdown_runner/Cargo.toml
new file mode 100644
index 0000000..b74e7ee
--- /dev/null
+++ b/guest/shutdown_runner/Cargo.toml
@@ -0,0 +1,17 @@
+[package]
+name = "shutdown_runner"
+version = "0.1.0"
+edition = "2021"
+license = "Apache-2.0"
+
+[dependencies]
+anyhow = "1.0.94"
+clap = { version = "4.5.20", features = ["derive"] }
+log = "0.4.22"
+netdev = "0.31.0"
+prost = "0.13.3"
+tokio = { version = "1.40.0", features = ["rt-multi-thread"] }
+tonic = "0.12.3"
+
+[build-dependencies]
+tonic-build = "0.12.3"
diff --git a/guest/shutdown_runner/build.rs b/guest/shutdown_runner/build.rs
new file mode 100644
index 0000000..e3939d4
--- /dev/null
+++ b/guest/shutdown_runner/build.rs
@@ -0,0 +1,7 @@
+fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let proto_file = "../../libs/debian_service/proto/DebianService.proto";
+
+    tonic_build::compile_protos(proto_file).unwrap();
+
+    Ok(())
+}
diff --git a/guest/shutdown_runner/src/main.rs b/guest/shutdown_runner/src/main.rs
new file mode 100644
index 0000000..19e9883
--- /dev/null
+++ b/guest/shutdown_runner/src/main.rs
@@ -0,0 +1,46 @@
+use api::debian_service_client::DebianServiceClient;
+use api::ShutdownQueueOpeningRequest;
+use std::process::Command;
+
+use anyhow::anyhow;
+use clap::Parser;
+use log::debug;
+pub mod api {
+    tonic::include_proto!("com.android.virtualization.terminal.proto");
+}
+
+#[derive(Parser)]
+/// Flags for running command
+pub struct Args {
+    /// grpc port number
+    #[arg(long)]
+    #[arg(alias = "grpc_port")]
+    grpc_port: String,
+}
+
+#[tokio::main]
+async fn main() -> Result<(), Box<dyn std::error::Error>> {
+    let args = Args::parse();
+    let gateway_ip_addr = netdev::get_default_gateway()?.ipv4[0];
+
+    let server_addr = format!("http://{}:{}", gateway_ip_addr.to_string(), args.grpc_port);
+
+    debug!("connect to grpc server {}", server_addr);
+
+    let mut client = DebianServiceClient::connect(server_addr).await.map_err(|e| e.to_string())?;
+
+    let mut res_stream = client
+        .open_shutdown_request_queue(tonic::Request::new(ShutdownQueueOpeningRequest {}))
+        .await?
+        .into_inner();
+
+    while let Some(_response) = res_stream.message().await? {
+        let status = Command::new("poweroff").status().expect("power off");
+        if !status.success() {
+            return Err(anyhow!("Failed to power off: {status}").into());
+        }
+        debug!("poweroff");
+        break;
+    }
+    Ok(())
+}
diff --git a/libs/debian_service/proto/DebianService.proto b/libs/debian_service/proto/DebianService.proto
index 61bcece..739f0ac 100644
--- a/libs/debian_service/proto/DebianService.proto
+++ b/libs/debian_service/proto/DebianService.proto
@@ -25,6 +25,7 @@
   rpc ReportVmActivePorts (ReportVmActivePortsRequest) returns (ReportVmActivePortsResponse) {}
   rpc ReportVmIpAddr (IpAddr) returns (ReportVmIpAddrResponse) {}
   rpc OpenForwardingRequestQueue (QueueOpeningRequest) returns (stream ForwardingRequestItem) {}
+  rpc OpenShutdownRequestQueue (ShutdownQueueOpeningRequest) returns (stream ShutdownRequestItem) {}
 }
 
 message QueueOpeningRequest {
@@ -51,3 +52,7 @@
   int32 guest_tcp_port = 1;
   int32 vsock_port = 2;
 }
+
+message ShutdownQueueOpeningRequest {}
+
+message ShutdownRequestItem {}
\ No newline at end of file