1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
//! This file controls the automatic upgrade procedure in lal for musl builds.
//!
//! It will, if a new version is available in the `Backend`, download it
//! and overwrite the running executable using a file renaming dance.
//!
//! Be very careful about updating these functions without also testing the musl
//! build on a variety of Cargo.toml.version strings.
//!
//! People should not have to be told to `curl lal.tar | tar xz -C prefix` again.

use semver::Version;
use std::env;
use std::path::{Path, PathBuf};
use std::fs;
use std::process::Command;

use super::{LalResult, CliError};
use super::{http_download_to_path, get_latest_lal_version, LatestLal};

struct ExeInfo {
    /// Whether ldd things its a dynamic executable
    dynamic: bool,
    /// Whether this is a debug build (only for dynamic executables)
    debug: bool,
    /// Path to current_exe
    path: String,
    /// Best guess at install prefix based on path (only for static executables)
    prefix: Option<PathBuf>,
    /// Parsed semver version
    version: Version,
}

fn identify_exe() -> LalResult<ExeInfo> {
    let pth = env::current_exe()?;
    trace!("lal at {}", pth.display());
    let ldd_output = Command::new("ldd").arg(&pth).output()?;
    let ldd_str = String::from_utf8_lossy(&ldd_output.stdout);
    let is_dynamic = !ldd_str.contains("not a dynamic executable");
    let pthstr: String = pth.to_str().unwrap().into();
    let prefix = if pthstr.contains("/bin/") {
        let v: Vec<&str> = pthstr.split("/bin/").collect();
        if v.len() == 2 { Some(Path::new(v[0]).to_owned()) } else { None }
    } else {
        None
    };
    Ok(ExeInfo {
           dynamic: is_dynamic,
           debug: pthstr.contains("debug"), // cheap check for compiled versions
           path: pthstr,
           prefix: prefix,
           version: Version::parse(env!("CARGO_PKG_VERSION")).unwrap(),
       })
}

// basic tarball extractor
// smaller than the INPUT extractor uses because it doesn't clear out anything
fn extract_tarball(input: PathBuf, output: &PathBuf) -> LalResult<()> {
    use tar::Archive;
    use flate2::read::GzDecoder;

    let data = fs::File::open(input)?;
    let decompressed = GzDecoder::new(data)?; // decoder reads data
    let mut archive = Archive::new(decompressed); // Archive reads decoded

    archive.unpack(&output)?;
    Ok(())
}

fn verify_permissions(exe: &ExeInfo) -> LalResult<()> {
    // this is sufficient unless the user copied it over manually with sudo
    // and then chowned it, but for all normal installs, touching the main file
    // would sufficiently check that we have write permissions
    let s = Command::new("touch").arg(&exe.path).status()?;
    if !s.success() {
        return Err(CliError::SubprocessFailure(s.code().unwrap_or(1001)));
    }
    Ok(())
}

fn overwrite_exe(latest: &LatestLal, exe: &ExeInfo) -> LalResult<()> {
    let prefix = exe.prefix.clone().unwrap();
    extract_tarball(prefix.join("lal.tar"), &prefix)?;
    validate_exe(latest, exe)?;
    Ok(())
}

fn validate_exe(latest: &LatestLal, exe: &ExeInfo) -> LalResult<()> {
    let lal_output = Command::new(&exe.path).arg("-V").output()?;
    let lal_str = String::from_utf8_lossy(&lal_output.stdout);
    debug!("Output from lal -V: {}", lal_str.trim());
    debug!("Expecting to find: {}", latest.version.to_string());
    if !lal_str.contains(&latest.version.to_string()) {
        let estr = format!("lal -V yielded {}", lal_str.trim());
        return Err(CliError::UpgradeValidationFailure(estr));
    }
    debug!("New version validated");
    Ok(())
}

fn upgrade_exe(latest: &LatestLal, exe: &ExeInfo) -> LalResult<()> {
    let prefix = exe.prefix.clone().unwrap();
    // 0. sanity - could we actually upgrade if we tried?
    verify_permissions(exe)
        .map_err(|_| CliError::MissingPrefixPermissions(prefix.to_string_lossy().into()))?;
    debug!("Have permissions to write in {}", prefix.display());

    // 1. rename current running executable to the same except _old suffix
    let old_file = prefix.join("bin").join("_lal_old");
    if old_file.is_file() {
        // remove previous upgrade backup
        fs::remove_file(&old_file)?;
    }
    // 2. make sure we can download the tarball before starting
    let tar_dest = prefix.join("lal.tar");
    info!("Downloading tarball to {}", tar_dest.display());
    http_download_to_path(&latest.url, &tar_dest)?;
    info!("Backing up {} to {}", exe.path, old_file.display());
    fs::rename(&exe.path, &old_file)?; // need to undo this if we fail
    // NB: DO NOT INSERT CALLS THAT CAN FAIL HERE BEFORE THE OVERWRITE
    // 3. force dump lal tarball into exe.prefix - rollback if it failed
    info!("Unpacking new version of lal into {}", prefix.display());
    match overwrite_exe(latest, exe) {
        // NB: This call takes a small amount of time - and can be aborted :/
        // it is not an atomic operation, so recovery can unfortunately fail :|
        Ok(_) => trace!("overwrite successful"),
        Err(e) => {
            // tarball could potentially fail to extract here
            warn!("lal upgrade failed - rolling back");
            warn!("Error: {}", e);
            fs::rename(&old_file, &exe.path)?; // better hope this works..
            return Err(e);
        }
    }

    Ok(()) // we did it!
}


/// Check for and possibly upgrade lal when using musl releases
///
/// This will query for the latest version, and upgrade in the one possible case.
/// If a newer version found (> in semver), and it's a static executable,
/// then an executable upgrade is attempted from the new release url.
pub fn upgrade(silent: bool) -> LalResult<bool> {
    let latest = get_latest_lal_version()?;
    let exe = identify_exe()?;

    if latest.version > exe.version {
        // New version found - always full output now
        info!("A new version of lal is available: {}", latest.version);
        info!("You are running {} at {}", exe.version, exe.path);
        println!("");

        if exe.dynamic {
            info!("Your version is built from source - please run (in source checkout):");
            let build_flag = if exe.debug { "" } else { "--release" };
            info!("rustup update stable && git pull && cargo build {}",
                  build_flag);
        } else if exe.prefix.is_some() {
            // install lal in the prefix it's normally in
            info!("Upgrading...");
            upgrade_exe(&latest, &exe)?;
            info!("lal upgraded successfully to {} at {}",
                  latest.version,
                  exe.path);
            println!("");
        } else {
            // static, but no good guess of where to install - let user decide:
            info!("Your version is prebuilt but installed weirdly - please run:");
            info!("curl {} | tar xz -C /usr/local", latest.url);
        }
    } else if silent {
        debug!("You are running the latest version of lal");
    } else {
        info!("You are running the latest version of lal");
    }
    Ok(latest.version > exe.version)
}