| // Copyright 2023 The ChromiumOS Authors |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| // service module provides code entry path from D-BUS. Hence, functions here |
| // are naturally mapped to D-BUS methods. |
| // D-BUS methods and signals for other services (ex. concierge) are in |
| // individual modules. |
| |
| mod concierge; |
| mod dlc; |
| pub mod helper; |
| pub mod signal; |
| pub mod spaced; |
| |
| use crate::common::*; |
| use crate::dbus_wrapper::DbusConnectionTrait; |
| use crate::dlc_queue::DlcQueuePtr; |
| use crate::shader_cache_mount::{ShaderCacheMount, ShaderCacheMountMapPtr, VmId}; |
| |
| use anyhow::{anyhow, Result}; |
| use dbus::MethodErr; |
| use log::{debug, error, info, warn}; |
| use protobuf::Message; |
| use std::path::{Path, PathBuf}; |
| use std::sync::Arc; |
| use system_api::shadercached::{ |
| InstallRequest, InstallResponse, PrepareShaderCacheRequest, PrepareShaderCacheResponse, |
| PurgeRequest, UninstallRequest, UnmountRequest, |
| }; |
| |
| // Selectively expose service methods |
| pub use concierge::add_shader_cache_group_permission; |
| pub use concierge::handle_vm_stopped; |
| pub use dlc::handle_dlc_state_changed; |
| pub use dlc::mount_dlc; |
| pub use dlc::periodic_dlc_handler; |
| pub use spaced::handle_disk_space_update; |
| |
| #[cfg(test)] |
| use helper::unsafe_ops::mock_quota::set_quota_normal; |
| #[cfg(not(test))] |
| use helper::unsafe_ops::quota::set_quota_normal; |
| |
| pub async fn handle_install<D: DbusConnectionTrait>( |
| raw_bytes: Vec<u8>, |
| mount_map: ShaderCacheMountMapPtr, |
| dlc_queue: DlcQueuePtr, |
| dbus_conn: Arc<D>, |
| ) -> Result<std::vec::Vec<u8>> { |
| let request: InstallRequest = Message::parse_from_bytes(&raw_bytes)?; |
| |
| let vm_id = VmId { |
| vm_name: request.vm_name, |
| vm_owner_id: request.vm_owner_id, |
| }; |
| |
| let mut mut_mount_map = mount_map.write().await; |
| // TODO(b/271776528): move the insert ShaderCacheMount to |
| // PrepareShaderCache method response once we support local RW shader |
| // cache in shadercached. |
| if !mut_mount_map.contains_key(&vm_id) { |
| let vm_gpu_cache_path = |
| Path::new(&concierge::get_vm_gpu_cache_path(&vm_id, dbus_conn.clone()).await?) |
| .to_path_buf(); |
| mut_mount_map.insert( |
| vm_id.clone(), |
| ShaderCacheMount::new(vm_gpu_cache_path, &vm_id)?, |
| ); |
| } else { |
| debug!("Reusing mount information for {:?}", vm_id); |
| } |
| |
| let shader_cache_mount = mut_mount_map |
| .get_mut(&vm_id) |
| .ok_or_else(|| MethodErr::failed("Failed to get mount information"))?; |
| |
| // Mesa cache path initialization must succeed before we enqueue mount |
| // Repeated initializations are no-op. |
| // |initialize()| will fail iff mesa shader cache has not been |
| // initialized - i.e. VM launched but no applications have run before. Under |
| // Normal circumstances, that's not happening because Steam Store UI would |
| // have triggered mesa to initialize shader cache. |
| // Hence, treat initialize attempt failure as failure. We are not supporting |
| // the case of manually triggering shadercached before Steam launch. |
| shader_cache_mount |
| .initialize(&vm_id, dbus_conn.clone()) |
| .await?; |
| |
| // Save the current mount status in the response |
| let mut response: InstallResponse = InstallResponse::new(); |
| if let Ok(is_mounted) = shader_cache_mount.is_game_mounted(request.steam_app_id, None) { |
| // Shader cache might be mounted from the previous mount call. |
| response.mounted = is_mounted; |
| } |
| |
| if request.mount && !response.mounted { |
| // Queue mount if not mounted already |
| shader_cache_mount.enqueue_mount(request.steam_app_id); |
| } |
| |
| if !is_dlc_installed(request.steam_app_id) { |
| if response.mounted { |
| error!("Invalid state, DLC not installed but reported as mount"); |
| error!("Proceeding with DLC installation"); |
| } |
| // Queue install and upon installation completion, mount if queued. |
| info!("Acquiring dlc write mutex to queue install"); |
| let mut dlc_queue = dlc_queue.write().await; |
| if let Some(dequeued_game) = dlc_queue.queue_install(&request.steam_app_id) { |
| shader_cache_mount.dequeue_mount(&dequeued_game); |
| } |
| info!("Successfully queued installation"); |
| return Ok(response.write_to_bytes()?); |
| } |
| |
| drop(mut_mount_map); |
| debug!( |
| "Shader cached already installed for {}", |
| request.steam_app_id |
| ); |
| if request.mount && !response.mounted { |
| // Mount if dlc is not mounted already |
| response.mounted = mount_dlc(request.steam_app_id, mount_map.clone(), dbus_conn.clone()) |
| .await |
| .is_ok(); |
| debug!("Mounting result: {}", response.mounted); |
| } |
| Ok(response.write_to_bytes()?) |
| } |
| |
| fn is_dlc_installed(steam_app_id: SteamAppId) -> bool { |
| // /run/imageloader/<dlc_name>/package must exist for DLC to be |
| // installed. |
| IMAGE_LOADER |
| .join(steam_app_id_to_dlc(steam_app_id)) |
| .join("package") |
| .exists() |
| } |
| |
| pub async fn handle_uninstall(raw_bytes: Vec<u8>, dlc_queue: DlcQueuePtr) -> Result<()> { |
| let request: UninstallRequest = protobuf::Message::parse_from_bytes(&raw_bytes)?; |
| let mut dlc_queue = dlc_queue.write().await; |
| dlc_queue.queue_uninstall(&request.steam_app_id); |
| Ok(()) |
| } |
| |
| pub async fn unmount_and_uninstall_all_shader_cache_dlcs<D: DbusConnectionTrait>( |
| mount_map: ShaderCacheMountMapPtr, |
| dbus_conn: Arc<D>, |
| ) -> Result<()> { |
| mount_map.clear_all_mounts(None).await?; |
| mount_map |
| .wait_unmount_completed(None, UNMOUNTER_INTERVAL * 2) |
| .await?; |
| |
| // TODO(b/270262568): Queue DLC uninstallations instead of waiting for |
| // unmounts. Ensure to disallow mounts during the period and allow mounts |
| // after completed. |
| dlc::uninstall_all_shader_cache_dlcs(dbus_conn.clone()).await?; |
| |
| Ok(()) |
| } |
| |
| pub async fn handle_purge<D: DbusConnectionTrait>( |
| raw_bytes: Vec<u8>, |
| mount_map: ShaderCacheMountMapPtr, |
| dbus_conn: Arc<D>, |
| ) -> Result<()> { |
| let request: PurgeRequest = protobuf::Message::parse_from_bytes(&raw_bytes)?; |
| |
| let mount_map_size = { mount_map.clone().read().await.len() }; |
| if mount_map_size <= 1 { |
| // If only one active user, unmount for all users and uninstall all |
| // DLCs. Shadercached does not keep state of users globally, hence |
| // it cannot know if Borealis is still in use for other users. This |
| // means another user may have to redownload the DLC, which is fine for |
| // now. |
| unmount_and_uninstall_all_shader_cache_dlcs(mount_map.clone(), dbus_conn.clone()).await?; |
| } |
| let vm_id = VmId { |
| vm_name: request.vm_name, |
| vm_owner_id: request.vm_owner_id, |
| }; |
| let cleared_dirs = spaced::delete_precompiled_cache(mount_map.clone(), vm_id).await?; |
| for dir_path in cleared_dirs { |
| std::fs::remove_dir(dir_path)?; |
| } |
| Ok(()) |
| } |
| |
| pub async fn handle_prepare_shader_cache( |
| raw_bytes: Vec<u8>, |
| _mount_map: ShaderCacheMountMapPtr, |
| ) -> Result<std::vec::Vec<u8>> { |
| let request: PrepareShaderCacheRequest = protobuf::Message::parse_from_bytes(&raw_bytes)?; |
| let mut response = PrepareShaderCacheResponse::new(); |
| if request.vm_name.is_empty() || request.vm_owner_id.is_empty() { |
| return Err(anyhow!("vm_name and vm_owner_id must be set")); |
| } |
| |
| let vm_id = VmId { |
| vm_name: request.vm_name, |
| vm_owner_id: request.vm_owner_id, |
| }; |
| debug!("Preparing shader cache for {:?}", vm_id); |
| |
| // TODO(b/271776528): insert ShaderCacheMount here once we support local RW |
| // shader cache in shadercached. |
| let shader_cache_mount = ShaderCacheMount::new(PathBuf::new(), &vm_id)?; |
| let path_str = shader_cache_mount.local_precompiled_cache_path()?; |
| let path = Path::new(&path_str); |
| if !path.exists() { |
| std::fs::create_dir_all(&path_str)?; |
| } |
| if let Err(e) = set_quota_normal(path) { |
| warn!("Failed to set quota for {}: {}", path_str, e); |
| } |
| response.precompiled_cache_path = path_str; |
| |
| Ok(response.write_to_bytes()?) |
| } |
| |
| pub async fn handle_unmount(raw_bytes: Vec<u8>, mount_map: ShaderCacheMountMapPtr) -> Result<()> { |
| let request: UnmountRequest = protobuf::Message::parse_from_bytes(&raw_bytes)?; |
| let vm_id = VmId { |
| vm_name: request.vm_name, |
| vm_owner_id: request.vm_owner_id, |
| }; |
| |
| let mut mount_map = mount_map.write().await; |
| if let Some(shader_cache_mount) = mount_map.get_mut(&vm_id) { |
| // If VM had mounted something before but nothing is currently mounted, |
| // unmount will be queued and unmount will succeed because there is |
| // nothing mounted. |
| // Shadercached will fire unmount signal in result, which can be |
| // picked up by `garcon <..> --unmount --wait` process and exit |
| // with success code. |
| // (note: --wait flag probably only needed for tasts and manual |
| // debugging). |
| shader_cache_mount.remove_game_from_db_list(request.steam_app_id)?; |
| } else { |
| return Err(anyhow!("VM had never mounted shader cache")); |
| } |
| |
| Ok(()) |
| } |