Skip to content

Commit c193632

Browse files
committed
fix: updater
1 parent 16ee953 commit c193632

File tree

11 files changed

+718
-23
lines changed

11 files changed

+718
-23
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -189,6 +189,7 @@ src/
189189
!GUI/src/preview_format/
190190
!GUI/src/material_ct.py
191191
!GUI/src/__init__.py
192+
!tools/installer/src/
192193
temp
193194
*-in.txt
194195
untitled*

GUI/manager/__init__.py

Lines changed: 23 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import os
2-
import json
3-
from pathlib import Path
2+
import shlex
43
import subprocess
54
import typing as t
65

@@ -12,7 +11,7 @@
1211
from assets import res
1312
from variables import PYPI_SOURCE
1413
from deploy.update import Proj
15-
from utils import conf, ori_path, env, uv_exc, exc_p, TaskObj, TasksObj
14+
from utils import conf, env, uv_exc, exc_p, TaskObj, TasksObj
1615
from utils.processed_class import PreviewHtml
1716
from utils.sql import SqlRecorder
1817
from GUI.uic.qfluent.components import (
@@ -176,24 +175,27 @@ def rerun(self):
176175
QTimer.singleShot(1000, self.gui.close)
177176

178177
def to_update(self, ver):
179-
# self.gui.open_url_by_browser(self.changelog_url)
180-
uv_env = {key: os.environ[key] for key in ('UV_TOOL_DIR', 'UV_TOOL_BIN_DIR') if key in os.environ}
181-
tool_dir = Path(uv_env['UV_TOOL_DIR'])
182-
if os.name == "nt":
183-
python_exc = tool_dir / "comicguispider" / "Scripts" / "python.exe"
184-
else:
185-
python_exc = tool_dir / "comicguispider" / "bin" / "python"
186-
with ori_path.joinpath("assets/update.txt").open("r", encoding="utf-8") as f:
187-
template = f.read()
188-
updater_script = template.replace(r"{uv_env_dict}", json.dumps(uv_env, ensure_ascii=False))
189-
script_path = tool_dir.joinpath("cgs_update.py")
190-
with open(script_path, "w", encoding="utf-8") as f:
191-
f.write(updater_script)
192-
args = [str(python_exc), str(script_path),
193-
'--uv-exc', uv_exc, '--version', ver,
194-
'--index-url', PYPI_SOURCE[conf.pypi_source]]
195-
if os.name == "nt":
178+
index_url = PYPI_SOURCE[conf.pypi_source]
179+
installer_exe = exc_p / "installer.exe"
180+
log = str(exc_p / "cgs_update.log")
181+
if os.name == "nt" and installer_exe.exists():
182+
args = [
183+
str(installer_exe),
184+
'--version', ver,
185+
'--uv-exc', uv_exc,
186+
'--index-url', index_url,
187+
'--parent-pid', str(os.getpid()),
188+
]
189+
for key in ('UV_TOOL_DIR', 'UV_TOOL_BIN_DIR'):
190+
if key in os.environ:
191+
args.extend([f'--{key.lower().replace("_", "-")}', os.environ[key]])
196192
subprocess.Popen(args, creationflags=subprocess.CREATE_NEW_CONSOLE, env=env)
197193
else:
198-
subprocess.Popen(args, start_new_session=True, env=env)
194+
cmd = f"{uv_exc} tool install ComicGUISpider=={ver} --force --index-url {index_url}"
195+
if os.name == "nt":
196+
subprocess.Popen(["cmd", "/c", "start", "", "powershell", "-NoProfile", "-Command",
197+
f"{cmd} 2>&1 | Tee-Object -FilePath {shlex.quote(log)} ; Read-Host 'Press Enter to close'"], env=env)
198+
else:
199+
full = f"""{cmd} 2>&1 | tee -a {shlex.quote(log)} ; echo 'done'; read -n1 -s -r -p 'Press any key to close...'"""
200+
subprocess.Popen(["setsid", "sh", "-c", full], start_new_session=True, env=env)
199201
self.gui.close()

assets/_pystand_static.int

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ if __name__ == '__main__':
5656
pkg_spec = pkg_spec.replace("ComicGUISpider", "ComicGUISpider[script]")
5757
install_cmd = [str(uv_exec), "tool", "install", pkg_spec, "--force"]
5858
if args.conf:
59-
install_cmd.extend(["--config-file", portable_env.joinpath(args.conf)])
59+
install_cmd = [str(uv_exec), "--config-file", portable_env.joinpath(args.conf), "tool", "install", pkg_spec, "--force"]
6060
else:
6161
install_cmd.extend(["--index-url", index_value])
6262
print(install_cmd)

deploy/packer.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -142,7 +142,7 @@ def packup(self, runtime_init=False):
142142
return self.packup()
143143
zip_file = self.preset_zip_file
144144
specified = ('runtime', '_pystand_static.int',
145-
f'{self._proj}.exe')
145+
f'{self._proj}.exe', 'installer.exe')
146146
mode = "w"
147147
elif self.preset_zip_file.exists():
148148
shutil.copy(self.preset_zip_file, self.zip_file)

