Sync source to CNB / Sync Git ref (push) Has been cancelled
- ProcessManager 新增 has_instance_process(备份启动守卫依赖) - 删除/启动/编辑/迁移目录时接入备份维护钩子(lock_instance_maintenance + begin_instance_deletion + delete_instance_backups),remove_instance 拆出 remove_instance_files_and_state - run 启动前检查活跃备份操作并加维护锁 - 目录迁移时搬移默认备份仓库并入队清理 - 更新 settings SELECT 的 SQLX 离线缓存(补 ui_font/backup_repository_path/mono_font) - create_recorded_symlink 非 Windows 未用参数加 cfg_attr 豁免
345 lines
11 KiB
Rust
345 lines
11 KiB
Rust
use crate::server_address::ServerAddress;
|
|
use crate::state::{
|
|
Credentials, InstanceInstallStage, ProcessMetadata, Settings, State,
|
|
};
|
|
use crate::util::io::IOError;
|
|
use std::time::Duration;
|
|
use tokio::process::Command;
|
|
|
|
pub use crate::launcher::jvm_args::{GcLaunchIntent, GcLaunchReport};
|
|
|
|
const DEFAULT_LAUNCH_PREPARATION_TIMEOUT: u64 = 60;
|
|
const MIN_LAUNCH_PREPARATION_TIMEOUT: u64 = 30;
|
|
const MAX_LAUNCH_PREPARATION_TIMEOUT: u64 = 600;
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub enum QuickPlayType {
|
|
None,
|
|
Singleplayer(String),
|
|
Server(ServerAddress),
|
|
}
|
|
|
|
#[tracing::instrument]
|
|
pub async fn run(
|
|
instance_id: &str,
|
|
quick_play_type: QuickPlayType,
|
|
offline_mode: bool,
|
|
) -> crate::Result<ProcessMetadata> {
|
|
run_with_extra_launch_args(instance_id, quick_play_type, offline_mode, None)
|
|
.await
|
|
}
|
|
|
|
#[tracing::instrument]
|
|
pub async fn run_with_extra_launch_args(
|
|
instance_id: &str,
|
|
quick_play_type: QuickPlayType,
|
|
offline_mode: bool,
|
|
extra_launch_args: Option<Vec<String>>,
|
|
) -> crate::Result<ProcessMetadata> {
|
|
Ok(run_with_extra_launch_args_inner(
|
|
instance_id,
|
|
quick_play_type,
|
|
offline_mode,
|
|
extra_launch_args,
|
|
None,
|
|
)
|
|
.await?
|
|
.0)
|
|
}
|
|
|
|
/// Like [`run_with_extra_launch_args`], but additionally resolves a GC-args
|
|
/// intent against the actual JVM and reports what was actually used (useful
|
|
/// for surfacing strategy fallback / flag pruning to the user).
|
|
#[tracing::instrument]
|
|
pub async fn run_with_extra_launch_args_with_gc(
|
|
instance_id: &str,
|
|
quick_play_type: QuickPlayType,
|
|
offline_mode: bool,
|
|
extra_launch_args: Option<Vec<String>>,
|
|
gc_intent: Option<GcLaunchIntent>,
|
|
) -> crate::Result<(ProcessMetadata, Option<GcLaunchReport>)> {
|
|
run_with_extra_launch_args_inner(
|
|
instance_id,
|
|
quick_play_type,
|
|
offline_mode,
|
|
extra_launch_args,
|
|
gc_intent,
|
|
)
|
|
.await
|
|
}
|
|
|
|
async fn run_with_extra_launch_args_inner(
|
|
instance_id: &str,
|
|
quick_play_type: QuickPlayType,
|
|
offline_mode: bool,
|
|
extra_launch_args: Option<Vec<String>>,
|
|
gc_intent: Option<GcLaunchIntent>,
|
|
) -> crate::Result<(ProcessMetadata, Option<GcLaunchReport>)> {
|
|
if crate::api::instance::has_active_instance_operation(instance_id) {
|
|
return Err(crate::ErrorKind::InputError(
|
|
"Wait for the active backup operation to finish before launching this instance"
|
|
.to_string(),
|
|
)
|
|
.into());
|
|
}
|
|
let _maintenance_guard =
|
|
crate::api::instance::lock_instance_maintenance(instance_id).await;
|
|
let _hosted_guard =
|
|
crate::pack::hosted::prepare_launch(instance_id, offline_mode).await?;
|
|
let state = State::get().await?;
|
|
let context =
|
|
crate::state::instances::commands::get_instance_launch_context(
|
|
instance_id,
|
|
&state.pool,
|
|
)
|
|
.await?;
|
|
let launch_preparation_timeout = context
|
|
.as_ref()
|
|
.and_then(|context| context.launch_overrides.launch_preparation_timeout)
|
|
.unwrap_or(DEFAULT_LAUNCH_PREPARATION_TIMEOUT)
|
|
.clamp(
|
|
MIN_LAUNCH_PREPARATION_TIMEOUT,
|
|
MAX_LAUNCH_PREPARATION_TIMEOUT,
|
|
);
|
|
let saved_player = context
|
|
.as_ref()
|
|
.and_then(|context| context.launch_overrides.player.as_ref());
|
|
let default_account = if let Some(player) = saved_player {
|
|
if offline_mode
|
|
&& player.account_type
|
|
!= crate::state::MinecraftAccountType::Offline
|
|
{
|
|
return Err(crate::ErrorKind::InputError("当前实例绑定的是在线玩家,请恢复网络后启动,或在实例设置中手动切换玩家".into()).as_error());
|
|
}
|
|
Credentials::for_instance_player(player, &state.pool).await?
|
|
} else if offline_mode {
|
|
Credentials::get_offline_credential(&state.pool)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
crate::ErrorKind::LauncherError(
|
|
"Offline mode requires an offline Minecraft account"
|
|
.to_string(),
|
|
)
|
|
.as_error()
|
|
})?
|
|
} else {
|
|
Credentials::get_default_credential(&state.pool)
|
|
.await?
|
|
.ok_or_else(|| crate::ErrorKind::NoCredentialsError.as_error())?
|
|
};
|
|
|
|
tokio::time::timeout(
|
|
Duration::from_secs(launch_preparation_timeout),
|
|
run_credentials(
|
|
instance_id,
|
|
&default_account,
|
|
quick_play_type,
|
|
offline_mode,
|
|
extra_launch_args,
|
|
gc_intent,
|
|
),
|
|
)
|
|
.await
|
|
.map_err(|_| {
|
|
crate::ErrorKind::LauncherError(
|
|
format!(
|
|
"Minecraft launch preparation timed out after {launch_preparation_timeout} seconds"
|
|
),
|
|
)
|
|
.as_error()
|
|
})?
|
|
}
|
|
|
|
#[tracing::instrument(skip(credentials))]
|
|
async fn run_credentials(
|
|
instance_id: &str,
|
|
credentials: &Credentials,
|
|
quick_play_type: QuickPlayType,
|
|
offline_mode: bool,
|
|
extra_launch_args: Option<Vec<String>>,
|
|
gc_intent: Option<GcLaunchIntent>,
|
|
) -> crate::Result<(ProcessMetadata, Option<GcLaunchReport>)> {
|
|
let state = State::get().await?;
|
|
let settings = Settings::get(&state.pool).await?;
|
|
let context =
|
|
crate::state::instances::commands::get_instance_launch_context(
|
|
instance_id,
|
|
&state.pool,
|
|
)
|
|
.await?
|
|
.ok_or_else(|| {
|
|
crate::ErrorKind::OtherError(format!(
|
|
"Tried to run a nonexistent instance {instance_id}!"
|
|
))
|
|
})?;
|
|
|
|
if offline_mode
|
|
&& context.instance.install_stage != InstanceInstallStage::Installed
|
|
{
|
|
return Err(crate::ErrorKind::LauncherError(
|
|
"Offline mode can only launch fully downloaded instances"
|
|
.to_string(),
|
|
)
|
|
.as_error());
|
|
}
|
|
|
|
let pre_launch_hooks = context
|
|
.launch_overrides
|
|
.hooks
|
|
.pre_launch
|
|
.as_ref()
|
|
.or(settings.hooks.pre_launch.as_ref())
|
|
.filter(|hook_command| !hook_command.is_empty());
|
|
if let Some(hook) = pre_launch_hooks {
|
|
let mut cmd = shlex::split(hook)
|
|
.ok_or_else(|| {
|
|
crate::ErrorKind::LauncherError(format!(
|
|
"Invalid pre-launch command: {hook}",
|
|
))
|
|
})?
|
|
.into_iter();
|
|
|
|
if let Some(command) = cmd.next() {
|
|
let full_path = crate::util::io::canonicalize(
|
|
state.directories.resolve_game_dir(
|
|
&context.instance.path,
|
|
context.instance.game_dir_override.as_deref(),
|
|
),
|
|
)?;
|
|
let mut command = Command::new(command);
|
|
command.args(cmd).current_dir(&full_path).kill_on_drop(true);
|
|
let result = command
|
|
.spawn()
|
|
.map_err(|e| IOError::with_path(e, &full_path))?
|
|
.wait()
|
|
.await
|
|
.map_err(IOError::from)?;
|
|
|
|
if !result.success() {
|
|
return Err(crate::ErrorKind::LauncherError(format!(
|
|
"Non-zero exit code for pre-launch hook: {}",
|
|
result.code().unwrap_or(-1)
|
|
))
|
|
.as_error());
|
|
}
|
|
}
|
|
}
|
|
|
|
let java_args = if let Some(extra_launch_args) = extra_launch_args {
|
|
extra_launch_args
|
|
} else {
|
|
context
|
|
.launch_overrides
|
|
.extra_launch_args
|
|
.clone()
|
|
.unwrap_or(settings.extra_launch_args)
|
|
};
|
|
let wrapper = context
|
|
.launch_overrides
|
|
.hooks
|
|
.wrapper
|
|
.clone()
|
|
.or(settings.hooks.wrapper)
|
|
.filter(|hook_command| !hook_command.is_empty());
|
|
let mut memory = context.launch_overrides.memory.unwrap_or(settings.memory);
|
|
let resolution = context
|
|
.launch_overrides
|
|
.game_resolution
|
|
.unwrap_or(settings.game_resolution);
|
|
let maximize_window = context
|
|
.launch_overrides
|
|
.maximize_window
|
|
.unwrap_or(settings.maximize_window);
|
|
let env_args = context
|
|
.launch_overrides
|
|
.custom_env_vars
|
|
.clone()
|
|
.unwrap_or(settings.custom_env_vars);
|
|
let post_exit_hook = context
|
|
.launch_overrides
|
|
.hooks
|
|
.post_exit
|
|
.clone()
|
|
.or(settings.hooks.post_exit)
|
|
.filter(|hook_command| !hook_command.is_empty());
|
|
|
|
let mut mc_set_options: Vec<(String, String)> = vec![];
|
|
if let Some(fullscreen) = context.launch_overrides.force_fullscreen {
|
|
mc_set_options.push(("fullscreen".to_string(), fullscreen.to_string()));
|
|
} else if settings.force_fullscreen {
|
|
mc_set_options.push(("fullscreen".to_string(), "true".to_string()));
|
|
}
|
|
|
|
if offline_mode {
|
|
crate::minecraft_skins::flush_pending_skin_change_for_profile(
|
|
credentials.offline_profile.id,
|
|
)
|
|
.await?;
|
|
} else {
|
|
crate::minecraft_skins::flush_pending_skin_change().await?;
|
|
}
|
|
if memory.optimize_before_launch
|
|
&& crate::api::memory::optimization_supported()
|
|
{
|
|
tracing::info!("Optimizing memory before launching Minecraft");
|
|
crate::api::memory::optimize().await?;
|
|
}
|
|
|
|
if memory.automatic {
|
|
let instance_path = state.directories.resolve_game_dir(
|
|
&context.instance.path,
|
|
context.instance.game_dir_override.as_deref(),
|
|
);
|
|
memory.maximum = crate::api::jre::automatic_memory_max_mb_for_instance(
|
|
&instance_path,
|
|
matches!(
|
|
context.applied_content_set.loader,
|
|
crate::state::ModLoader::Forge
|
|
| crate::state::ModLoader::Fabric
|
|
| crate::state::ModLoader::Quilt
|
|
| crate::state::ModLoader::NeoForge
|
|
| crate::state::ModLoader::Cleanroom
|
|
| crate::state::ModLoader::LiteLoader
|
|
| crate::state::ModLoader::LegacyFabric
|
|
| crate::state::ModLoader::Babric
|
|
),
|
|
);
|
|
tracing::info!(
|
|
"Automatically allocated {} MiB of memory",
|
|
memory.maximum
|
|
);
|
|
}
|
|
|
|
let mut gc_report: Option<GcLaunchReport> = None;
|
|
let process = crate::launcher::launch_minecraft(
|
|
&java_args,
|
|
&env_args,
|
|
&mc_set_options,
|
|
&wrapper,
|
|
&memory,
|
|
&resolution,
|
|
maximize_window,
|
|
credentials,
|
|
post_exit_hook,
|
|
&context,
|
|
gc_intent,
|
|
&mut gc_report,
|
|
quick_play_type,
|
|
offline_mode,
|
|
)
|
|
.await?;
|
|
Ok((process, gc_report))
|
|
}
|
|
|
|
pub async fn kill(instance_id: &str) -> crate::Result<()> {
|
|
let state = State::get().await?;
|
|
let processes =
|
|
crate::api::process::get_by_instance_id(instance_id).await?;
|
|
|
|
for process in processes {
|
|
state.process_manager.kill(process.uuid).await?;
|
|
}
|
|
|
|
Ok(())
|
|
}
|