mirror of
https://gitlab.com/cleanflash/installer.git
synced 2026-08-13 10:51:37 +08:00
Implement CLI and silent install / uninstall
This commit is contained in:
@@ -9,6 +9,9 @@ clean_flash_common = { path = "../clean_flash_common" }
|
||||
clean_flash_ui = { path = "../clean_flash_ui" }
|
||||
minifb = { workspace = true }
|
||||
windows-sys = { workspace = true }
|
||||
argh = { workspace = true }
|
||||
ratatui = { workspace = true }
|
||||
crossterm = { workspace = true }
|
||||
|
||||
[build-dependencies]
|
||||
winresource = "0.1"
|
||||
|
||||
@@ -1,4 +1,18 @@
|
||||
fn main() {
|
||||
// On macOS, embed Info.plist into the binary so the OS recognises it as a GUI app.
|
||||
if cfg!(target_os = "macos") {
|
||||
let plist = std::path::Path::new("../../resources/macos/uninstaller-Info.plist");
|
||||
if plist.exists() {
|
||||
println!("cargo:rustc-link-arg=-sectcreate");
|
||||
println!("cargo:rustc-link-arg=__TEXT");
|
||||
println!("cargo:rustc-link-arg=__info_plist");
|
||||
println!(
|
||||
"cargo:rustc-link-arg={}",
|
||||
plist.canonicalize().unwrap().display()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if cfg!(target_os = "windows") {
|
||||
let mut res = winresource::WindowsResource::new();
|
||||
if std::path::Path::new("../../resources/icon.ico").exists() {
|
||||
|
||||
@@ -0,0 +1,38 @@
|
||||
use argh::FromArgs;
|
||||
|
||||
/// Clean Flash Player Uninstaller
|
||||
///
|
||||
/// Uninstall Clean Flash Player with GUI, TUI, or silent mode.
|
||||
#[derive(FromArgs)]
|
||||
pub struct UninstallerArgs {
|
||||
/// use the terminal UI (ratatui) instead of the graphical window
|
||||
#[argh(switch)]
|
||||
pub cli: bool,
|
||||
|
||||
/// run the uninstallation non-interactively (no GUI or TUI)
|
||||
#[argh(switch)]
|
||||
pub uninstall: bool,
|
||||
|
||||
/// suppress all output (only valid with --uninstall)
|
||||
#[argh(switch)]
|
||||
pub silent: bool,
|
||||
}
|
||||
|
||||
/// The mode in which the application should run.
|
||||
pub enum RunMode {
|
||||
Gui,
|
||||
Tui,
|
||||
Silent,
|
||||
}
|
||||
|
||||
impl UninstallerArgs {
|
||||
pub fn determine_mode(&self) -> RunMode {
|
||||
if self.uninstall {
|
||||
RunMode::Silent
|
||||
} else if self.cli {
|
||||
RunMode::Tui
|
||||
} else {
|
||||
RunMode::Gui
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,16 @@
|
||||
#![windows_subsystem = "windows"]
|
||||
|
||||
mod cli_args;
|
||||
mod silent_uninstaller;
|
||||
mod tui_uninstaller;
|
||||
mod uninstall_form;
|
||||
|
||||
use cli_args::{UninstallerArgs, RunMode};
|
||||
use clean_flash_ui::renderer::Renderer;
|
||||
use minifb::{Key, MouseButton, MouseMode, Window, WindowOptions};
|
||||
use uninstall_form::{UninstallForm, HEIGHT, WIDTH};
|
||||
|
||||
fn main() {
|
||||
fn run_gui() {
|
||||
let title = format!(
|
||||
"Clean Flash Player {} Uninstaller",
|
||||
clean_flash_common::update_checker::FLASH_VERSION
|
||||
@@ -51,3 +55,32 @@ fn main() {
|
||||
.expect("Failed to update window buffer");
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let args: UninstallerArgs = argh::from_env();
|
||||
|
||||
match args.determine_mode() {
|
||||
RunMode::Silent => {
|
||||
if let Err(e) = silent_uninstaller::run_silent_uninstall(&args) {
|
||||
eprintln!("[!] Fatal error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
RunMode::Tui => {
|
||||
if let Err(e) = tui_uninstaller::run_tui_uninstaller() {
|
||||
eprintln!("[!] TUI error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
RunMode::Gui => {
|
||||
let gui_result = std::panic::catch_unwind(run_gui);
|
||||
if gui_result.is_err() {
|
||||
eprintln!("GUI failed to initialize, falling back to terminal UI...");
|
||||
if let Err(e) = tui_uninstaller::run_tui_uninstaller() {
|
||||
eprintln!("[!] TUI error: {}", e);
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
use crate::cli_args::UninstallerArgs;
|
||||
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||
|
||||
struct SilentProgressCallback {
|
||||
silent: bool,
|
||||
}
|
||||
|
||||
impl ProgressCallback for SilentProgressCallback {
|
||||
fn update_progress_label(&self, text: &str, _tick: bool) {
|
||||
if !self.silent {
|
||||
eprintln!("[*] {}", text);
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_progress(&self) {}
|
||||
}
|
||||
|
||||
pub fn run_silent_uninstall(args: &UninstallerArgs) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let version = update_checker::FLASH_VERSION;
|
||||
|
||||
if !args.silent {
|
||||
eprintln!("Clean Flash Player {} - Silent Uninstall", version);
|
||||
}
|
||||
|
||||
let callback = SilentProgressCallback {
|
||||
silent: args.silent,
|
||||
};
|
||||
|
||||
let redir = redirection::disable_redirection();
|
||||
let result: Result<(), InstallError> = uninstaller::uninstall(&callback);
|
||||
redirection::enable_redirection(redir);
|
||||
|
||||
match result {
|
||||
Ok(()) => {
|
||||
if !args.silent {
|
||||
eprintln!("[+] Uninstallation completed successfully.");
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if !args.silent {
|
||||
eprintln!("[!] Uninstallation failed: {}", e);
|
||||
}
|
||||
std::process::exit(1);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,306 @@
|
||||
use clean_flash_common::{uninstaller, redirection, update_checker, ProgressCallback, InstallError};
|
||||
use crossterm::{
|
||||
event::{self, Event, KeyCode, KeyEventKind},
|
||||
terminal::{disable_raw_mode, enable_raw_mode, EnterAlternateScreen, LeaveAlternateScreen},
|
||||
execute,
|
||||
};
|
||||
use ratatui::{
|
||||
prelude::*,
|
||||
widgets::{Block, Borders, Gauge, Paragraph, Wrap},
|
||||
};
|
||||
use std::io::{self, stdout};
|
||||
use std::sync::{Arc, Mutex};
|
||||
|
||||
const FLASH_VERSION: &str = update_checker::FLASH_VERSION;
|
||||
const UNINSTALL_TICKS: u32 = 10;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq)]
|
||||
enum Panel {
|
||||
Confirm,
|
||||
Uninstall,
|
||||
Complete,
|
||||
Failure,
|
||||
}
|
||||
|
||||
struct ProgressState {
|
||||
label: String,
|
||||
value: u32,
|
||||
maximum: u32,
|
||||
done: bool,
|
||||
error: Option<String>,
|
||||
}
|
||||
|
||||
struct TuiUninstallState {
|
||||
panel: Panel,
|
||||
progress: Arc<Mutex<ProgressState>>,
|
||||
failure_detail: String,
|
||||
}
|
||||
|
||||
impl TuiUninstallState {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
panel: Panel::Confirm,
|
||||
progress: Arc::new(Mutex::new(ProgressState {
|
||||
label: "Preparing...".into(),
|
||||
value: 0,
|
||||
maximum: UNINSTALL_TICKS,
|
||||
done: false,
|
||||
error: None,
|
||||
})),
|
||||
failure_detail: String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_uninstall(&mut self) {
|
||||
self.panel = Panel::Uninstall;
|
||||
|
||||
{
|
||||
let mut state = self.progress.lock().unwrap();
|
||||
state.label = "Preparing...".into();
|
||||
state.value = 0;
|
||||
state.maximum = UNINSTALL_TICKS;
|
||||
state.done = false;
|
||||
state.error = None;
|
||||
}
|
||||
|
||||
let progress = Arc::clone(&self.progress);
|
||||
std::thread::spawn(move || {
|
||||
let callback = ThreadProgressCallback {
|
||||
state: Arc::clone(&progress),
|
||||
};
|
||||
|
||||
let redir = redirection::disable_redirection();
|
||||
let result: Result<(), InstallError> = uninstaller::uninstall(&callback);
|
||||
redirection::enable_redirection(redir);
|
||||
|
||||
let mut state = progress.lock().unwrap();
|
||||
if let Err(e) = result {
|
||||
state.error = Some(e.to_string());
|
||||
}
|
||||
state.done = true;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
struct ThreadProgressCallback {
|
||||
state: Arc<Mutex<ProgressState>>,
|
||||
}
|
||||
|
||||
impl ProgressCallback for ThreadProgressCallback {
|
||||
fn update_progress_label(&self, text: &str, tick: bool) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.label = text.to_string();
|
||||
if tick {
|
||||
state.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
fn tick_progress(&self) {
|
||||
let mut state = self.state.lock().unwrap();
|
||||
state.value += 1;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn run_tui_uninstaller() -> Result<(), Box<dyn std::error::Error>> {
|
||||
enable_raw_mode()?;
|
||||
let mut stdout = stdout();
|
||||
execute!(stdout, EnterAlternateScreen)?;
|
||||
let backend = CrosstermBackend::new(stdout);
|
||||
let mut terminal = Terminal::new(backend)?;
|
||||
|
||||
let result = run_app(&mut terminal);
|
||||
|
||||
disable_raw_mode()?;
|
||||
execute!(terminal.backend_mut(), LeaveAlternateScreen)?;
|
||||
terminal.show_cursor()?;
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
fn run_app(terminal: &mut Terminal<CrosstermBackend<io::Stdout>>) -> Result<(), Box<dyn std::error::Error>> {
|
||||
let mut state = TuiUninstallState::new();
|
||||
|
||||
loop {
|
||||
terminal.draw(|f| draw_ui(f, &state))?;
|
||||
|
||||
if state.panel == Panel::Uninstall {
|
||||
// Poll progress
|
||||
{
|
||||
let ps = state.progress.lock().unwrap();
|
||||
if ps.done {
|
||||
if let Some(ref err) = ps.error {
|
||||
state.failure_detail = err.clone();
|
||||
drop(ps);
|
||||
state.panel = Panel::Failure;
|
||||
} else {
|
||||
drop(ps);
|
||||
state.panel = Panel::Complete;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if event::poll(std::time::Duration::from_millis(100))? {
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind == KeyEventKind::Press && (key.code == KeyCode::Char('q') || key.code == KeyCode::Esc) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
continue;
|
||||
}
|
||||
|
||||
if let Event::Key(key) = event::read()? {
|
||||
if key.kind != KeyEventKind::Press {
|
||||
continue;
|
||||
}
|
||||
|
||||
match key.code {
|
||||
KeyCode::Char('q') | KeyCode::Esc => break,
|
||||
KeyCode::Enter => {
|
||||
match state.panel {
|
||||
Panel::Confirm => state.start_uninstall(),
|
||||
Panel::Complete => break,
|
||||
Panel::Failure => state.start_uninstall(), // retry
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn draw_ui(f: &mut Frame, state: &TuiUninstallState) {
|
||||
let area = f.area();
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(5),
|
||||
Constraint::Length(3),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
// Title
|
||||
let title = format!(
|
||||
" Clean Flash Player {} Uninstaller ",
|
||||
FLASH_VERSION
|
||||
);
|
||||
let title_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::Cyan))
|
||||
.title(title)
|
||||
.title_alignment(Alignment::Center);
|
||||
f.render_widget(title_block, chunks[0]);
|
||||
|
||||
// Content
|
||||
let content_block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::default().fg(Color::DarkGray));
|
||||
let inner = content_block.inner(chunks[1]);
|
||||
f.render_widget(content_block, chunks[1]);
|
||||
|
||||
match state.panel {
|
||||
Panel::Confirm => draw_confirm(f, inner),
|
||||
Panel::Uninstall => draw_uninstall(f, inner, state),
|
||||
Panel::Complete => draw_complete(f, inner),
|
||||
Panel::Failure => draw_failure(f, inner, state),
|
||||
}
|
||||
|
||||
// Footer
|
||||
let footer_text = match state.panel {
|
||||
Panel::Confirm => "[Enter] Uninstall [q/Esc] Quit",
|
||||
Panel::Uninstall => "Uninstalling...",
|
||||
Panel::Complete => "[Enter/q] Quit",
|
||||
Panel::Failure => "[Enter] Retry [q] Quit",
|
||||
};
|
||||
let footer = Paragraph::new(footer_text)
|
||||
.alignment(Alignment::Center)
|
||||
.block(Block::default().borders(Borders::ALL).border_style(Style::default().fg(Color::DarkGray)));
|
||||
f.render_widget(footer, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_confirm(f: &mut Frame, area: Rect) {
|
||||
let text = "You are about to uninstall Clean Flash Player.\n\
|
||||
Please close all browsers, including Google Chrome, Mozilla Firefox and Internet Explorer.\n\n\
|
||||
The installer will completely remove all versions of Flash Player from this computer,\n\
|
||||
including Clean Flash Player and older versions of Adobe Flash Player.\n\n\
|
||||
Press Enter to proceed.";
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(Style::default().fg(Color::White));
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_uninstall(f: &mut Frame, area: Rect, state: &TuiUninstallState) {
|
||||
let ps = state.progress.lock().unwrap();
|
||||
let label = ps.label.clone();
|
||||
let ratio = if ps.maximum > 0 {
|
||||
(ps.value as f64 / ps.maximum as f64).min(1.0)
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
drop(ps);
|
||||
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([
|
||||
Constraint::Length(2),
|
||||
Constraint::Length(1),
|
||||
Constraint::Length(3),
|
||||
Constraint::Min(0),
|
||||
])
|
||||
.split(area);
|
||||
|
||||
let header = Paragraph::new("Uninstallation in progress...")
|
||||
.style(Style::default().fg(Color::White).add_modifier(Modifier::BOLD));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
let status = Paragraph::new(label).style(Style::default().fg(Color::White));
|
||||
f.render_widget(status, chunks[1]);
|
||||
|
||||
let gauge = Gauge::default()
|
||||
.block(Block::default().borders(Borders::ALL))
|
||||
.gauge_style(Style::default().fg(Color::Cyan).bg(Color::DarkGray))
|
||||
.ratio(ratio);
|
||||
f.render_widget(gauge, chunks[2]);
|
||||
}
|
||||
|
||||
fn draw_complete(f: &mut Frame, area: Rect) {
|
||||
let text = "All versions of Flash Player have been successfully uninstalled.\n\n\
|
||||
If you ever change your mind, check out Clean Flash Player's website!";
|
||||
|
||||
let paragraph = Paragraph::new(text)
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(Style::default().fg(Color::Green));
|
||||
f.render_widget(paragraph, area);
|
||||
}
|
||||
|
||||
fn draw_failure(f: &mut Frame, area: Rect, state: &TuiUninstallState) {
|
||||
let chunks = Layout::default()
|
||||
.direction(Direction::Vertical)
|
||||
.constraints([Constraint::Length(3), Constraint::Min(2)])
|
||||
.split(area);
|
||||
|
||||
let header = Paragraph::new(
|
||||
"Oops! The uninstallation process has encountered an unexpected problem.\nPress Enter to retry or q to quit.",
|
||||
)
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(Style::default().fg(Color::Red).add_modifier(Modifier::BOLD));
|
||||
f.render_widget(header, chunks[0]);
|
||||
|
||||
let detail_text = if state.failure_detail.len() > 500 {
|
||||
&state.failure_detail[..500]
|
||||
} else {
|
||||
&state.failure_detail
|
||||
};
|
||||
let detail = Paragraph::new(detail_text.to_string())
|
||||
.wrap(Wrap { trim: false })
|
||||
.style(Style::default().fg(Color::White));
|
||||
f.render_widget(detail, chunks[1]);
|
||||
}
|
||||
Reference in New Issue
Block a user