tools/installer/Cargo.toml

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
[package]
2+
name = "cgs-installer"
3+
version = "2.8.6"
4+
edition = "2024"
5+
6+
[[bin]]
7+
name = "installer"
8+
path = "src/main.rs"
9+
10+
[dependencies]
11+
eframe = "0.33"
12+
clap = { version = "4.5", features = ["derive"] }
13+
pep440_rs = "0.7"
14+
regex = "1.12"
15+
16+
[target.'cfg(windows)'.dependencies]
17+
windows = { version = "0.62", features = [
18+
"Win32_System_Threading",
19+
"Win32_Foundation",
20+
] }
21+
22+
[profile.release]
23+
opt-level = "z"
24+
lto = true
25+
strip = true
26+
codegen-units = 1

tools/installer/build.rs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
use std::env;
2+
use std::fs;
3+
use std::path::PathBuf;
4+
5+
fn main() {
6+
#[cfg(windows)]
7+
{
8+
let target_env = env::var("CARGO_CFG_TARGET_ENV").unwrap_or_default();
9+
if target_env == "msvc" {
10+
let out_dir = PathBuf::from(env::var("OUT_DIR").expect("OUT_DIR not set"));
11+
let manifest_path = out_dir.join("installer.manifest");
12+
let manifest = r#"<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
13+
<assembly xmlns="urn:schemas-microsoft-com:asm.v1" manifestVersion="1.0">
14+
<assemblyIdentity type="win32" name="ComicGUISpider.Installer" version="1.0.0.0"/>
15+
<trustInfo xmlns="urn:schemas-microsoft-com:asm.v3">
16+
<security>
17+
<requestedPrivileges>
18+
<requestedExecutionLevel level="asInvoker" uiAccess="false"/>
19+
</requestedPrivileges>
20+
</security>
21+
</trustInfo>
22+
<application xmlns="urn:schemas-microsoft-com:asm.v3">
23+
<windowsSettings>
24+
<activeCodePage xmlns="http://schemas.microsoft.com/SMI/2019/WindowsSettings">UTF-8</activeCodePage>
25+
<dpiAwareness xmlns="http://schemas.microsoft.com/SMI/2016/WindowsSettings">PerMonitorV2</dpiAwareness>
26+
</windowsSettings>
27+
</application>
28+
</assembly>
29+
"#;
30+
fs::write(&manifest_path, manifest).expect("failed to write manifest");
31+
println!("cargo:rustc-link-arg-bins=/MANIFEST:EMBED");
32+
println!(
33+
"cargo:rustc-link-arg-bins=/MANIFESTINPUT:{}",
34+
manifest_path.display()
35+
);
36+
}
37+
}
38+
println!("cargo:rerun-if-changed=build.rs");
39+
}

