use anyhow::{anyhow, bail, Context, Result};
use chrono::{DateTime, Utc};
use cluFlock::{ExclusiveFlock, FlockLock, SharedFlock};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::fs::{File, OpenOptions};
use std::io::{BufReader, ErrorKind, Seek, SeekFrom, Write};
#[cfg(target_os = "windows")]
use std::mem;
use tempfile::NamedTempFile;
use crate::global_paths::GlobalPaths;
fn is_default<T: Default + PartialEq>(t: &T) -> bool {
t == &T::default()
}
fn default_versionsdb_update_interval() -> i64 {
1440
}
fn is_default_versionsdb_update_interval(i: &i64) -> bool {
*i == default_versionsdb_update_interval()
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JuliaupConfigVersion {
#[serde(rename = "Path")]
pub path: String,
#[serde(rename = "BinaryPath", skip_serializing_if = "Option::is_none")]
pub binary_path: Option<String>,
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
#[serde(untagged)]
pub enum JuliaupConfigChannel {
DirectDownloadChannel {
#[serde(rename = "Path")]
path: String,
#[serde(rename = "Url")]
url: String,
#[serde(rename = "LocalETag")]
local_etag: String,
#[serde(rename = "ServerETag")]
server_etag: String,
#[serde(rename = "Version")]
version: String,
#[serde(rename = "BinaryPath", skip_serializing_if = "Option::is_none")]
binary_path: Option<String>,
},
SystemChannel {
#[serde(rename = "Version")]
version: String,
},
LinkedChannel {
#[serde(rename = "Command")]
command: String,
#[serde(rename = "Args")]
args: Option<Vec<String>>,
},
AliasChannel {
#[serde(rename = "Target")]
target: String,
#[serde(rename = "Args")]
args: Option<Vec<String>>,
},
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JuliaupConfigSettings {
#[serde(
rename = "CreateChannelSymlinks",
default,
skip_serializing_if = "is_default"
)]
pub create_channel_symlinks: bool,
#[serde(
rename = "VersionsDbUpdateInterval",
default = "default_versionsdb_update_interval",
skip_serializing_if = "is_default_versionsdb_update_interval"
)]
pub versionsdb_update_interval: i64,
#[serde(
rename = "AutoInstallChannels",
default,
skip_serializing_if = "Option::is_none"
)]
pub auto_install_channels: Option<bool>,
#[serde(
rename = "ManifestVersionDetect",
default,
skip_serializing_if = "is_default"
)]
pub manifest_version_detect: bool,
}
impl Default for JuliaupConfigSettings {
fn default() -> Self {
JuliaupConfigSettings {
create_channel_symlinks: false,
versionsdb_update_interval: default_versionsdb_update_interval(),
auto_install_channels: None,
manifest_version_detect: false,
}
}
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JuliaupOverride {
#[serde(rename = "Path")]
pub path: String,
#[serde(rename = "Channel")]
pub channel: String,
}
#[derive(Serialize, Deserialize, Clone, PartialEq)]
pub struct JuliaupConfig {
#[serde(rename = "Default")]
pub default: Option<String>,
#[serde(rename = "InstalledVersions")]
pub installed_versions: HashMap<String, JuliaupConfigVersion>,
#[serde(rename = "InstalledChannels")]
pub installed_channels: HashMap<String, JuliaupConfigChannel>,
#[serde(rename = "Settings", default)]
pub settings: JuliaupConfigSettings,
#[serde(rename = "Overrides", default)]
pub overrides: Vec<JuliaupOverride>,
#[serde(
rename = "LastVersionDbUpdate",
skip_serializing_if = "Option::is_none"
)]
pub last_version_db_update: Option<DateTime<Utc>>,
}
#[cfg(feature = "selfupdate")]
#[derive(Serialize, Deserialize, Clone)]
pub struct JuliaupSelfConfig {
#[serde(
rename = "BackgroundSelfUpdateInterval",
skip_serializing_if = "Option::is_none"
)]
pub background_selfupdate_interval: Option<i64>,
#[serde(
rename = "StartupSelfUpdateInterval",
skip_serializing_if = "Option::is_none"
)]
pub startup_selfupdate_interval: Option<i64>,
#[serde(rename = "ModifyPath", default, skip_serializing_if = "is_default")]
pub modify_path: bool,
#[serde(rename = "JuliaupChannel", skip_serializing_if = "Option::is_none")]
pub juliaup_channel: Option<String>,
#[serde(rename = "LastSelfUpdate", skip_serializing_if = "Option::is_none")]
pub last_selfupdate: Option<DateTime<Utc>>,
}
pub struct JuliaupConfigFile {
pub file: File,
pub lock: FlockLock<File>,
pub data: JuliaupConfig,
#[cfg(feature = "selfupdate")]
pub self_file: File,
#[cfg(feature = "selfupdate")]
pub self_data: JuliaupSelfConfig,
}
pub struct JuliaupReadonlyConfigFile {
pub data: JuliaupConfig,
#[cfg(feature = "selfupdate")]
pub self_data: JuliaupSelfConfig,
}
pub fn get_read_lock(paths: &GlobalPaths) -> Result<FlockLock<File>> {
std::fs::create_dir_all(&paths.juliauphome).with_context(|| {
format!(
"Could not create juliaup home folder `{}`.",
paths.juliauphome.display()
)
})?;
let lock_file = match OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&paths.lockfile)
{
Ok(file) => file,
Err(e) => {
return Err(anyhow!(
"Could not create lockfile `{}`: {}.",
paths.lockfile.display(),
e
));
}
};
let file_lock = match SharedFlock::try_lock(lock_file) {
Ok(lock) => lock,
Err(e) => {
eprintln!(
"Juliaup configuration is locked by another process, waiting for it to unlock."
);
SharedFlock::wait_lock(e.into()).unwrap()
}
};
Ok(file_lock)
}
pub fn load_config_db(
paths: &GlobalPaths,
existing_lock: Option<&FlockLock<File>>,
) -> Result<JuliaupReadonlyConfigFile> {
let mut file_lock: Option<FlockLock<File>> = None;
if existing_lock.is_none() {
file_lock = Some(get_read_lock(paths)?);
}
let v = match std::fs::OpenOptions::new()
.read(true)
.open(&paths.juliaupconfig)
{
Ok(file) => {
let reader = BufReader::new(&file);
serde_json::from_reader(reader).with_context(|| {
format!(
"Failed to parse configuration file '{:?}' for reading.",
paths.juliaupconfig
)
})?
}
Err(error) => match error.kind() {
ErrorKind::NotFound => JuliaupConfig {
default: None,
installed_versions: HashMap::new(),
installed_channels: HashMap::new(),
overrides: Vec::new(),
settings: JuliaupConfigSettings::default(),
last_version_db_update: None,
},
other_error => {
bail!(
"Problem opening the file {:?}: {:?}",
paths.juliaupconfig,
other_error
)
}
},
};
#[cfg(feature = "selfupdate")]
let selfconfig: JuliaupSelfConfig;
#[cfg(feature = "selfupdate")]
{
selfconfig = match std::fs::OpenOptions::new()
.read(true)
.open(&paths.juliaupselfconfig)
{
Ok(file) => {
let reader = BufReader::new(&file);
serde_json::from_reader(reader).with_context(|| {
format!(
"Failed to parse self configuration file '{:?}' for reading.",
paths.juliaupselfconfig
)
})?
}
Err(error) => bail!(
"Could not open self configuration file {:?}: {:?}",
paths.juliaupselfconfig,
error
),
};
}
if let Some(file_lock) = file_lock {
file_lock.unlock().with_context(|| {
format!(
"Failed to unlock configuration lock file `{}`.",
paths.lockfile.display()
)
})?;
}
Ok(JuliaupReadonlyConfigFile {
data: v,
#[cfg(feature = "selfupdate")]
self_data: selfconfig,
})
}
pub fn load_mut_config_db(paths: &GlobalPaths) -> Result<JuliaupConfigFile> {
std::fs::create_dir_all(&paths.juliauphome).with_context(|| {
format!(
"Could not create juliaup home folder `{}`.",
paths.juliauphome.display()
)
})?;
let lock_file = match OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&paths.lockfile)
{
Ok(file) => file,
Err(e) => {
return Err(anyhow!(
"Could not create lockfile `{}`: {}.",
paths.lockfile.display(),
e
));
}
};
let file_lock = match ExclusiveFlock::try_lock(lock_file) {
Ok(lock) => lock,
Err(e) => {
eprintln!(
"Juliaup configuration is locked by another process, waiting for it to unlock."
);
ExclusiveFlock::wait_lock(e.into()).unwrap()
}
};
let mut file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&paths.juliaupconfig)
.with_context(|| {
format!(
"Failed to open juliaup config file `{}`.",
paths.juliaupconfig.display()
)
})?;
let stream_len = file.seek(SeekFrom::End(0)).with_context(|| {
format!(
"Failed to determine the length of configuration file `{}`.",
paths.juliaupconfig.display()
)
})?;
let data = match stream_len {
0 => {
let new_config = JuliaupConfig {
default: None,
installed_versions: HashMap::new(),
installed_channels: HashMap::new(),
overrides: Vec::new(),
settings: JuliaupConfigSettings::default(),
last_version_db_update: None,
};
serde_json::to_writer_pretty(&file, &new_config).with_context(|| {
format!(
"Failed to write configuration file `{}`.",
paths.juliaupconfig.display()
)
})?;
file.sync_all().with_context(|| {
format!(
"Failed to sync configuration file `{}` to disk.",
paths.juliaupconfig.display()
)
})?;
file.rewind().with_context(|| {
format!(
"Failed to rewind config file `{}` after initial write.",
paths.juliaupconfig.display()
)
})?;
new_config
}
_ => {
file.rewind().with_context(|| {
format!(
"Failed to rewind existing config file `{}`.",
paths.juliaupconfig.display()
)
})?;
let reader = BufReader::new(&file);
serde_json::from_reader(reader).with_context(|| {
format!(
"Failed to parse configuration file `{}`.",
paths.juliaupconfig.display()
)
})?
}
};
#[cfg(feature = "selfupdate")]
let self_file: File;
#[cfg(feature = "selfupdate")]
let self_data: JuliaupSelfConfig;
#[cfg(feature = "selfupdate")]
{
self_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&paths.juliaupselfconfig)
.with_context(|| {
format!(
"Failed to open juliaup self config file `{}`.",
paths.juliaupselfconfig.display()
)
})?;
let reader = BufReader::new(&self_file);
self_data = serde_json::from_reader(reader).with_context(|| {
format!(
"Failed to parse self configuration file '{:?}' for reading.",
paths.juliaupselfconfig
)
})?
}
let result = JuliaupConfigFile {
file,
lock: file_lock,
data,
#[cfg(feature = "selfupdate")]
self_file,
#[cfg(feature = "selfupdate")]
self_data,
};
Ok(result)
}
pub fn save_config_db(
juliaup_config_file: &mut JuliaupConfigFile,
paths: &crate::global_paths::GlobalPaths,
) -> Result<()> {
let config_dir = paths.juliaupconfig.parent().ok_or_else(|| {
anyhow!(
"Config file path `{}` has no parent directory.",
paths.juliaupconfig.display()
)
})?;
let mut temp_file = NamedTempFile::new_in(config_dir).with_context(|| {
format!(
"Failed to create temporary config file in directory `{}`.",
config_dir.display()
)
})?;
serde_json::to_writer_pretty(&mut temp_file, &juliaup_config_file.data).with_context(|| {
format!(
"Failed to write configuration data for `{}` to temporary file.",
paths.juliaupconfig.display()
)
})?;
temp_file.flush().with_context(|| {
format!(
"Failed to flush temporary configuration data for `{}`.",
paths.juliaupconfig.display()
)
})?;
temp_file.as_file().sync_all().with_context(|| {
format!(
"Failed to sync temporary configuration data for `{}` to disk.",
paths.juliaupconfig.display()
)
})?;
#[cfg(target_os = "windows")]
{
let dummy_path = config_dir.join(".dummy");
let dummy = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&dummy_path)
.with_context(|| {
format!(
"Failed to create dummy file handle `{}`.",
dummy_path.display()
)
})?;
let _ = mem::replace(&mut juliaup_config_file.file, dummy);
}
temp_file.persist(&paths.juliaupconfig).with_context(|| {
format!(
"Failed to persist configuration file `{}`.",
paths.juliaupconfig.display()
)
})?;
#[cfg(target_os = "windows")]
{
juliaup_config_file.file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&paths.juliaupconfig)
.with_context(|| {
format!(
"Failed to reopen configuration file `{}` after save.",
paths.juliaupconfig.display()
)
})?;
}
#[cfg(feature = "selfupdate")]
{
let self_config_dir = paths.juliaupselfconfig.parent().ok_or_else(|| {
anyhow!(
"Self config file path `{}` has no parent directory.",
paths.juliaupselfconfig.display()
)
})?;
let mut temp_self_file = NamedTempFile::new_in(self_config_dir).with_context(|| {
format!(
"Failed to create temporary self config file in directory `{}`.",
self_config_dir.display()
)
})?;
serde_json::to_writer_pretty(&mut temp_self_file, &juliaup_config_file.self_data)
.with_context(|| {
format!(
"Failed to write self configuration data for `{}` to temporary file.",
paths.juliaupselfconfig.display()
)
})?;
temp_self_file.flush().with_context(|| {
format!(
"Failed to flush temporary self configuration data for `{}`.",
paths.juliaupselfconfig.display()
)
})?;
temp_self_file.as_file().sync_all().with_context(|| {
format!(
"Failed to sync temporary self configuration data for `{}` to disk.",
paths.juliaupselfconfig.display()
)
})?;
#[cfg(target_os = "windows")]
{
let dummy_self_path = self_config_dir.join(".dummy_self");
let dummy = OpenOptions::new()
.read(true)
.write(true)
.create(true)
.truncate(false)
.open(&dummy_self_path)
.with_context(|| {
format!(
"Failed to create dummy self config file handle `{}`.",
dummy_self_path.display()
)
})?;
let _ = mem::replace(&mut juliaup_config_file.self_file, dummy);
}
temp_self_file
.persist(&paths.juliaupselfconfig)
.with_context(|| {
format!(
"Failed to persist self configuration file `{}`.",
paths.juliaupselfconfig.display()
)
})?;
#[cfg(target_os = "windows")]
{
juliaup_config_file.self_file = std::fs::OpenOptions::new()
.read(true)
.write(true)
.open(&paths.juliaupselfconfig)
.with_context(|| {
format!(
"Failed to reopen self configuration file `{}` after save.",
paths.juliaupselfconfig.display()
)
})?;
}
}
Ok(())
}