blob: 8aee51db25bdca4bdc53abfd366e7cbf8bdd2585 [file] [log] [blame]
// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use anyhow::{anyhow, Context, Error};
use argh::FromArgs;
use carnelian::{
app::Config,
color::Color,
drawing::{load_font, DisplayRotation, FontFace},
input, make_message,
render::{rive::load_rive, Context as RenderContext},
scene::{
facets::{RiveFacet, TextFacet, TextFacetOptions, TextHorizontalAlignment},
scene::{Scene, SceneBuilder},
},
App, AppAssistant, AppAssistantPtr, AppSender, MessageTarget, Point, Size, ViewAssistant,
ViewAssistantContext, ViewAssistantPtr, ViewKey,
};
use euclid::{point2, size2};
use fidl_fuchsia_boot::ArgumentsMarker;
use fidl_fuchsia_hardware_display::VirtconMode;
use fuchsia_async::{self as fasync};
use fuchsia_watch::PathEvent;
use fuchsia_zircon::Event;
use futures::StreamExt;
use rive_rs::{self as rive};
use std::path::PathBuf;
use fuchsia_zircon as zx;
use std::io::{self, Write};
mod menu;
use menu::{Key, MenuButtonType, MenuEvent, MenuState, MenuStateMachine};
pub mod installer;
use installer::{
find_install_source, get_block_device, get_block_devices, get_bootloader_type, paver_connect,
set_active_configuration, BlockDevice, BootloaderType,
};
pub mod partition;
use partition::Partition;
const INSTALLER_HEADLINE: &'static str = "Fuchsia Workstation Installer";
const BG_COLOR: Color = Color { r: 238, g: 23, b: 128, a: 255 };
const WARN_BG_COLOR: Color = Color { r: 158, g: 11, b: 0, a: 255 };
const HEADING_COLOR: Color = Color::new();
// Menu interaction
const HID_USAGE_KEY_UP: u32 = 82;
const HID_USAGE_KEY_DOWN: u32 = 81;
const HID_USAGE_KEY_ENTER: u32 = 40;
const LOGO_IMAGE_PATH: &str = "/pkg/data/logo.riv";
enum InstallerMessages {
MenuUp,
MenuDown,
MenuEnter,
Error(String),
GotInstallSource(BlockDevice),
GotBootloaderType(BootloaderType),
GotInstallDestinations(Vec<BlockDevice>),
GotBlockDevices(Vec<BlockDevice>),
ProgressUpdate(String),
}
/// Installer
#[derive(Debug, FromArgs)]
#[argh(name = "recovery")]
struct Args {
/// rotate
#[argh(option)]
rotation: Option<DisplayRotation>,
}
#[derive(Clone, Debug, PartialEq)]
struct InstallationPaths {
install_source: Option<BlockDevice>,
install_target: Option<BlockDevice>,
bootloader_type: Option<BootloaderType>,
install_destinations: Vec<BlockDevice>,
available_disks: Vec<BlockDevice>,
}
impl InstallationPaths {
pub fn new() -> InstallationPaths {
InstallationPaths {
install_source: None,
install_target: None,
bootloader_type: None,
install_destinations: Vec::new(),
available_disks: Vec::new(),
}
}
}
struct InstallerAppAssistant {
app_sender: AppSender,
display_rotation: DisplayRotation,
automated: bool,
}
impl InstallerAppAssistant {
fn new(app_sender: AppSender, automated: bool) -> Self {
let args: Args = argh::from_env();
Self {
app_sender,
display_rotation: args.rotation.unwrap_or(DisplayRotation::Deg0),
automated,
}
}
}
impl AppAssistant for InstallerAppAssistant {
fn setup(&mut self) -> Result<(), Error> {
Ok(())
}
fn create_view_assistant(&mut self, view_key: ViewKey) -> Result<ViewAssistantPtr, Error> {
let file = load_rive(LOGO_IMAGE_PATH).ok();
Ok(Box::new(InstallerViewAssistant::new(
&self.app_sender,
view_key,
file,
INSTALLER_HEADLINE,
self.automated,
)?))
}
fn filter_config(&mut self, config: &mut Config) {
config.view_mode = carnelian::app::ViewMode::Direct;
config.virtcon_mode = Some(VirtconMode::Forced);
config.display_rotation = self.display_rotation;
config.buffer_count = Some(1);
}
}
struct RenderResources {
scene: Scene,
}
impl RenderResources {
fn new(
_render_context: &mut RenderContext,
file: &Option<rive::File>,
target_size: Size,
heading: &str,
face: &FontFace,
menu_state_machine: &mut MenuStateMachine,
) -> Self {
let min_dimension = target_size.width.min(target_size.height);
let logo_edge = min_dimension * 0.24;
let text_size = min_dimension / 10.0;
let top_margin = 0.255;
// Set background colour
let bg_color = match menu_state_machine.get_state() {
MenuState::Warning => WARN_BG_COLOR,
MenuState::Error => WARN_BG_COLOR,
_ => BG_COLOR,
};
let mut builder = SceneBuilder::new().background_color(bg_color).round_scene_corners(true);
let logo_size: Size = size2(logo_edge, logo_edge);
// Calculate position for the logo image
let logo_position = {
let x = target_size.width * 0.8;
let y = target_size.height * 0.7;
point2(x, y)
};
if let Some(file) = file {
builder.facet_at_location(
Box::new(
RiveFacet::new_from_file(logo_size, &file, None).expect("facet_from_file"),
),
logo_position,
);
}
let heading_text_location =
point2(target_size.width / 2.0, top_margin + (target_size.height * 0.05));
builder.text(
face.clone(),
&heading,
text_size,
heading_text_location,
TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Center,
color: HEADING_COLOR,
..TextFacetOptions::default()
},
);
// Build menu
menu_builder(&mut builder, menu_state_machine, target_size, heading_text_location, face);
Self { scene: builder.build() }
}
}
struct InstallerViewAssistant {
face: FontFace,
heading: &'static str,
menu_state_machine: MenuStateMachine,
installation_paths: InstallationPaths,
app_sender: AppSender,
view_key: ViewKey,
file: Option<rive::File>,
render_resources: Option<RenderResources>,
automated: bool,
prev_state: MenuState,
}
impl InstallerViewAssistant {
fn new(
app_sender: &AppSender,
view_key: ViewKey,
file: Option<rive::File>,
heading: &'static str,
automated: bool,
) -> Result<InstallerViewAssistant, Error> {
InstallerViewAssistant::setup(app_sender, view_key)?;
let face = load_font(PathBuf::from("/pkg/data/fonts/Roboto-Regular.ttf"))?;
Ok(InstallerViewAssistant {
face,
heading: heading,
menu_state_machine: MenuStateMachine::new(),
installation_paths: InstallationPaths::new(),
app_sender: app_sender.clone(),
view_key,
file,
render_resources: None,
automated,
prev_state: MenuState::Warning,
})
}
fn setup(_: &AppSender, _: ViewKey) -> Result<(), Error> {
Ok(())
}
fn handle_installer_message(&mut self, message: &InstallerMessages) {
match message {
// Menu Interaction
InstallerMessages::MenuUp => {
self.menu_state_machine.handle_event(MenuEvent::Navigate(Key::Up));
}
InstallerMessages::MenuDown => {
self.menu_state_machine.handle_event(MenuEvent::Navigate(Key::Down));
}
InstallerMessages::MenuEnter => {
// Get disks if usb install selected
match self.menu_state_machine.get_selected_button_type() {
MenuButtonType::USBInstall => {
// Get installation targets
fasync::Task::local(setup_installation_paths(
self.app_sender.clone(),
self.view_key,
))
.detach();
}
MenuButtonType::Disk(target) => {
// Disk was selected as installation target
self.installation_paths.install_target = Some(target.clone());
self.menu_state_machine.handle_event(MenuEvent::Enter);
}
MenuButtonType::Yes => {
// User agrees to wipe disk and isntall
self.menu_state_machine.handle_event(MenuEvent::Enter);
fasync::Task::local(fuchsia_install(
self.app_sender.clone(),
self.view_key,
self.installation_paths.clone(),
))
.detach();
}
_ => {
self.menu_state_machine.handle_event(MenuEvent::Enter);
}
}
}
InstallerMessages::Error(error_msg) => {
self.menu_state_machine.handle_event(MenuEvent::Error(error_msg.clone()));
}
InstallerMessages::GotInstallSource(install_source_path) => {
self.installation_paths.install_source = Some(install_source_path.clone());
}
InstallerMessages::GotBootloaderType(bootloader_type) => {
self.installation_paths.bootloader_type = Some(bootloader_type.clone());
}
InstallerMessages::GotInstallDestinations(destinations) => {
self.installation_paths.install_destinations = destinations.clone();
// Send disks to menu
self.menu_state_machine
.handle_event(MenuEvent::GotBlockDevices(destinations.clone()));
}
InstallerMessages::GotBlockDevices(devices) => {
self.installation_paths.available_disks = devices.clone();
}
InstallerMessages::ProgressUpdate(string) => {
self.menu_state_machine.handle_event(MenuEvent::ProgressUpdate(string.clone()));
}
}
// Render menu changes
self.render_resources = None;
self.app_sender.request_render(self.view_key);
}
}
impl ViewAssistant for InstallerViewAssistant {
fn setup(&mut self, context: &ViewAssistantContext) -> Result<(), Error> {
self.view_key = context.key;
Ok(())
}
fn render(
&mut self,
_render_context: &mut RenderContext,
ready_event: Event,
context: &ViewAssistantContext,
) -> Result<(), Error> {
// Emulate the size that Carnelian passes when the display is rotated
let target_size = context.size;
if self.render_resources.is_none() {
self.render_resources = Some(RenderResources::new(
_render_context,
&self.file,
target_size,
self.heading,
&self.face,
&mut self.menu_state_machine,
));
}
let render_resources = self.render_resources.as_mut().unwrap();
render_resources.scene.render(_render_context, ready_event, context)?;
context.request_render();
if self.automated && self.menu_state_machine.get_state() != self.prev_state {
self.prev_state = self.menu_state_machine.get_state();
match self.menu_state_machine.get_state() {
MenuState::SelectInstall | MenuState::SelectDisk | MenuState::Warning => {
println!(
"installer: {:?}, proceeding to next screen",
self.menu_state_machine.get_state()
);
self.app_sender.queue_message(
MessageTarget::View(self.view_key),
make_message(InstallerMessages::MenuEnter),
);
}
MenuState::Progress => println!("Install in progress"),
MenuState::Error => println!("install failed :("),
}
}
Ok(())
}
fn handle_keyboard_event(
&mut self,
context: &mut ViewAssistantContext,
_event: &input::Event,
keyboard_event: &input::keyboard::Event,
) -> Result<(), Error> {
if keyboard_event.phase == input::keyboard::Phase::Pressed {
let pressed_key = keyboard_event.hid_usage;
match pressed_key {
HID_USAGE_KEY_UP => {
context.queue_message(make_message(InstallerMessages::MenuUp));
}
HID_USAGE_KEY_DOWN => {
context.queue_message(make_message(InstallerMessages::MenuDown));
}
HID_USAGE_KEY_ENTER => {
context.queue_message(make_message(InstallerMessages::MenuEnter));
}
_ => {}
}
}
Ok(())
}
fn handle_message(&mut self, message: carnelian::Message) {
if let Some(message) = message.downcast_ref::<InstallerMessages>() {
self.handle_installer_message(message);
}
}
}
fn menu_builder(
builder: &mut SceneBuilder,
menu_state_machine: &mut MenuStateMachine,
target_size: Size,
heading_location: Point,
face: &FontFace,
) {
// Installer subheading properties
let subheading_size = target_size.width.min(target_size.height) / 15.0;
let subheading_x = heading_location.x;
let subheading_y = heading_location.y + (subheading_size * 2.0);
let subheading_location = point2(subheading_x, subheading_y);
let text_options = TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Center,
..TextFacetOptions::default()
};
// Render subheading
let subheading_facet = TextFacet::with_options(
face.clone(),
&menu_state_machine.get_heading(),
subheading_size,
text_options,
);
builder.facet_at_location(subheading_facet, subheading_location);
// Default button properties
let mut text_size: f32 = target_size.width.min(target_size.height) / 20.0;
// Render state specific things
let menu_state = menu_state_machine.get_state();
match menu_state {
MenuState::SelectInstall => {
render_buttons_vec(
builder,
menu_state_machine,
target_size,
subheading_location,
face,
text_size,
);
}
MenuState::SelectDisk => {
text_size = target_size.width.min(target_size.height) / 33.0;
render_buttons_vec(
builder,
menu_state_machine,
target_size,
subheading_location,
face,
text_size,
);
}
MenuState::Warning => {
// TODO(fxbug.dev/92116):): figure out \n alignment quirk so this can be one message
// Additional messages
let warn_facet = TextFacet::with_options(
face.clone(),
menu::CONST_WARN_MESSAGE,
subheading_size,
text_options,
);
let warn_msg_y = subheading_location.y + (subheading_size * 2.0);
let warn_msg_location = point2(subheading_x, warn_msg_y);
builder.facet_at_location(warn_facet, warn_msg_location);
// Proceed message
let proceed_facet = TextFacet::with_options(
face.clone(),
menu::CONST_WARN_PROCEED,
subheading_size,
text_options,
);
let proceed_msg_y = warn_msg_y + (subheading_size * 2.0);
let proceed_msg_location = point2(subheading_x, proceed_msg_y);
builder.facet_at_location(proceed_facet, proceed_msg_location);
// Render buttons further down for warning screen
render_buttons_vec(
builder,
menu_state_machine,
target_size,
proceed_msg_location,
face,
text_size,
);
}
MenuState::Progress => {
// progress message
text_size = target_size.width.min(target_size.height) / 25.0;
let progress_facet = TextFacet::with_options(
face.clone(),
&menu_state_machine.get_error_msg(),
text_size,
text_options,
);
let progress_msg_y = subheading_location.y + (text_size * 5.0);
let progress_msg_location = point2(subheading_x, progress_msg_y);
builder.facet_at_location(progress_facet, progress_msg_location);
}
MenuState::Error => {
// Render body
let error_facet = TextFacet::with_options(
face.clone(),
&menu_state_machine.get_error_msg(),
subheading_size,
text_options,
);
let err_msg_y = subheading_location.y + (subheading_size * 2.0);
let err_msg_location = point2(subheading_x, err_msg_y);
builder.facet_at_location(error_facet, err_msg_location);
// Ask to restart
let restart_facet = TextFacet::with_options(
face.clone(),
menu::CONST_ERR_RESTART,
subheading_size,
text_options,
);
let restart_msg_y = err_msg_y + (subheading_size * 2.0);
let restart_msg_location = point2(subheading_x, restart_msg_y);
builder.facet_at_location(restart_facet, restart_msg_location);
}
}
}
fn render_buttons_vec(
builder: &mut SceneBuilder,
menu_state_machine: &mut MenuStateMachine,
target_size: Size,
heading_location: Point,
face: &FontFace,
text_size: f32,
) {
let menu_button_x: f32 = target_size.width * 0.2;
let menu_button_y: f32 = heading_location.y + target_size.width.min(target_size.height) * 0.2;
let menu_button_spacer: f32 = text_size * 1.5;
let menu_buttons = menu_state_machine.get_buttons();
let mut button_spacer: f32 = 0.0;
for button in menu_buttons.iter() {
let mut text_options = TextFacetOptions {
horizontal_alignment: TextHorizontalAlignment::Left,
..TextFacetOptions::default()
};
if button.is_selected() {
text_options.color = Color::white();
};
let new_menu_button =
TextFacet::with_options(face.clone(), &button.get_text(), text_size, text_options);
builder.facet_at_location(
new_menu_button,
point2(menu_button_x, menu_button_y + button_spacer),
);
button_spacer += menu_button_spacer;
}
}
async fn get_installation_paths(app_sender: AppSender, view_key: ViewKey) -> Result<(), Error> {
let block_devices = get_block_devices().await?;
let bootloader_type = get_bootloader_type().await?;
// Send got bootloader message
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::GotBootloaderType(bootloader_type)),
);
// Find the location of the installer
let install_source = find_install_source(&block_devices, bootloader_type).await?;
// Send got installer messgae
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::GotInstallSource(install_source.clone())),
);
// Make list of available destinations for installation
let mut destinations = Vec::new();
for block_device in block_devices.iter() {
if block_device != install_source {
destinations.push(block_device.clone());
}
}
// Send error if no destinations found
if destinations.is_empty() {
return Err(anyhow!("Found no block devices for installation."));
};
app_sender.queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::GotBlockDevices(block_devices)),
);
// Else end destinations
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::GotInstallDestinations(
destinations.into_iter().filter(|d| d.is_disk()).collect(),
)),
);
Ok(())
}
async fn setup_installation_paths(app_sender: AppSender, view_key: ViewKey) {
match get_installation_paths(app_sender.clone(), view_key).await {
Ok(_install_source) => {
println!("Found installer & block devices ");
}
Err(e) => {
// Send error
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::Error(e.to_string())),
);
println!("ERROR getting install target: {}", e);
}
};
}
async fn fuchsia_install(
app_sender: AppSender,
view_key: ViewKey,
installation_paths: InstallationPaths,
) {
// Execute install
match do_install(app_sender.clone(), view_key, installation_paths).await {
Ok(_) => {
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from(
"Success! Please restart your computer",
))),
);
}
Err(e) => {
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::Error(String::from(format!(
"Error {}, please restart",
e
)))),
);
}
}
}
async fn do_install(
app_sender: AppSender,
view_key: ViewKey,
installation_paths: InstallationPaths,
) -> Result<(), Error> {
let install_target =
installation_paths.install_target.ok_or(anyhow!("No installation target?"))?;
let install_source =
installation_paths.install_source.ok_or(anyhow!("No installation source?"))?;
let bootloader_type = installation_paths.bootloader_type.unwrap();
// TODO(fxbug.dev/100712): Remove this once flake is resolved.
println!("Installing to {} ({}), source {} ({})", install_target.topo_path, install_target.class_path, install_source.topo_path, install_source.class_path);
let (paver, data_sink) =
paver_connect(&install_target.class_path).context("Could not contact paver")?;
println!("Wiping old partition tables...");
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from(
"Wiping old partition tables...",
))),
);
data_sink.wipe_partition_tables().await?;
println!("Initializing Fuchsia partition tables...");
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from(
"Initializing Fuchsia partition tables...",
))),
);
data_sink.initialize_partition_tables().await?;
println!("Success.");
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from("Getting source partitions"))),
);
let to_install = Partition::get_partitions(
&install_source,
&installation_paths.available_disks,
bootloader_type,
)
.await
.context("Getting source partitions")?;
let num_partitions = to_install.len();
let mut current_partition = 1;
for part in to_install {
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from(format!(
"paving partition {} of {}",
current_partition, num_partitions
)))),
);
current_partition += 1;
print!("{:?}... ", part);
io::stdout().flush()?;
if let Err(e) = part.pave(&data_sink).await {
println!("Failed ({:?})", e);
} else {
println!("OK");
if part.is_ab() {
print!("{:?} [-B]... ", part);
io::stdout().flush()?;
if part.pave_b(&data_sink).await.is_err() {
println!("Failed");
} else {
println!("OK");
}
}
}
}
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from("Flushing Partitions"))),
);
zx::Status::ok(data_sink.flush().await.context("Sending flush")?)
.context("Flushing partitions")?;
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from(
"Setting active configuration for the new system",
))),
);
set_active_configuration(&paver)
.await
.context("Setting active configuration for the new system")?;
app_sender.clone().queue_message(
MessageTarget::View(view_key),
make_message(InstallerMessages::ProgressUpdate(String::from("Configureation complete!!"))),
);
Ok(())
}
/// Wait for a display to become available.
async fn wait_for_display() -> Result<(), Error> {
let mut stream =
fuchsia_watch::watch("/dev/class/display-controller").await.context("Starting watch")?;
while let Some(element) = stream.next().await {
match element {
PathEvent::Added(_, _) | PathEvent::Existing(_, _) => return Ok(()),
_ => {}
}
}
Err(anyhow!("Didn't find a display device"))
}
/// Check to see if we're doing a non-interactive installs.
/// Non-interactive installs are very limited and will likely only work on systems with a single
/// disk.
/// They are intended to be used in end-to-end tests.
async fn check_is_interactive() -> Result<bool, Error> {
let proxy = fuchsia_component::client::connect_to_protocol::<ArgumentsMarker>()
.context("Connecting to boot arguments service")?;
let automated =
proxy.get_bool("installer.non-interactive", false).await.context("Getting bool")?;
println!(
"workstation installer: {}doing automated install.",
if automated { "" } else { "not " }
);
if automated {
wait_for_install_disk().await.context("Waiting for install disk")?;
}
Ok(automated)
}
/// Wait for an installation source to become present on the system.
async fn wait_for_install_disk() -> Result<(), Error> {
let mut stream = fuchsia_watch::watch("/dev/class/block").await.context("Starting watch")?;
let bootloader_type = get_bootloader_type().await?;
let mut devices = vec![];
while let Some(element) = stream.next().await {
match element {
PathEvent::Added(path, _) | PathEvent::Existing(path, _) => {
match get_block_device(path.to_str().unwrap().to_owned()).await {
Ok(Some(bd)) => {
devices.push(bd);
if let Ok(_) = find_install_source(&devices, bootloader_type).await {
return Ok(());
}
}
_ => {}
}
}
_ => {}
}
}
Err(anyhow!("Didn't find an install disk"))
}
fn main() -> Result<(), Error> {
println!("workstation installer: started.");
// Before we give control to carnelian, wait until a display driver is bound.
let (display_result, interactive_result) =
fuchsia_async::LocalExecutor::new().context("Creating executor")?.run_singlethreaded(
async move { futures::join!(wait_for_display(), check_is_interactive()) },
);
display_result.context("Waiting for display controller")?;
let automated = interactive_result.context("Fetching installer boot arguments")?;
App::run(Box::new(move |app_sender: &AppSender| {
Box::pin(async move {
let assistant = Box::new(InstallerAppAssistant::new(app_sender.clone(), automated));
Ok::<AppAssistantPtr, Error>(assistant)
})
}))
}