tools/installer/src/args.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
use clap::Parser;
2+
3+
#[derive(Debug, Clone, Parser)]
4+
#[command(name = "installer", version, about = "ComicGUISpider native updater")]
5+
pub struct CliArgs {
6+
#[arg(long = "uv-exc")]
7+
pub uv_exc: String,
8+
9+
#[arg(long)]
10+
pub version: String,
11+
12+
#[arg(long = "index-url", default_value_t)]
13+
pub index_url: String,
14+
15+
#[arg(long = "parent-pid", default_value_t = 0)]
16+
pub parent_pid: u32,
17+
18+
#[arg(long = "uv-tool-dir", default_value_t)]
19+
pub uv_tool_dir: String,
20+
21+
#[arg(long = "uv-tool-bin-dir", default_value_t)]
22+
pub uv_tool_bin_dir: String,
23+
24+
#[arg(long = "no-gui")]
25+
pub no_gui: bool,
26+
}

tools/installer/src/gui.rs

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
use crate::process::{spawn_update_worker, InstallerConfig, InstallerEvent};
2+
use eframe::egui;
3+
use egui::{Color32, Frame, Margin, RichText};
4+
use std::sync::atomic::{AtomicI32, Ordering};
5+
use std::sync::mpsc;
6+
use std::sync::Arc;
7+
use std::time::{Duration, Instant};
8+
9+
const BG: Color32 = Color32::from_rgb(26, 26, 46);
10+
const TEXT_PRIMARY: Color32 = Color32::from_rgb(226, 226, 234);
11+
const TEXT_SECONDARY: Color32 = Color32::from_rgb(136, 136, 160);
12+
const ACCENT: Color32 = Color32::from_rgb(96, 165, 250);
13+
const SUCCESS: Color32 = Color32::from_rgb(74, 222, 128);
14+
const ERROR: Color32 = Color32::from_rgb(248, 113, 113);
15+
16+
fn configure_visuals(ctx: &egui::Context) {
17+
ctx.set_visuals(egui::Visuals {
18+
panel_fill: BG,
19+
window_fill: BG,
20+
override_text_color: Some(TEXT_PRIMARY),
21+
..egui::Visuals::dark()
22+
});
23+
}
24+
25+
fn status_color(status: &str) -> Color32 {
26+
let s = status.to_ascii_lowercase();
27+
if s.contains("fail") || s.contains("error") {
28+
ERROR
29+
} else if s.contains("installed") || s.contains("success") || s.contains("restart") || s.contains("done") {
30+
SUCCESS
31+
} else if s.contains("download") || s.contains("resolv") || s.contains("install") {
32+
ACCENT
33+
} else {
34+
TEXT_SECONDARY
35+
}
36+
}
37+
38+
pub fn run_gui(config: InstallerConfig) -> i32 {
39+
let (tx, rx) = mpsc::channel::<InstallerEvent>();
40+
let _worker = spawn_update_worker(config.clone(), tx);
41+
let exit_code = Arc::new(AtomicI32::new(1));
42+
let exit_code_app = Arc::clone(&exit_code);
43+
44+
let options = eframe::NativeOptions {
45+
viewport: egui::ViewportBuilder::default()
46+
.with_inner_size([480.0, 220.0])
47+
.with_resizable(false),
48+
..Default::default()
49+
};
50+
51+
let ver = config.version.clone();
52+
let result = eframe::run_native(
53+
"CGS Updater",
54+
options,
55+
Box::new(move |cc| {
56+
configure_visuals(&cc.egui_ctx);
57+
Ok(Box::new(UpdaterApp::new(ver, rx, exit_code_app)))
58+
}),
59+
);
60+
61+
if result.is_err() {
62+
return 1;
63+
}
64+
exit_code.load(Ordering::Relaxed)
65+
}
66+
67+
struct UpdaterApp {
68+
version: String,
69+
status: String,
70+
progress: u8,
71+
rx: mpsc::Receiver<InstallerEvent>,
72+
exit_code: Arc<AtomicI32>,
73+
close_at: Option<Instant>,
74+
}
75+
76+
impl UpdaterApp {
77+
fn new(
78+
version: String,
79+
rx: mpsc::Receiver<InstallerEvent>,
80+
exit_code: Arc<AtomicI32>,
81+
) -> Self {
82+
Self {
83+
version,
84+
status: "Preparing update...".into(),
85+
progress: 0,
86+
rx,
87+
exit_code,
88+
close_at: None,
89+
}
90+
}
91+
92+
fn drain_events(&mut self) {
93+
while let Ok(ev) = self.rx.try_recv() {
94+
match ev {
95+
InstallerEvent::Progress { percent, status } => {
96+
self.progress = percent.min(100);
97+
self.status = status;
98+
}
99+
InstallerEvent::Finished { exit_code, message } => {
100+
self.exit_code.store(exit_code, Ordering::Relaxed);
101+
self.status = message;
102+
if exit_code == 0 {
103+
self.progress = 100;
104+
self.close_at = Some(Instant::now() + Duration::from_millis(1500));
105+
}
106+
}
107+
InstallerEvent::Fatal { message } => {
108+
self.exit_code.store(1, Ordering::Relaxed);
109+
self.status = message;
110+
}
111+
}
112+
}
113+
}
114+
}
115+
116+
impl eframe::App for UpdaterApp {
117+
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
118+
self.drain_events();
119+
120+
if let Some(t) = self.close_at {
121+
if Instant::now() >= t {
122+
ctx.send_viewport_cmd(egui::ViewportCommand::Close);
123+
return;
124+
}
125+
ctx.request_repaint_after(Duration::from_millis(50));
126+
}
127+
128+
let panel = Frame::new()
129+
.inner_margin(Margin { left: 30, right: 30, top: 28, bottom: 20 })
130+
.corner_radius(8.0)
131+
.fill(BG);
132+
133+
egui::CentralPanel::default().frame(panel).show(ctx, |ui| {
134+
ui.label(RichText::new("CGS Updater").size(22.0).strong().color(TEXT_PRIMARY));
135+
ui.add_space(4.0);
136+
ui.label(RichText::new(format!("v{}", self.version)).size(13.0).color(TEXT_SECONDARY));
137+
ui.add_space(12.0);
138+
ui.separator();
139+
ui.add_space(12.0);
140+
ui.label(RichText::new(&self.status).size(14.0).color(status_color(&self.status)));
141+
ui.add_space(16.0);
142+
ui.add(
143+
egui::ProgressBar::new(self.progress as f32 / 100.0)
144+
.fill(ACCENT)
145+
.show_percentage(),
146+
);
147+
});
148+
149+
ctx.request_repaint_after(Duration::from_millis(100));
150+
}
151+
}

tools/installer/src/main.rs

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
mod args;
2+
mod gui;
3+
mod process;
4+
mod version;
5+
6+
use args::CliArgs;
7+
use clap::Parser;
8+
use process::{cleanup_stale_dirs, run_update_cli, wait_for_parent_exit, InstallerConfig};
9+
10+
fn main() {
11+
let args = CliArgs::parse();
12+
let config = InstallerConfig::from_args(&args);
13+
14+
wait_for_parent_exit(args.parent_pid, 30_000);
15+
if let Err(err) = cleanup_stale_dirs(&config) {
16+
eprintln!("stale cleanup: {err}");
17+
}
18+
19+
let exit_code = if args.no_gui {
20+
run_update_cli(&config)
21+
} else {
22+
gui::run_gui(config)
23+
};
24+
25+
std::process::exit(exit_code);
26+
}

0 commit comments

Comments
 (0)