This commit is contained in:
@@ -0,0 +1,26 @@
|
||||
[package]
|
||||
name = "runner"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
anyhow = "1.0.102"
|
||||
clap = { version = "4.6.0", features = ["derive"] }
|
||||
duct = "1.1.1"
|
||||
mutually_exclusive_features = "0.1.0"
|
||||
nix = { version = "0.31.2", features = ["sched", "mount"] }
|
||||
tracing = "0.1.44"
|
||||
tracing-subscriber = "0.3.23"
|
||||
walkdir = "2.5.0"
|
||||
xdgdir = "0.8.0"
|
||||
|
||||
[features]
|
||||
default = ["v3"]
|
||||
|
||||
v3 = []
|
||||
photo = []
|
||||
designer = []
|
||||
publisher = []
|
||||
@@ -0,0 +1,115 @@
|
||||
use std::{
|
||||
fs,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use anyhow::{Context, Result};
|
||||
use duct::cmd;
|
||||
use tracing::{info, instrument};
|
||||
|
||||
use crate::make_env;
|
||||
|
||||
pub static REGISTRY_PATCHES: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| PathBuf::from(env!("REGISTRY_PATCHES")));
|
||||
pub static ON_LINUX: LazyLock<PathBuf> = LazyLock::new(|| PathBuf::from(env!("ON_LINUX")));
|
||||
pub const RSYNC: &str = env!("RSYNC");
|
||||
|
||||
const LATEST_REVISION: u32 = 10;
|
||||
|
||||
#[instrument(skip_all, ret)]
|
||||
pub fn read_revision(wine_prefix: &Path) -> Result<Option<u32>> {
|
||||
let revision_file = wine_prefix.join(".revision");
|
||||
|
||||
if !revision_file.is_file() {
|
||||
return Ok(None);
|
||||
}
|
||||
|
||||
let revision: u32 = fs::read_to_string(revision_file)
|
||||
.context("reading revision file")?
|
||||
.split_whitespace()
|
||||
.collect::<String>()
|
||||
.parse()
|
||||
.context("parsing revision to u32")?;
|
||||
|
||||
return Ok(Some(revision));
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn sync_v2_settings(wine_prefix: &Path, user: &str) -> Result<()> {
|
||||
for app in ["Photo", "Designer", "Publisher"] {
|
||||
let app_settings_dst = wine_prefix.join(format!(
|
||||
"drive_c/users/{user}/AppData/Roaming/Affinity/{app}/2.0/"
|
||||
));
|
||||
let app_settings_src = ON_LINUX.join(format!("Auxiliary/Settings/{app}/2.0/"));
|
||||
|
||||
fs::create_dir_all(&app_settings_dst).context(format!(
|
||||
"creating settings directory for {app} w/ user {user}: {}",
|
||||
app_settings_dst.display()
|
||||
))?;
|
||||
|
||||
let rsync = cmd!(
|
||||
RSYNC,
|
||||
"-v",
|
||||
"--ignore-existing",
|
||||
"--chmod=D755,F644",
|
||||
"--recursive",
|
||||
&app_settings_src,
|
||||
&app_settings_dst
|
||||
)
|
||||
.stderr_to_stdout()
|
||||
.read()
|
||||
.context(format!(
|
||||
"syncing {app} settings with rsync: dst {}, src {}",
|
||||
app_settings_dst.display(),
|
||||
app_settings_src.display()
|
||||
))?;
|
||||
|
||||
for line in rsync.lines().filter(|x| !x.is_empty()) {
|
||||
info!(app = ?app, "{line}");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write_revision(wine_prefix: &Path) -> Result<()> {
|
||||
let revision_file = wine_prefix.join(".revision");
|
||||
|
||||
fs::write(&revision_file, LATEST_REVISION.to_string()).context("writing revision to file")?;
|
||||
|
||||
info!("wrote {LATEST_REVISION:?} to {revision_file:?}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn perform_migrations(wine_prefix: &Path) -> Result<()> {
|
||||
let revision = crate::check::read_revision(wine_prefix)?.unwrap_or(0);
|
||||
|
||||
if revision < 10 {
|
||||
let migration = make_env(
|
||||
&cmd!(
|
||||
crate::WINE,
|
||||
"regedit",
|
||||
"/S",
|
||||
REGISTRY_PATCHES.join("one.reg")
|
||||
),
|
||||
wine_prefix,
|
||||
true,
|
||||
)
|
||||
.stderr_to_stdout()
|
||||
.read()
|
||||
.context("applying one.reg")?;
|
||||
|
||||
for line in migration.lines() {
|
||||
info!("vkd3d migration: {line}");
|
||||
}
|
||||
|
||||
info!("finished vkd3d migration.");
|
||||
}
|
||||
|
||||
write_revision(wine_prefix).context("writing revision")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -0,0 +1,619 @@
|
||||
use std::{
|
||||
env,
|
||||
fs::{self},
|
||||
io::{self, BufRead, BufReader},
|
||||
os::unix::fs::PermissionsExt,
|
||||
path::{Path, PathBuf},
|
||||
sync::LazyLock,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use clap::{Parser, Subcommand};
|
||||
use duct::cmd;
|
||||
use mutually_exclusive_features::exactly_one_of;
|
||||
use nix::{
|
||||
libc::{self, S_IWUSR},
|
||||
mount::{MsFlags, mount, umount},
|
||||
sched::{CloneFlags, unshare},
|
||||
sys::wait::{WaitStatus, waitpid},
|
||||
unistd::{ForkResult, fork},
|
||||
};
|
||||
use tracing::{Level, error, info, instrument};
|
||||
use tracing_subscriber::FmtSubscriber;
|
||||
use walkdir::WalkDir;
|
||||
use xdgdir::BaseDir;
|
||||
|
||||
use crate::check::{perform_migrations, sync_v2_settings};
|
||||
|
||||
mod check;
|
||||
mod migrate;
|
||||
|
||||
exactly_one_of!("v3", "photo", "designer", "publisher");
|
||||
|
||||
const MOUNT_FAIL_STATUS: i32 = 111;
|
||||
|
||||
pub(crate) const WINE: &str = env!("WINE");
|
||||
pub(crate) const WINESERVER: &str = env!("WINESERVER");
|
||||
pub(crate) const WINETRICKS: &str = env!("WINETRICKS");
|
||||
pub(crate) const FUSE_OVERLAYFS: &str = env!("FUSE_OVERLAYFS");
|
||||
pub(crate) const GNUTAR: &str = env!("GNUTAR");
|
||||
pub(crate) const ZENITY: &str = env!("ZENITY");
|
||||
|
||||
pub(crate) static LOWER_DIR: LazyLock<PathBuf> =
|
||||
LazyLock::new(|| PathBuf::from(env::var("LOWER_DIR").expect("LOWER_DIR env var to exist")));
|
||||
|
||||
#[derive(Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
struct Arguments {
|
||||
/// Make Wine far more verbose.
|
||||
#[arg(long)]
|
||||
verbose: bool,
|
||||
|
||||
/// Specified tool to use. If none matches, defaults to the affinity application.
|
||||
#[command(subcommand)]
|
||||
subcommand: Option<Program>,
|
||||
|
||||
/// Arguments for affinity application
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
affinity: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug)]
|
||||
enum Program {
|
||||
/// Run wine within sandbox
|
||||
Wine {
|
||||
/// arguments to wine
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
arguments: Vec<String>,
|
||||
},
|
||||
/// Run winetricks within sandbox
|
||||
Winetricks {
|
||||
/// arguments to winetricks
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
arguments: Vec<String>,
|
||||
},
|
||||
/// Run wineboot within sandbox
|
||||
Wineboot {
|
||||
/// arguments to wineboot
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
arguments: Vec<String>,
|
||||
},
|
||||
/// Run wineserver within sandbox
|
||||
Wineserver {
|
||||
/// arguments to wineserver
|
||||
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||
arguments: Vec<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum ProgramToExecute {
|
||||
Affinity { arguments: Vec<String> },
|
||||
Other(Program),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct Paths {
|
||||
upper: PathBuf,
|
||||
work: PathBuf,
|
||||
wine_prefix: PathBuf,
|
||||
}
|
||||
|
||||
impl Paths {
|
||||
fn ensure_created(&self) -> anyhow::Result<()> {
|
||||
let work_dir = self.work.join("work");
|
||||
|
||||
if work_dir.is_dir() {
|
||||
let mut perms = fs::metadata(&work_dir)?.permissions();
|
||||
perms.set_mode(0o700);
|
||||
fs::set_permissions(&work_dir, perms).context("set workdir permissions")?;
|
||||
|
||||
fs::remove_dir_all(&work_dir).context("delete workdir")?;
|
||||
}
|
||||
|
||||
fs::create_dir_all(&self.upper).context("creating upper")?;
|
||||
fs::create_dir_all(&self.work).context("creating work")?;
|
||||
fs::create_dir_all(&self.wine_prefix).context("creating wine_prefix")?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn init_tracing() {
|
||||
let subscriber = FmtSubscriber::builder()
|
||||
.with_max_level(Level::TRACE)
|
||||
.finish();
|
||||
|
||||
tracing::subscriber::set_global_default(subscriber).expect("setting default subscriber failed");
|
||||
}
|
||||
|
||||
fn make_env(expression: &duct::Expression, wine_prefix: &Path, verbose: bool) -> duct::Expression {
|
||||
let expression = expression.env("WINEPREFIX", wine_prefix.display().to_string());
|
||||
|
||||
if verbose {
|
||||
return expression;
|
||||
}
|
||||
|
||||
expression.env("WINEDEBUG", "-all,fixme-all".to_string())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn warmup_prefix_directories(destination: &PathBuf) -> io::Result<()> {
|
||||
for entry in WalkDir::new(&*LOWER_DIR)
|
||||
.into_iter()
|
||||
.filter_map(std::result::Result::ok)
|
||||
{
|
||||
if !entry.file_type().is_dir() {
|
||||
continue;
|
||||
}
|
||||
|
||||
let relative_path = entry.path().strip_prefix(&*LOWER_DIR).unwrap();
|
||||
let target_path = destination.join(relative_path);
|
||||
|
||||
if let Err(err) = fs::create_dir_all(&target_path) {
|
||||
error!(target_path = ?target_path, err = ?err, "failed to create");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument]
|
||||
fn warmup_prefix_registry(destination: &PathBuf, user: &String) -> io::Result<()> {
|
||||
let important_files = vec!["system.reg", "user.reg", "userdef.reg", ".update-timestamp"];
|
||||
|
||||
for file in important_files {
|
||||
let src_path = LOWER_DIR.join(file);
|
||||
let dst_path = destination.join(file);
|
||||
|
||||
if !src_path.try_exists()? || dst_path.try_exists()? {
|
||||
continue;
|
||||
}
|
||||
|
||||
let content = fs::read_to_string(&src_path)?;
|
||||
let modified_content = content.replace("nixbld", user);
|
||||
|
||||
fs::write(&dst_path, modified_content)?;
|
||||
|
||||
let mut perms = fs::metadata(&dst_path)?.permissions();
|
||||
let current_mode = perms.mode();
|
||||
|
||||
// set write bit
|
||||
perms.set_mode(current_mode | S_IWUSR);
|
||||
fs::set_permissions(&dst_path, perms)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn wineboot_update(wine_prefix: &Path, verbose: bool) -> anyhow::Result<()> {
|
||||
let handle = make_env(
|
||||
&cmd!(WINE, "wineboot", "--update")
|
||||
.stderr_to_stdout()
|
||||
.unchecked(),
|
||||
wine_prefix,
|
||||
verbose,
|
||||
)
|
||||
.reader()?;
|
||||
|
||||
let lines = BufReader::new(&handle).lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
info!("{line}");
|
||||
}
|
||||
|
||||
match handle.try_wait()? {
|
||||
Some(output) => {
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"wineboot --update failed: {:?}",
|
||||
output.status
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("wineboot child is still running in some way.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn execute(paths: &Paths, program: &ProgramToExecute, verbose: bool) -> anyhow::Result<()> {
|
||||
let user = std::env::var("USER")?;
|
||||
|
||||
info!(user = ?user);
|
||||
|
||||
if let Err(e) = warmup_prefix_directories(&paths.upper) {
|
||||
error!(error = %e, "warmup prefix directories failed (non-fatal)");
|
||||
}
|
||||
if let Err(e) = warmup_prefix_registry(&paths.upper, &user) {
|
||||
error!(error = %e, "warmup prefix registry failed (non-fatal)");
|
||||
}
|
||||
wineboot_update(&paths.wine_prefix, verbose)?;
|
||||
|
||||
info!("finished warming & wineboot");
|
||||
|
||||
perform_migrations(&paths.wine_prefix).context("performing migrations")?;
|
||||
|
||||
if cfg!(not(feature = "v3")) {
|
||||
sync_v2_settings(&paths.wine_prefix, &user).context("sync v2 settings")?;
|
||||
}
|
||||
|
||||
info!("finished pre-check");
|
||||
|
||||
let application_handle = match program {
|
||||
ProgramToExecute::Affinity { arguments } => {
|
||||
let exe_suffix = if cfg!(feature = "v3") {
|
||||
"Affinity/AffinityHook.exe"
|
||||
} else if cfg!(feature = "photo") {
|
||||
"Photo 2/Photo.exe"
|
||||
} else if cfg!(feature = "publisher") {
|
||||
"Publisher 2/Publisher.exe"
|
||||
} else if cfg!(feature = "designer") {
|
||||
"Designer 2/Designer.exe"
|
||||
} else {
|
||||
unreachable!()
|
||||
};
|
||||
|
||||
let exe_path = format!(
|
||||
"{}/drive_c/Program Files/Affinity/{}",
|
||||
paths.wine_prefix.display(),
|
||||
exe_suffix
|
||||
);
|
||||
|
||||
info!("using exe {exe_path:?}");
|
||||
|
||||
cmd(
|
||||
WINE,
|
||||
std::iter::once(exe_path).chain(arguments.iter().cloned()),
|
||||
)
|
||||
}
|
||||
ProgramToExecute::Other(Program::Wine { arguments }) => cmd(WINE, arguments),
|
||||
ProgramToExecute::Other(Program::Winetricks { arguments }) => cmd(WINETRICKS, arguments),
|
||||
ProgramToExecute::Other(Program::Wineboot { arguments }) => cmd(
|
||||
WINE,
|
||||
std::iter::once("wineboot".to_string()).chain(arguments.iter().cloned()),
|
||||
),
|
||||
ProgramToExecute::Other(Program::Wineserver { arguments }) => cmd(WINESERVER, arguments),
|
||||
};
|
||||
|
||||
let application_handle = make_env(
|
||||
&application_handle.stderr_to_stdout().unchecked(),
|
||||
&paths.wine_prefix,
|
||||
verbose,
|
||||
)
|
||||
.reader()?;
|
||||
|
||||
let lines = BufReader::new(&application_handle).lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
println!("{line}");
|
||||
}
|
||||
|
||||
match application_handle.try_wait()? {
|
||||
Some(output) => {
|
||||
if output.status.success() {
|
||||
info!(status = ?output.status, "application process ended cleanly");
|
||||
} else {
|
||||
return Err(anyhow::anyhow!(
|
||||
"application process failed: {:?}",
|
||||
output.status
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("application process is still running in some way.");
|
||||
}
|
||||
}
|
||||
|
||||
let wineserver_wait = make_env(
|
||||
&cmd!(WINESERVER, "-w").stderr_to_stdout().unchecked(),
|
||||
&paths.wine_prefix,
|
||||
verbose,
|
||||
)
|
||||
.reader()?;
|
||||
|
||||
let lines = BufReader::new(&wineserver_wait).lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
info!("{line}");
|
||||
}
|
||||
|
||||
match wineserver_wait.try_wait()? {
|
||||
Some(output) => {
|
||||
if output.status.success() {
|
||||
info!(status = ?output.status, "wineserver -w exited cleanly");
|
||||
} else {
|
||||
return Err(anyhow::anyhow!("wineserver -w failed: {:?}", output.status));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("wineserver child is still running in some way.");
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all, ret)]
|
||||
fn make_mount_options(paths: &Paths) -> String {
|
||||
format!(
|
||||
"lowerdir={},upperdir={},workdir={}",
|
||||
LOWER_DIR.display(),
|
||||
paths.upper.display(),
|
||||
paths.work.display()
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip(paths))]
|
||||
fn cleanup_fuse(paths: &Paths) -> anyhow::Result<()> {
|
||||
let fusermount3 = cmd!(
|
||||
"/usr/bin/env",
|
||||
"fusermount3",
|
||||
"-u",
|
||||
"-z",
|
||||
paths.wine_prefix.display().to_string()
|
||||
)
|
||||
.stdout_to_stderr()
|
||||
.stderr_capture()
|
||||
.run();
|
||||
|
||||
let success = match &fusermount3 {
|
||||
Ok(output) => {
|
||||
if output.status.success() {
|
||||
true
|
||||
} else {
|
||||
error!(status = %output.status, stderr = ?output.stderr, "`fusermount3` failed");
|
||||
false
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(error = ?err, "`fusermount3` failed");
|
||||
false
|
||||
}
|
||||
};
|
||||
|
||||
if !success {
|
||||
let fusermount = cmd!(
|
||||
"/usr/bin/env",
|
||||
"fusermount",
|
||||
"-u",
|
||||
"-z",
|
||||
paths.wine_prefix.display().to_string()
|
||||
)
|
||||
.stdout_to_stderr()
|
||||
.stderr_capture()
|
||||
.run()?;
|
||||
|
||||
if !fusermount.status.success() {
|
||||
error!(status = %fusermount.status, stderr = ?String::from_utf8_lossy(&fusermount.stderr), "`fusermount2` failed");
|
||||
|
||||
return Err(anyhow::anyhow!(
|
||||
"both `fusermount3` & `fusermount` failed! fusermount2: {:?}",
|
||||
String::from_utf8_lossy(&fusermount.stderr)
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
info!("unmounted fuse cleanly");
|
||||
|
||||
let _ = fs::remove_dir(&paths.wine_prefix)
|
||||
.inspect_err(|err| error!(error = ?err, "failed to remove wine_prefix"));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn mount_run_unprivileged(
|
||||
paths: &Paths,
|
||||
program: &ProgramToExecute,
|
||||
verbose: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
let handle = cmd!(
|
||||
FUSE_OVERLAYFS,
|
||||
"-o",
|
||||
make_mount_options(paths),
|
||||
&paths.wine_prefix
|
||||
)
|
||||
.stderr_to_stdout()
|
||||
.unchecked()
|
||||
.reader()?;
|
||||
|
||||
let lines = BufReader::new(&handle).lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
info!("{line}");
|
||||
}
|
||||
|
||||
match handle.try_wait()? {
|
||||
Some(output) => {
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"fuse-overlayfs failed to mount: {:?}",
|
||||
output.status
|
||||
));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
error!("fuse-overlayfs child is still running in some way.");
|
||||
}
|
||||
}
|
||||
|
||||
if let Err(err) = execute(paths, program, verbose) {
|
||||
error!(error = ?err, "Running with fuse failed.");
|
||||
let _ = cleanup_fuse(paths);
|
||||
return Err(anyhow::anyhow!(
|
||||
"failed to run application with `fuse-overlayfs`: {err:?}"
|
||||
));
|
||||
}
|
||||
|
||||
cleanup_fuse(paths)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[instrument(skip(paths))]
|
||||
fn cleanup_privileged(paths: &Paths) {
|
||||
let _ = umount(&paths.wine_prefix)
|
||||
.inspect_err(|err| error!(error = ?err, "failed to unmount wine_prefix"));
|
||||
let _ = fs::remove_dir(&paths.wine_prefix)
|
||||
.inspect_err(|err| error!(error = ?err, "failed to remove wine_prefix"));
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn mount_execute_privileged(paths: &Paths, program: &ProgramToExecute, verbose: bool) {
|
||||
match mount(
|
||||
Some("overlay"),
|
||||
&paths.wine_prefix,
|
||||
Some("overlay"),
|
||||
MsFlags::empty(),
|
||||
Some(make_mount_options(paths).as_str()),
|
||||
) {
|
||||
Ok(()) => {
|
||||
if let Err(err) = execute(paths, program, verbose) {
|
||||
error!(error = ?err, "Running privileged failed.");
|
||||
cleanup_privileged(paths);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
error!(errorno = ?err, "Mount failed.");
|
||||
std::process::exit(MOUNT_FAIL_STATUS);
|
||||
}
|
||||
}
|
||||
|
||||
cleanup_privileged(paths);
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
fn mount_run_privileged(
|
||||
paths: &Paths,
|
||||
ids: (u32, u32),
|
||||
program: &ProgramToExecute,
|
||||
verbose: bool,
|
||||
) -> anyhow::Result<()> {
|
||||
if let Err(err) = fs::write("/proc/self/setgroups", b"deny\n") {
|
||||
error!(error = ?err, "failed to write setgroups");
|
||||
mount_run_unprivileged(paths, program, verbose)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let uid_map = format!("0 {} 1\n", ids.0);
|
||||
if let Err(err) = fs::write("/proc/self/uid_map", uid_map) {
|
||||
error!(error = ?err, "failed to write uid_map");
|
||||
mount_run_unprivileged(paths, program, verbose)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let gid_map = format!("0 {} 1\n", ids.1);
|
||||
if let Err(err) = fs::write("/proc/self/gid_map", gid_map) {
|
||||
error!(error = ?err, "failed to write gid_map");
|
||||
mount_run_unprivileged(paths, program, verbose)?;
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
match unsafe { fork() } {
|
||||
Ok(ForkResult::Parent { child }) => match waitpid(child, None) {
|
||||
Ok(WaitStatus::Exited(_, status)) if status == MOUNT_FAIL_STATUS => {
|
||||
info!("Falling back on unprivileged due to failed mount");
|
||||
mount_run_unprivileged(paths, program, verbose)?;
|
||||
}
|
||||
Ok(WaitStatus::Exited(_, status)) => {
|
||||
std::process::exit(status);
|
||||
}
|
||||
Ok(WaitStatus::Signaled(_, signal, _)) => {
|
||||
info!(signal= ?signal, "child was killed by signal");
|
||||
std::process::exit(1);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(errorno = ?err, "failed to wait for child");
|
||||
std::process::exit(1);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
Ok(ForkResult::Child) => {
|
||||
unsafe {
|
||||
// make sure this namespace dies if the parent ends
|
||||
nix::libc::prctl(nix::libc::PR_SET_PDEATHSIG, nix::libc::SIGKILL);
|
||||
}
|
||||
mount_execute_privileged(paths, program, verbose);
|
||||
std::process::exit(0);
|
||||
}
|
||||
Err(err) => {
|
||||
error!(errorno = ?err, "Fork failed.");
|
||||
info!("Falling back on unprivileged");
|
||||
mount_run_unprivileged(paths, program, verbose)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
init_tracing();
|
||||
|
||||
let args = Arguments::parse();
|
||||
|
||||
let base = BaseDir::new(if cfg!(feature = "v3") {
|
||||
"affinity-v3"
|
||||
} else {
|
||||
"affinity"
|
||||
})
|
||||
.expect("obtaining xdg basedir failed");
|
||||
|
||||
let paths = Paths {
|
||||
upper: base.data,
|
||||
work: base.state,
|
||||
wine_prefix: base
|
||||
.runtime
|
||||
.unwrap_or_else(std::env::temp_dir)
|
||||
.join(format!("affinity-nix-prefix-{}", std::process::id())),
|
||||
};
|
||||
|
||||
info!(paths = ?paths, lower = ?LOWER_DIR.display());
|
||||
|
||||
if !LOWER_DIR.is_dir() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"{} does not exist or is not a directory! If $LOWER_DIR does not exist, you are likely using the wrong package, refer to affinity-nix's readme for correct usage.",
|
||||
LOWER_DIR.display()
|
||||
));
|
||||
}
|
||||
|
||||
paths
|
||||
.ensure_created()
|
||||
.context("setting up tmp directories")?;
|
||||
migrate::migrate(&paths).context("migrating to new overlayfs runner")?;
|
||||
|
||||
let uid = unsafe { libc::getuid() };
|
||||
let gid = unsafe { libc::getgid() };
|
||||
|
||||
let unshare =
|
||||
unshare(CloneFlags::CLONE_NEWUSER | CloneFlags::CLONE_NEWNS | CloneFlags::CLONE_NEWPID);
|
||||
|
||||
let program = if let Some(subcommand) = args.subcommand {
|
||||
ProgramToExecute::Other(subcommand)
|
||||
} else {
|
||||
ProgramToExecute::Affinity {
|
||||
arguments: args.affinity,
|
||||
}
|
||||
};
|
||||
|
||||
info!(program = ?program);
|
||||
|
||||
match unshare {
|
||||
Ok(()) => mount_run_privileged(&paths, (uid, gid), &program, args.verbose),
|
||||
Err(err) => {
|
||||
error!(errorno = ?err, "Unshare failed.");
|
||||
mount_run_unprivileged(&paths, &program, args.verbose)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
use std::{
|
||||
env, fs,
|
||||
io::{BufRead, BufReader},
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use anyhow::Context;
|
||||
use duct::cmd;
|
||||
use tracing::{error, info, instrument};
|
||||
|
||||
use crate::{GNUTAR, Paths, ZENITY};
|
||||
|
||||
const MESSAGE: &str = "There have been upgrades to affinity-nix!
|
||||
|
||||
Affinity and it's dependencies are no longer installed directly, reducing startup time and manual updating.
|
||||
|
||||
To migrate, we need to delete the old installation. Your registry files and user data won't be touched.";
|
||||
|
||||
fn make_message(backup_location: &Path) -> String {
|
||||
format!(
|
||||
"{MESSAGE} A backup will be created in at {}. Is that OK?",
|
||||
backup_location.display()
|
||||
)
|
||||
}
|
||||
|
||||
#[instrument(skip_all)]
|
||||
pub fn migrate(paths: &Paths) -> anyhow::Result<()> {
|
||||
let Some(revision) = crate::check::read_revision(&paths.upper)? else {
|
||||
info!("revision does not exist, skipping backup");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
if revision >= 9 {
|
||||
info!("revision more than or equal to 9, skipping backup");
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let backup_path = Path::new(&env::var("HOME").context("finding $HOME var")?).join(format!(
|
||||
"affinity-nix-backup-{}.tar.zst",
|
||||
std::process::id()
|
||||
));
|
||||
|
||||
let question = cmd!(
|
||||
ZENITY,
|
||||
"--question",
|
||||
"--width",
|
||||
"480",
|
||||
format!("--text={}", make_message(&backup_path))
|
||||
)
|
||||
.unchecked()
|
||||
.run()?;
|
||||
|
||||
if !question.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"User refused to migrate, we cannot continue or there may be data loss."
|
||||
));
|
||||
}
|
||||
|
||||
let tar_handle = cmd!(GNUTAR, "--zstd", "-cvpf", backup_path, &paths.upper)
|
||||
.stdout_to_stderr()
|
||||
.stderr_capture()
|
||||
.reader()?;
|
||||
|
||||
let lines = BufReader::new(&tar_handle).lines();
|
||||
|
||||
for line in lines {
|
||||
let line = line?;
|
||||
info!("{line}");
|
||||
}
|
||||
|
||||
match tar_handle.try_wait()? {
|
||||
Some(output) => {
|
||||
if !output.status.success() {
|
||||
return Err(anyhow::anyhow!(
|
||||
"backing up with tar failed: {:?}",
|
||||
output.status
|
||||
));
|
||||
}
|
||||
|
||||
info!(status = %output.status, "tar ended cleanly");
|
||||
}
|
||||
None => {
|
||||
error!("tar child is still running in some way.");
|
||||
}
|
||||
}
|
||||
|
||||
let drive_c = paths.upper.join("drive_c");
|
||||
|
||||
if !drive_c.is_dir() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
for entry in drive_c.read_dir().context("reading drive_c")? {
|
||||
let entry = entry.context("reading drive_c entry")?;
|
||||
|
||||
if entry.file_name() == "users" {
|
||||
continue;
|
||||
}
|
||||
|
||||
let path = entry.path();
|
||||
|
||||
info!(path = ?path, "removing entry");
|
||||
|
||||
if entry
|
||||
.file_type()
|
||||
.context("reading entry filetype")?
|
||||
.is_dir()
|
||||
{
|
||||
fs::remove_dir_all(&path)
|
||||
.with_context(|| format!("removing directory {}", path.display()))?;
|
||||
} else {
|
||||
fs::remove_file(&path).with_context(|| format!("removing file {}", path.display()))?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Reference in New Issue
Block a user