Merge changes from topic "avf_clean" into main
* changes:
Re-organize compos directories
Re-organize authfs directories
diff --git a/android/FerrochromeApp/AndroidManifest.xml b/android/FerrochromeApp/AndroidManifest.xml
index 7afffe5..d640c4a 100644
--- a/android/FerrochromeApp/AndroidManifest.xml
+++ b/android/FerrochromeApp/AndroidManifest.xml
@@ -6,6 +6,8 @@
<uses-permission android:name="android.permission.KILL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.KILL_ALL_BACKGROUND_PROCESSES" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" />
+ <uses-permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER" />
+
<queries>
<intent>
<action android:name="android.virtualization.VM_LAUNCHER" />
diff --git a/android/VmLauncherApp/AndroidManifest.xml b/android/VmLauncherApp/AndroidManifest.xml
index c6ab1f2..67b7a45 100644
--- a/android/VmLauncherApp/AndroidManifest.xml
+++ b/android/VmLauncherApp/AndroidManifest.xml
@@ -7,6 +7,10 @@
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-feature android:name="android.software.virtualization_framework" android:required="true" />
+
+ <permission android:name="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
+ android:protectionLevel="signature|preinstalled"/>
+
<application
android:label="VmLauncherApp">
<activity android:name=".MainActivity"
@@ -14,6 +18,7 @@
android:configChanges="orientation|screenSize|keyboard|keyboardHidden|navigation|uiMode"
android:theme="@style/MyTheme"
android:resizeableActivity="false"
+ android:permission="com.android.virtualization.vmlauncher.permission.USE_VM_LAUNCHER"
android:exported="true">
<intent-filter>
<action android:name="android.virtualization.VM_LAUNCHER" />
diff --git a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
index 0b93968..a93c173 100644
--- a/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
+++ b/android/VmLauncherApp/java/com/android/virtualization/vmlauncher/MainActivity.java
@@ -457,10 +457,16 @@
+ holder.getSurface()
+ ")");
runWithDisplayService(
- (service) ->
- service.setSurface(
+ s ->
+ s.setSurface(
holder.getSurface(),
false /* forCursor */));
+ // TODO execute the above and the below togther with the same call
+ // to runWithDisplayService. Currently this doesn't work because
+ // setSurface somtimes trigger an exception and as a result
+ // drawSavedFrameForSurface is skipped.
+ runWithDisplayService(
+ s -> s.drawSavedFrameForSurface(false /* forCursor */));
}
@Override
@@ -544,6 +550,12 @@
}
@Override
+ protected void onPause() {
+ super.onPause();
+ runWithDisplayService(s -> s.saveFrameForSurface(false /* forCursor */));
+ }
+
+ @Override
protected void onStop() {
super.onStop();
if (mVirtualMachine != null) {
diff --git a/android/virtmgr/Android.bp b/android/virtmgr/Android.bp
index ae85934..a21ee6c 100644
--- a/android/virtmgr/Android.bp
+++ b/android/virtmgr/Android.bp
@@ -87,6 +87,14 @@
apex_available: ["com.android.virt"],
}
+rust_binary {
+ name: "early_virtmgr",
+ defaults: ["virtualizationmanager_defaults"],
+ srcs: ["src/main.rs"],
+ cfgs: ["early"],
+ apex_available: ["com.android.virt"],
+}
+
rust_test {
name: "virtualizationmanager_device_test",
srcs: ["src/main.rs"],
diff --git a/android/virtmgr/src/main.rs b/android/virtmgr/src/main.rs
index 4e88507..445260f 100644
--- a/android/virtmgr/src/main.rs
+++ b/android/virtmgr/src/main.rs
@@ -131,7 +131,11 @@
// Start thread pool for kernel Binder connection to VirtualizationServiceInternal.
ProcessState::start_thread_pool();
- GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
+ if cfg!(early) {
+ panic!("Early VM not implemented");
+ } else {
+ GLOBAL_SERVICE.removeMemlockRlimit().expect("Failed to remove memlock rlimit");
+ }
let service = VirtualizationService::init();
let service =
diff --git a/build/apex/Android.bp b/build/apex/Android.bp
index 5e74aca..8de7b61 100644
--- a/build/apex/Android.bp
+++ b/build/apex/Android.bp
@@ -45,6 +45,7 @@
config_namespace: "ANDROID",
bool_variables: [
"release_avf_enable_device_assignment",
+ "release_avf_enable_early_vm",
"release_avf_enable_llpvm_changes",
"release_avf_enable_network",
"avf_remote_attestation_enabled",
@@ -63,6 +64,7 @@
"systemserverclasspath_fragments",
"vintf_fragments",
"apps",
+ "binaries",
],
}
@@ -210,6 +212,16 @@
"virtualizationservice.xml",
],
},
+ release_avf_enable_early_vm: {
+ arch: {
+ arm64: {
+ binaries: ["early_virtmgr"],
+ },
+ x86_64: {
+ binaries: ["early_virtmgr"],
+ },
+ },
+ },
},
}
diff --git a/demo_accessor/README.md b/demo_accessor/README.md
index a3959a5..c85cf3c 100644
--- a/demo_accessor/README.md
+++ b/demo_accessor/README.md
@@ -28,7 +28,7 @@
```shell
adb remount -R || adb wait-for-device # Remount to push apex to /system_ext
adb root && adb remount # Ensure it's rebooted.
-adb push $ANDROID_PRODUCT_OUT/system_ext/com.android.virt.accessor_demo.apex /system_ext/apex
+adb push $ANDROID_PRODUCT_OUT/system_ext/apex/com.android.virt.accessor_demo.apex /system_ext/apex
adb reboot && adb wait-for-device # Ensure that newly pushed apex at /system_ext is installed
```
diff --git a/docs/custom_vm.md b/docs/custom_vm.md
index 6422678..cdeddf5 100644
--- a/docs/custom_vm.md
+++ b/docs/custom_vm.md
@@ -207,28 +207,17 @@
### Running the VM
-First, enable the `VmLauncherApp` app. This needs to be done only once. In the
-future, this step won't be necesssary.
+1. Grant permission to the `VmLauncherApp` if the virt apex is Google-signed.
+ ```shell
+ $ adb shell su root pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
+ ```
-```
-$ adb root
-$ adb shell pm enable com.android.virtualization.vmlauncher/.MainActivityAlias
-$ adb unroot
-```
+2. Ensure your device is connected to the Internet.
-If virt apex is Google-signed, you need to enable the app and grant the
-permission to the app.
-```
-$ adb root
-$ adb shell pm enable com.google.android.virtualization.vmlauncher/com.android.virtualization.vmlauncher.MainActivityAlias
-$ adb shell pm grant com.google.android.virtualization.vmlauncher android.permission.USE_CUSTOM_VIRTUAL_MACHINE
-$ adb unroot
-```
-
-Second, ensure your device is connected to the Internet.
-
-Finally, tap the VmLauncherApp app from the launcher UI. You will see
-Ferrochrome booting!
+3. Launch the app with adb.
+ ```shell
+ $ adb shell su root am start-activity -a android.virtualization.VM_LAUNCHER
+ ```
If it doesn’t work well, try
diff --git a/libs/android_display_backend/Android.bp b/libs/android_display_backend/Android.bp
index 32587dd..f682627 100644
--- a/libs/android_display_backend/Android.bp
+++ b/libs/android_display_backend/Android.bp
@@ -46,6 +46,9 @@
"android.system.virtualizationcommon-ndk",
"android.system.virtualizationservice-ndk",
],
+ static_libs: [
+ "libbase",
+ ],
shared_libs: [
"libbinder_ndk",
"libnativewindow",
diff --git a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
index e42cdd1..77e3a8c 100644
--- a/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
+++ b/libs/android_display_backend/aidl/android/crosvm/ICrosvmAndroidDisplayService.aidl
@@ -27,4 +27,6 @@
void setSurface(inout Surface surface, boolean forCursor);
void setCursorStream(in ParcelFileDescriptor stream);
void removeSurface(boolean forCursor);
+ void saveFrameForSurface(boolean forCursor);
+ void drawSavedFrameForSurface(boolean forCursor);
}
diff --git a/libs/android_display_backend/crosvm_android_display_client.cpp b/libs/android_display_backend/crosvm_android_display_client.cpp
index 6e4a793..3802a69 100644
--- a/libs/android_display_backend/crosvm_android_display_client.cpp
+++ b/libs/android_display_backend/crosvm_android_display_client.cpp
@@ -16,6 +16,7 @@
#include <aidl/android/crosvm/BnCrosvmAndroidDisplayService.h>
#include <aidl/android/system/virtualizationservice_internal/IVirtualizationServiceInternal.h>
+#include <android-base/result.h>
#include <android/binder_manager.h>
#include <android/binder_process.h>
#include <system/graphics.h> // for HAL_PIXEL_FORMAT_*
@@ -28,16 +29,16 @@
using aidl::android::system::virtualizationservice_internal::IVirtualizationServiceInternal;
using aidl::android::view::Surface;
+using android::base::Error;
+using android::base::Result;
+
namespace {
class SinkANativeWindow_Buffer {
public:
- SinkANativeWindow_Buffer() = default;
- virtual ~SinkANativeWindow_Buffer() = default;
-
- bool configure(uint32_t width, uint32_t height, int format) {
+ Result<void> configure(uint32_t width, uint32_t height, int format) {
if (format != HAL_PIXEL_FORMAT_BGRA_8888) {
- return false;
+ return Error() << "Pixel format " << format << " is not BGRA_8888.";
}
mBufferBits.resize(width * height * 4);
@@ -48,7 +49,7 @@
.format = format,
.bits = mBufferBits.data(),
};
- return true;
+ return {};
}
operator ANativeWindow_Buffer&() { return mBuffer; }
@@ -58,16 +59,28 @@
std::vector<uint8_t> mBufferBits;
};
-// Wrapper which contains the latest available Surface/ANativeWindow
-// from the DisplayService, if available. A Surface/ANativeWindow may
-// not always be available if, for example, the VmLauncherApp on the
-// other end of the DisplayService is not in the foreground / is paused.
+static Result<void> copyBuffer(ANativeWindow_Buffer& from, ANativeWindow_Buffer& to) {
+ if (from.width != to.width || from.height != to.height) {
+ return Error() << "dimension mismatch. from=(" << from.width << ", " << from.height << ") "
+ << "to=(" << to.width << ", " << to.height << ")";
+ }
+ uint32_t* dst = reinterpret_cast<uint32_t*>(to.bits);
+ uint32_t* src = reinterpret_cast<uint32_t*>(from.bits);
+ size_t bytes_on_line = to.width * 4; // 4 bytes per pixel
+ for (int32_t h = 0; h < to.height; h++) {
+ memcpy(dst + (h * to.stride), src + (h * from.stride), bytes_on_line);
+ }
+ return {};
+}
+
+// Wrapper which contains the latest available Surface/ANativeWindow from the DisplayService, if
+// available. A Surface/ANativeWindow may not always be available if, for example, the VmLauncherApp
+// on the other end of the DisplayService is not in the foreground / is paused.
class AndroidDisplaySurface {
public:
- AndroidDisplaySurface() = default;
- virtual ~AndroidDisplaySurface() = default;
+ AndroidDisplaySurface(const std::string& name) : mName(name) {}
- void setSurface(Surface* surface) {
+ void setNativeSurface(Surface* surface) {
{
std::lock_guard lk(mSurfaceMutex);
mNativeSurface = std::make_unique<Surface>(surface->release());
@@ -90,7 +103,7 @@
return mNativeSurface.get();
}
- void configure(uint32_t width, uint32_t height) {
+ Result<void> configure(uint32_t width, uint32_t height) {
std::unique_lock lk(mSurfaceMutex);
mRequestedSurfaceDimensions = Rect{
@@ -98,7 +111,13 @@
.height = height,
};
- mSinkBuffer.configure(width, height, kFormat);
+ if (auto ret = mSinkBuffer.configure(width, height, kFormat); !ret.ok()) {
+ return Error() << "Failed to configure sink buffer: " << ret.error();
+ }
+ if (auto ret = mSavedFrameBuffer.configure(width, height, kFormat); !ret.ok()) {
+ return Error() << "Failed to configure saved frame buffer: " << ret.error();
+ }
+ return {};
}
void waitForNativeSurface() {
@@ -106,7 +125,7 @@
mNativeSurfaceReady.wait(lk, [this] { return mNativeSurface != nullptr; });
}
- int lock(ANativeWindow_Buffer* out_buffer) {
+ Result<void> lock(ANativeWindow_Buffer* out_buffer) {
std::unique_lock lk(mSurfaceMutex);
Surface* surface = mNativeSurface.get();
@@ -114,62 +133,137 @@
// Surface not currently available but not necessarily an error
// if, for example, the VmLauncherApp is not in the foreground.
*out_buffer = mSinkBuffer;
- return 0;
+ return {};
}
ANativeWindow* anw = surface->get();
if (anw == nullptr) {
- return -1;
+ return Error() << "Failed to get ANativeWindow";
}
if (mNativeSurfaceNeedsConfiguring) {
if (!mRequestedSurfaceDimensions) {
- return -1;
+ return Error() << "Surface dimension is not configured yet!";
}
const auto& dims = *mRequestedSurfaceDimensions;
// Ensure locked buffers have our desired format.
if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
- return -1;
+ return Error() << "Failed to set buffer geometry.";
}
mNativeSurfaceNeedsConfiguring = false;
}
- return ANativeWindow_lock(anw, out_buffer, nullptr);
+ if (ANativeWindow_lock(anw, out_buffer, nullptr) != 0) {
+ return Error() << "Failed to lock window";
+ }
+ mLastBuffer = *out_buffer;
+ return {};
}
- int unlockAndPost() {
+ Result<void> unlockAndPost() {
std::unique_lock lk(mSurfaceMutex);
Surface* surface = mNativeSurface.get();
if (surface == nullptr) {
// Surface not currently available but not necessarily an error
// if, for example, the VmLauncherApp is not in the foreground.
- return 0;
+ return {};
}
ANativeWindow* anw = surface->get();
if (anw == nullptr) {
- return -1;
+ return Error() << "Failed to get ANativeWindow";
}
- return ANativeWindow_unlockAndPost(anw);
+ if (ANativeWindow_unlockAndPost(anw) != 0) {
+ return Error() << "Failed to unlock and post window";
+ }
+ return {};
}
+ // Saves the last frame drawn
+ Result<void> saveFrame() {
+ std::unique_lock lk(mSurfaceMutex);
+ if (auto ret = copyBuffer(mLastBuffer, mSavedFrameBuffer); !ret.ok()) {
+ return Error() << "Failed to copy frame: " << ret.error();
+ }
+ return {};
+ }
+
+ // Draws the saved frame
+ Result<void> drawSavedFrame() {
+ std::unique_lock lk(mSurfaceMutex);
+ Surface* surface = mNativeSurface.get();
+ if (surface == nullptr) {
+ return Error() << "Surface not ready";
+ }
+
+ ANativeWindow* anw = surface->get();
+ if (anw == nullptr) {
+ return Error() << "Failed to get ANativeWindow";
+ }
+
+ // TODO: dedup this and the one in lock(...)
+ if (mNativeSurfaceNeedsConfiguring) {
+ if (!mRequestedSurfaceDimensions) {
+ return Error() << "Surface dimension is not configured yet!";
+ }
+ const auto& dims = *mRequestedSurfaceDimensions;
+
+ // Ensure locked buffers have our desired format.
+ if (ANativeWindow_setBuffersGeometry(anw, dims.width, dims.height, kFormat) != 0) {
+ return Error() << "Failed to set buffer geometry.";
+ }
+
+ mNativeSurfaceNeedsConfiguring = false;
+ }
+
+ ANativeWindow_Buffer buf;
+ if (ANativeWindow_lock(anw, &buf, nullptr) != 0) {
+ return Error() << "Failed to lock window";
+ }
+
+ if (auto ret = copyBuffer(mSavedFrameBuffer, buf); !ret.ok()) {
+ return Error() << "Failed to copy frame: " << ret.error();
+ }
+
+ if (ANativeWindow_unlockAndPost(anw) != 0) {
+ return Error() << "Failed to unlock and post window";
+ }
+ return {};
+ }
+
+ const std::string& name() const { return mName; }
+
private:
// Note: crosvm always uses BGRA8888 or BGRX8888. See devices/src/virtio/gpu/mod.rs in
// crosvm where the SetScanoutBlob command is handled. Let's use BGRA not BGRX with a hope
// that we will need alpha blending for the cursor surface.
static constexpr const int kFormat = HAL_PIXEL_FORMAT_BGRA_8888;
+ std::string mName;
+
std::mutex mSurfaceMutex;
std::unique_ptr<Surface> mNativeSurface;
std::condition_variable mNativeSurfaceReady;
bool mNativeSurfaceNeedsConfiguring = true;
+ // Buffer which crosvm uses when in background. This is just to not fail crosvm even when
+ // Android-side Surface doesn't exist. The content drawn here is never displayed on the physical
+ // screen.
SinkANativeWindow_Buffer mSinkBuffer;
+ // Buffer which is currently allocated for crosvm to draw onto. This holds the last frame. This
+ // is what gets displayed on the physical screen.
+ ANativeWindow_Buffer mLastBuffer;
+
+ // Copy of mLastBuffer made by the call saveFrameForSurface. This holds the last good (i.e.
+ // non-blank) frame before the VM goes background. When the VM is brought up to foreground,
+ // this is drawn to the physical screen until the VM starts to emit actual frames.
+ SinkANativeWindow_Buffer mSavedFrameBuffer;
+
struct Rect {
uint32_t width = 0;
uint32_t height = 0;
@@ -183,35 +277,50 @@
virtual ~DisplayService() = default;
ndk::ScopedAStatus setSurface(Surface* surface, bool forCursor) override {
- if (forCursor) {
- mCursor.setSurface(surface);
- } else {
- mScanout.setSurface(surface);
- }
+ getSurface(forCursor).setNativeSurface(surface);
return ::ndk::ScopedAStatus::ok();
}
ndk::ScopedAStatus removeSurface(bool forCursor) override {
- if (forCursor) {
- mCursor.removeSurface();
- } else {
- mScanout.removeSurface();
- }
+ getSurface(forCursor).removeSurface();
return ::ndk::ScopedAStatus::ok();
}
- AndroidDisplaySurface* getCursorSurface() { return &mCursor; }
- AndroidDisplaySurface* getScanoutSurface() { return &mScanout; }
-
ndk::ScopedFileDescriptor& getCursorStream() { return mCursorStream; }
ndk::ScopedAStatus setCursorStream(const ndk::ScopedFileDescriptor& in_stream) {
mCursorStream = ndk::ScopedFileDescriptor(dup(in_stream.get()));
return ::ndk::ScopedAStatus::ok();
}
+ ndk::ScopedAStatus saveFrameForSurface(bool forCursor) override {
+ if (auto ret = getSurface(forCursor).saveFrame(); !ret.ok()) {
+ std::string msg = std::format("Failed to save frame: {}", ret.error().message());
+ return ::ndk::ScopedAStatus(
+ AStatus_fromServiceSpecificErrorWithMessage(-1, msg.c_str()));
+ }
+ return ::ndk::ScopedAStatus::ok();
+ }
+
+ ndk::ScopedAStatus drawSavedFrameForSurface(bool forCursor) override {
+ if (auto ret = getSurface(forCursor).drawSavedFrame(); !ret.ok()) {
+ std::string msg = std::format("Failed to draw saved frame: {}", ret.error().message());
+ return ::ndk::ScopedAStatus(
+ AStatus_fromServiceSpecificErrorWithMessage(-1, msg.c_str()));
+ }
+ return ::ndk::ScopedAStatus::ok();
+ }
+
+ AndroidDisplaySurface& getSurface(bool forCursor) {
+ if (forCursor) {
+ return mCursor;
+ } else {
+ return mScanout;
+ }
+ }
+
private:
- AndroidDisplaySurface mScanout;
- AndroidDisplaySurface mCursor;
+ AndroidDisplaySurface mScanout{"scanout"};
+ AndroidDisplaySurface mCursor{"cursor"};
ndk::ScopedFileDescriptor mCursorStream;
};
@@ -287,21 +396,18 @@
return nullptr;
}
- AndroidDisplaySurface* displaySurface = forCursor ? ctx->disp_service->getCursorSurface()
- : ctx->disp_service->getScanoutSurface();
- if (displaySurface == nullptr) {
- ctx->errorf("AndroidDisplaySurface was not created");
- return nullptr;
+ AndroidDisplaySurface& surface = ctx->disp_service->getSurface(forCursor);
+ if (auto ret = surface.configure(width, height); !ret.ok()) {
+ ctx->errorf("Failed to configure surface %s: %s", surface.name().c_str(),
+ ret.error().message().c_str());
}
- displaySurface->configure(width, height);
-
- displaySurface->waitForNativeSurface(); // this can block
+ surface.waitForNativeSurface(); // this can block
// TODO(b/332785161): if we know that surface can get destroyed dynamically while VM is running,
// consider calling ANativeWindow_acquire here and _release in destroy_android_surface, so that
// crosvm doesn't hold a dangling pointer.
- return displaySurface;
+ return &surface;
}
extern "C" void destroy_android_surface(struct AndroidDisplayContext*, ANativeWindow*) {
@@ -321,8 +427,10 @@
return false;
}
- if (surface->lock(out_buffer) != 0) {
- ctx->errorf("Failed to lock buffer");
+ auto ret = surface->lock(out_buffer);
+ if (!ret.ok()) {
+ ctx->errorf("Failed to lock surface %s: %s", surface->name().c_str(),
+ ret.error().message().c_str());
return false;
}
@@ -351,8 +459,10 @@
return;
}
- if (surface->unlockAndPost() != 0) {
- ctx->errorf("Failed to unlock and post AndroidDisplaySurface.");
- return;
+ auto ret = surface->unlockAndPost();
+ if (!ret.ok()) {
+ ctx->errorf("Failed to unlock and post for surface %s: %s", surface->name().c_str(),
+ ret.error().message().c_str());
}
+ return;
}
diff --git a/libs/libvmbase/src/console.rs b/libs/libvmbase/src/console.rs
index cd05250..7b01bb6 100644
--- a/libs/libvmbase/src/console.rs
+++ b/libs/libvmbase/src/console.rs
@@ -23,8 +23,8 @@
// Matches the UART count in crosvm.
const MAX_CONSOLES: usize = 4;
-static CONSOLES: [SpinMutex<Option<Uart>>; MAX_CONSOLES] =
- [SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None), SpinMutex::new(None)];
+static CONSOLES: [Once<SpinMutex<Uart>>; MAX_CONSOLES] =
+ [Once::new(), Once::new(), Once::new(), Once::new()];
static ADDRESSES: [Once<usize>; MAX_CONSOLES] =
[Once::new(), Once::new(), Once::new(), Once::new()];
@@ -48,10 +48,10 @@
ADDRESSES[i].call_once(|| base_address);
// Initialize the console driver, for normal console accesses.
- let mut console = CONSOLES[i].lock();
- assert!(console.is_none(), "console::init() called more than once");
- // SAFETY: base_address must be the base of a mapped UART.
- console.replace(unsafe { Uart::new(base_address) });
+ assert!(!CONSOLES[i].is_completed(), "console::init() called more than once");
+ // SAFETY: The caller promised that base_address is the base of a mapped UART with no
+ // aliases.
+ CONSOLES[i].call_once(|| SpinMutex::new(unsafe { Uart::new(base_address) }));
}
}
@@ -59,8 +59,7 @@
///
/// Panics if the n-th console was not initialized by calling [`init`] first.
pub fn writeln(n: usize, format_args: Arguments) {
- let mut guard = CONSOLES[n].lock();
- let uart = guard.as_mut().unwrap();
+ let uart = &mut *CONSOLES[n].get().unwrap().lock();
write(uart, format_args).unwrap();
let _ = uart.write_str("\n");