|
|
|
@@ -15,6 +15,8 @@
|
|
|
|
|
//! half exists only as a CI secret, and is generated by the operator — a release
|
|
|
|
|
//! signing key that has passed through anyone else's hands is not a signing key.
|
|
|
|
|
|
|
|
|
|
use std::path::Path;
|
|
|
|
|
|
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
use tauri::State;
|
|
|
|
|
use tauri_plugin_updater::UpdaterExt;
|
|
|
|
@@ -26,6 +28,16 @@ const FEED_BASE: &str = "https://git.fabledsword.com/bvandeusen/thoughtsync/rele
|
|
|
|
|
|
|
|
|
|
const CHANNEL_PREF: &str = "update_channel";
|
|
|
|
|
|
|
|
|
|
/// The channel the app follows, as recorded by whatever installed it. Written into
|
|
|
|
|
/// the app-data directory by `desktop/packaging/install.sh`; both sides must agree on
|
|
|
|
|
/// this name.
|
|
|
|
|
const INSTALL_MARKER: &str = "install-channel";
|
|
|
|
|
|
|
|
|
|
/// The marker value we last acted on. Deliberately separate from `CHANNEL_PREF`: it
|
|
|
|
|
/// records what the INSTALLER said, so the two can be compared and a change told apart
|
|
|
|
|
/// from a repeat. See `adopt_installer_channel`.
|
|
|
|
|
const CHANNEL_SEED_PREF: &str = "update_channel_seed";
|
|
|
|
|
|
|
|
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
|
|
|
|
#[serde(rename_all = "snake_case")]
|
|
|
|
|
pub enum Channel {
|
|
|
|
@@ -50,6 +62,22 @@ impl Channel {
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Strict parse, for the installer's marker file only.
|
|
|
|
|
///
|
|
|
|
|
/// Unrecognized reads as "no opinion" here rather than as `stable` (which is what
|
|
|
|
|
/// `parse` does), because the two are asked different questions. `parse` reads a
|
|
|
|
|
/// pref that is definitely *this app's* setting, so it has to answer with a
|
|
|
|
|
/// channel. The marker is a file written by something else, and a truncated or
|
|
|
|
|
/// hand-edited one must not silently move someone OFF the channel they chose.
|
|
|
|
|
/// Ignoring it is the only reading that can't do harm in either direction.
|
|
|
|
|
fn from_marker(raw: &str) -> Option<Self> {
|
|
|
|
|
match raw.trim() {
|
|
|
|
|
"stable" => Some(Channel::Stable),
|
|
|
|
|
"dev" => Some(Channel::Dev),
|
|
|
|
|
_ => None,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn feed_url(self) -> String {
|
|
|
|
|
format!("{FEED_BASE}/{}/latest.json", self.as_str())
|
|
|
|
|
}
|
|
|
|
@@ -89,6 +117,62 @@ fn self_update_blocker() -> Option<String> {
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Adopt the channel the installer recorded, when it differs from the one we last
|
|
|
|
|
/// adopted. Called once at startup, before the frontend can ask what the channel is.
|
|
|
|
|
///
|
|
|
|
|
/// The installer knows which channel the user asked for; the app is what acts on it.
|
|
|
|
|
/// Until this existed, `install.sh --channel dev` left the pref at its `stable`
|
|
|
|
|
/// default, so a dev build checked the stable feed — which advertises an OLDER
|
|
|
|
|
/// version — and reported "up to date" forever (issue 2183). Two places held the same
|
|
|
|
|
/// decision and only one of them was set.
|
|
|
|
|
///
|
|
|
|
|
/// Comparing against the last-adopted value, rather than only seeding when the pref is
|
|
|
|
|
/// unset, is what makes both directions work: choosing a channel in the app sticks
|
|
|
|
|
/// across launches (the marker hasn't changed since we adopted it), while re-running
|
|
|
|
|
/// the installer on a DIFFERENT channel is honoured (it has). Seeding-when-unset would
|
|
|
|
|
/// have fixed only the very first install and left the second one silently wrong.
|
|
|
|
|
///
|
|
|
|
|
/// Every failure is logged and stepped over. No marker at all is the ordinary state of
|
|
|
|
|
/// a build installed some other way — from source, from a downloaded AppImage, or by a
|
|
|
|
|
/// Windows installer that has no channel concept — and none of that should keep the
|
|
|
|
|
/// app from opening.
|
|
|
|
|
pub fn adopt_installer_channel(db: &Db, data_dir: &Path) {
|
|
|
|
|
let path = data_dir.join(INSTALL_MARKER);
|
|
|
|
|
let Ok(raw) = std::fs::read_to_string(&path) else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
let Some(channel) = Channel::from_marker(&raw) else {
|
|
|
|
|
log::warn!(
|
|
|
|
|
"ignoring an unreadable install channel marker at {}",
|
|
|
|
|
path.display()
|
|
|
|
|
);
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
match adopt(db, channel) {
|
|
|
|
|
Ok(true) => log::info!(
|
|
|
|
|
"following the {} update channel, as recorded by the installer",
|
|
|
|
|
channel.as_str()
|
|
|
|
|
),
|
|
|
|
|
Ok(false) => {}
|
|
|
|
|
Err(e) => log::warn!("could not apply the installer's update channel: {e}"),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// Apply `channel` unless we already applied this same marker value. Returns whether
|
|
|
|
|
/// anything changed.
|
|
|
|
|
fn adopt(db: &Db, channel: Channel) -> Result<bool, String> {
|
|
|
|
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
let adopted = store::pref(&conn, CHANNEL_SEED_PREF).map_err(|e| e.to_string())?;
|
|
|
|
|
// Already acted on this marker — whatever the pref says now is the user's own
|
|
|
|
|
// choice, and re-applying would quietly undo it.
|
|
|
|
|
if adopted.as_deref() == Some(channel.as_str()) {
|
|
|
|
|
return Ok(false);
|
|
|
|
|
}
|
|
|
|
|
store::set_pref(&conn, CHANNEL_PREF, channel.as_str()).map_err(|e| e.to_string())?;
|
|
|
|
|
store::set_pref(&conn, CHANNEL_SEED_PREF, channel.as_str()).map_err(|e| e.to_string())?;
|
|
|
|
|
Ok(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn read_channel(db: &State<'_, Db>) -> Result<Channel, String> {
|
|
|
|
|
let conn = db.0.lock().map_err(|e| e.to_string())?;
|
|
|
|
|
let raw = store::pref(&conn, CHANNEL_PREF).map_err(|e| e.to_string())?;
|
|
|
|
@@ -224,6 +308,100 @@ fn describe_check_error(detail: &str) -> String {
|
|
|
|
|
#[cfg(test)]
|
|
|
|
|
mod tests {
|
|
|
|
|
use super::*;
|
|
|
|
|
use crate::local::schema;
|
|
|
|
|
use rusqlite::Connection;
|
|
|
|
|
use std::sync::Mutex;
|
|
|
|
|
|
|
|
|
|
fn db() -> Db {
|
|
|
|
|
let conn = Connection::open_in_memory().expect("in-memory db");
|
|
|
|
|
schema::migrate(&conn).expect("migrate");
|
|
|
|
|
Db(Mutex::new(conn))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// The channel `update_check` would actually use.
|
|
|
|
|
fn effective(db: &Db) -> Channel {
|
|
|
|
|
let conn = db.0.lock().expect("lock");
|
|
|
|
|
store::pref(&conn, CHANNEL_PREF)
|
|
|
|
|
.expect("read pref")
|
|
|
|
|
.as_deref()
|
|
|
|
|
.map(Channel::parse)
|
|
|
|
|
.unwrap_or(Channel::Stable)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn set_channel(db: &Db, channel: Channel) {
|
|
|
|
|
let conn = db.0.lock().expect("lock");
|
|
|
|
|
store::set_pref(&conn, CHANNEL_PREF, channel.as_str()).expect("set pref");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/// A scratch directory holding a marker file, named uniquely so tests can run in
|
|
|
|
|
/// parallel. Dropped by the OS' temp cleanup; nothing here is worth a new dependency.
|
|
|
|
|
fn data_dir_with_marker(contents: &str) -> std::path::PathBuf {
|
|
|
|
|
let dir = std::env::temp_dir().join(format!("ts-marker-{}", uuid::Uuid::new_v4()));
|
|
|
|
|
std::fs::create_dir_all(&dir).expect("scratch dir");
|
|
|
|
|
std::fs::write(dir.join(INSTALL_MARKER), contents).expect("write marker");
|
|
|
|
|
dir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_dev_install_follows_the_dev_channel() {
|
|
|
|
|
// The bug this whole mechanism exists for (2183): the installer knew, the app
|
|
|
|
|
// didn't, and a dev install checked the stable feed forever.
|
|
|
|
|
let db = db();
|
|
|
|
|
assert_eq!(
|
|
|
|
|
effective(&db),
|
|
|
|
|
Channel::Stable,
|
|
|
|
|
"default before any install"
|
|
|
|
|
);
|
|
|
|
|
adopt_installer_channel(&db, &data_dir_with_marker("dev\n"));
|
|
|
|
|
assert_eq!(effective(&db), Channel::Dev);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn choosing_a_channel_in_the_app_survives_the_next_launch() {
|
|
|
|
|
// Adoption runs on EVERY startup, so the marker must not keep re-asserting
|
|
|
|
|
// itself over a choice the user made afterwards.
|
|
|
|
|
let db = db();
|
|
|
|
|
let dir = data_dir_with_marker("dev");
|
|
|
|
|
adopt_installer_channel(&db, &dir);
|
|
|
|
|
set_channel(&db, Channel::Stable);
|
|
|
|
|
adopt_installer_channel(&db, &dir);
|
|
|
|
|
assert_eq!(effective(&db), Channel::Stable);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn reinstalling_on_a_different_channel_moves_the_app() {
|
|
|
|
|
// The half a seed-only-when-unset fix would have missed: install stable, then
|
|
|
|
|
// install dev. The pref is already set, but the user just asked for dev.
|
|
|
|
|
let db = db();
|
|
|
|
|
adopt_installer_channel(&db, &data_dir_with_marker("stable"));
|
|
|
|
|
assert_eq!(effective(&db), Channel::Stable);
|
|
|
|
|
adopt_installer_channel(&db, &data_dir_with_marker("dev"));
|
|
|
|
|
assert_eq!(effective(&db), Channel::Dev);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn a_damaged_marker_changes_nothing() {
|
|
|
|
|
// Ignored, NOT read as stable — a corrupt file must not move someone off the
|
|
|
|
|
// channel they're on.
|
|
|
|
|
let db = db();
|
|
|
|
|
set_channel(&db, Channel::Dev);
|
|
|
|
|
adopt_installer_channel(&db, &data_dir_with_marker("de"));
|
|
|
|
|
assert_eq!(effective(&db), Channel::Dev);
|
|
|
|
|
assert_eq!(Channel::from_marker("nightly"), None);
|
|
|
|
|
assert_eq!(Channel::from_marker(""), None);
|
|
|
|
|
assert_eq!(Channel::from_marker(" dev\n"), Some(Channel::Dev));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn no_marker_at_all_is_not_an_error() {
|
|
|
|
|
// Built from source, or installed by anything that isn't install.sh.
|
|
|
|
|
let db = db();
|
|
|
|
|
set_channel(&db, Channel::Dev);
|
|
|
|
|
let empty = std::env::temp_dir().join(format!("ts-nomarker-{}", uuid::Uuid::new_v4()));
|
|
|
|
|
std::fs::create_dir_all(&empty).expect("scratch dir");
|
|
|
|
|
adopt_installer_channel(&db, &empty);
|
|
|
|
|
assert_eq!(effective(&db), Channel::Dev);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
#[test]
|
|
|
|
|
fn each_channel_has_its_own_fixed_feed() {
|
|
|
|
|