#[cfg(windows)]
mod existing;
pub(super) mod version;
use crate::cli::args::{Cli, Command, NapcatCommand};
use crate::core::contracts::{instance::Started as StartedData, scalar::LocalId};
use crate::core::{
domain::Scope,
error::{AppError, Result},
output::Envelope,
};
use crate::messaging::session::{Session, remember_identity, status_data};
use crate::napcat::runtime::start_engine::Started;
use crate::storage::{
config::Settings,
credentials::{Credentials, SystemCredentials},
references::References,
state::{Registry, Store, select},
};
use crate::transport::client::Backend;
use std::time::Instant;
pub(super) fn install(cli: &Cli, store: &Store, deadline: Instant) -> Result<Option<Envelope>> {
if let Command::Napcat {
command: NapcatCommand::Install { version, from },
} = &cli.command
{
let release = crate::napcat::install::catalog::lookup(version)?;
crate::core::deadline::check(deadline)?;
drop(store.transaction(deadline)?);
let credentials = SystemCredentials::new(store.root())?;
let references = References::open(store, &credentials, deadline)?;
return crate::napcat::install::acquire::install(
&release,
from.as_deref(),
store,
&references,
deadline,
)
.map(Some);
}
Ok(None)
}
pub(super) fn start(
cli: &Cli,
settings: &Settings,
store: &Store,
registered: &Registry,
deadline: Instant,
) -> Result<Option<Envelope>> {
if let Command::Napcat {
command: NapcatCommand::Start(args),
} = &cli.command
{
#[cfg(windows)]
if !args.new && !registered.instances.is_empty() {
let selected = select(registered, cli.instance.as_deref())?;
existing::guard_start(store, &selected, deadline)?;
}
let automatic_preparation = cli.instance.is_none()
&& (registered.instances.is_empty()
|| matches!(registered.instances.as_slice(), [instance] if matches!(instance.state.as_str(), "preparing" | "prepared")));
if args.new || automatic_preparation {
let version = if !args.new
&& let [instance] = registered.instances.as_slice()
{
let installation = registered
.installations
.iter()
.find(|item| item.id == instance.installation_id)
.ok_or_else(AppError::storage)?;
ensure_version(args.version.as_deref(), installation)?;
installation.version.as_str()
} else {
args.version.as_deref().unwrap_or("v4.18.38")
};
let release = crate::napcat::install::catalog::lookup(version)?;
let mut witnesses = crate::napcat::compatibility::available_witnesses(store)?;
if args.verify_compatibility {
let inputs = crate::napcat::runtime::startup::inspect_trial(
store,
registered,
&release,
&witnesses,
&super::diagnostics::NativeProbe,
deadline,
)?;
witnesses.push(crate::napcat::compatibility::Witness {
key: inputs.compatibility().clone(),
verdict: crate::napcat::compatibility::Verdict::Unknown,
kind: crate::napcat::compatibility::EvidenceKind::AuthorizedStartupTrial,
reference: "EXPLICIT_STARTUP_TRIAL".into(),
});
}
let _inputs = crate::napcat::runtime::startup::inspect(
store,
registered,
&release,
&witnesses,
&super::diagnostics::NativeProbe,
deadline,
)?;
#[cfg(windows)]
{
let credentials = SystemCredentials::new(store.root())?;
let references = References::open(store, &credentials, deadline)?;
let environment = std::env::vars_os().collect();
let context = crate::napcat::runtime::start_engine::Context {
store,
references: &references,
credentials: &credentials,
release: &release,
witnesses: &witnesses,
probe: &super::diagnostics::NativeProbe,
environment: &environment,
};
let started = if args.new {
let key = args.request_id.as_deref().ok_or_else(AppError::invalid)?;
crate::napcat::runtime::start_engine::explicit(&context, key, deadline)?
} else {
crate::napcat::runtime::start_engine::automatic(&context, deadline)?
};
return response(store, &credentials, &settings.config, started, deadline)
.map(Some);
}
#[cfg(not(windows))]
return Err(AppError::new(
"STARTUP_NOT_IMPLEMENTED",
"New instance startup has not completed its lifecycle verification.",
4,
));
}
let instance = select(registered, cli.instance.as_deref())?;
let installation = registered
.installations
.iter()
.find(|item| item.id == instance.installation_id)
.ok_or_else(|| {
AppError::new(
"INSTALLATION_NOT_FOUND",
"Instance installation is not registered.",
4,
)
})?;
ensure_version(args.version.as_deref(), installation)?;
#[cfg(windows)]
if matches!(
instance.state.as_str(),
"stopped" | "preparing" | "prepared"
) {
return existing::start(store, settings, &instance, installation, deadline).map(Some);
}
#[cfg(windows)]
if instance.state == "login_required" {
let credentials = SystemCredentials::new(store.root())?;
return response(
store,
&credentials,
&settings.config,
crate::napcat::runtime::start_engine::Started {
instance,
reused: true,
request_id: None,
},
deadline,
)
.map(Some);
}
}
Ok(None)
}
pub(super) fn restart(
cli: &Cli,
settings: &Settings,
store: &Store,
registered: &Registry,
deadline: Instant,
) -> Result<Option<Envelope>> {
if !matches!(
cli.command,
Command::Napcat {
command: NapcatCommand::Restart
}
) {
return Ok(None);
}
let instance = select(registered, cli.instance.as_deref())?;
let installation = registered
.installations
.iter()
.find(|item| item.id == instance.installation_id)
.ok_or_else(AppError::storage)?;
#[cfg(windows)]
return existing::restart(store, settings, &instance, installation, deadline).map(Some);
#[cfg(not(windows))]
{
let _ = (settings, store, instance, installation, deadline);
Err(AppError::new(
"PLATFORM_UNSUPPORTED",
"Owned instance restart requires Windows.",
4,
))
}
}
fn ensure_version(
requested: Option<&str>,
installation: &crate::storage::state::Installation,
) -> Result<()> {
if requested.is_some_and(|version| {
version.strip_prefix('v').unwrap_or(version)
!= installation
.version
.strip_prefix('v')
.unwrap_or(&installation.version)
}) {
return Err(AppError::new(
"INSTANCE_VERSION_CONFLICT",
"Requested version differs from the selected instance; explicitly update its installation first.",
4,
));
}
Ok(())
}
pub fn response(
store: &Store,
credentials: &impl Credentials,
config: &crate::storage::config::Config,
started: Started,
deadline: Instant,
) -> Result<crate::core::output::Envelope> {
if started.instance.state == "login_required" {
let mut error = AppError::new(
"AUTH_REQUIRED",
"The instance is waiting for login in its QQ window; inspect status after authentication.",
4,
).with_details(crate::core::contracts::failure::Details::Login {instance_id:started.instance.id.clone(),state:crate::core::contracts::failure::LoginState::LoginRequired,account:(),reused:started.reused});
error.request_id = started.request_id;
error.next_action = Some(Box::new(
crate::core::contracts::action::NextAction::Status {
instance: (started.instance.id).to_owned(),
},
));
return Err(error);
}
let registry = store.read()?;
let current = crate::storage::state::select(®istry, Some(&started.instance.id))?;
if current != started.instance {
return Err(started.stale());
}
let installation = registry
.installations
.iter()
.find(|i| i.id == current.installation_id)
.ok_or_else(AppError::storage)?;
let transport =
crate::app::backend::Context::load(store, ®istry, credentials, ¤t, deadline)?;
let token = credentials.get(¤t.credential_key)?;
let backend = transport.client(¤t, &token, config)?;
let session = Session {
backend: &backend,
config,
instance: ¤t,
expected_version: &installation.version,
};
reuse(&session, store, started.reused, deadline)
}
pub(super) fn reuse<B: Backend>(
session: &Session<'_, B>,
store: &Store,
reused: bool,
deadline: Instant,
) -> Result<Envelope> {
let instance = session.instance;
let info = session.verify(deadline)?;
remember_identity(store, instance, &info.account, deadline)?;
let data = StartedData {
instance_id: LocalId::new(&instance.id)?,
status: status_data(&info),
reused,
};
Ok(Envelope::typed(data).scoped(Scope {
instance_id: instance.id.clone(),
account: info.account,
}))
}