@allansantos-dev/smart-tool 0.9.45 → 0.9.47
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +27 -0
- package/README.md +9 -4
- package/autostart.py +5 -3
- package/client_hooks.py +1 -1
- package/config.py +12 -0
- package/folder_picker.py +7 -5
- package/i18n.py +213 -0
- package/locales/pt-BR.json +926 -0
- package/notice_action.py +8 -0
- package/package.json +1 -1
- package/projects_ui.py +7 -2
- package/setup_audit.py +5 -3
- package/setup_ui.py +15 -2
- package/smart_tool_daemon.py +1 -0
- package/toast_notice.py +3 -2
- package/tray.py +108 -49
- package/update_check.py +29 -10
- package/update_splash.py +197 -0
- package/version.py +1 -1
package/update_splash.py
ADDED
|
@@ -0,0 +1,197 @@
|
|
|
1
|
+
"""Update screen: a small window with no buttons that runs the update with no console window and shows each step in
|
|
2
|
+
the interface language, then the result. Started by the tray (update_check.run_update) as `pythonw update_splash.py
|
|
3
|
+
<version>`; it outlives the tray, which the installer stops and starts again.
|
|
4
|
+
|
|
5
|
+
The install command runs with CREATE_NO_WINDOW, so it and every console program it starts (node, the installer,
|
|
6
|
+
pip, PowerShell, schtasks) share one hidden console. Its output goes to update_check.UPDATE_LOG; a failure shows the last line,
|
|
7
|
+
the log path and a Windows notification, and the installer has already restored the previous version."""
|
|
8
|
+
import os
|
|
9
|
+
import queue
|
|
10
|
+
import subprocess
|
|
11
|
+
import sys
|
|
12
|
+
import threading
|
|
13
|
+
import time
|
|
14
|
+
|
|
15
|
+
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
|
|
16
|
+
import i18n
|
|
17
|
+
import node_locator
|
|
18
|
+
import toast_notice
|
|
19
|
+
import update_check
|
|
20
|
+
import version
|
|
21
|
+
|
|
22
|
+
TIMEOUT_S = 20 * 60
|
|
23
|
+
SUCCESS_CLOSE_MS = 4000
|
|
24
|
+
FAILURE_CLOSE_MS = 15000
|
|
25
|
+
MUTEX_NAME = "Local\\SmartToolUpdate"
|
|
26
|
+
NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
|
27
|
+
STEPS = (
|
|
28
|
+
("Downloading uv", i18n.N_("Downloading components…")),
|
|
29
|
+
("Preparing Python", i18n.N_("Preparing Python…")),
|
|
30
|
+
("TLS inspection", i18n.N_("Preparing Python…")),
|
|
31
|
+
("Camoufox browser", i18n.N_("Checking the browsers…")),
|
|
32
|
+
("Chromium browser", i18n.N_("Checking the browsers…")),
|
|
33
|
+
("Older Smart Tool installation found", i18n.N_("Removing an older installation…")),
|
|
34
|
+
("Updating Smart Tool at", i18n.N_("Copying the new files…")),
|
|
35
|
+
("Installing Smart Tool at", i18n.N_("Copying the new files…")),
|
|
36
|
+
("Files changed:", i18n.N_("Installing dependencies…")),
|
|
37
|
+
("Browser control", i18n.N_("Checking browser control…")),
|
|
38
|
+
("Failed:", i18n.N_("Restoring the previous version…")),
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def step_for(line):
|
|
43
|
+
"""English catalog key of the step an installer output line starts, or None."""
|
|
44
|
+
for marker, step in STEPS:
|
|
45
|
+
if marker in line:
|
|
46
|
+
return step
|
|
47
|
+
return None
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def install_command():
|
|
51
|
+
"""argv of the update: the same npx command a user types, through cmd.exe because npx is a .cmd script."""
|
|
52
|
+
npx = node_locator.which("npx.cmd")
|
|
53
|
+
if not npx:
|
|
54
|
+
raise RuntimeError(i18n.t("Node.js (npx) was not found; install Node.js and try again."))
|
|
55
|
+
return ["cmd.exe", "/d", "/c", npx, "--yes", f"{update_check.PACKAGE}@latest", "install"], npx
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_install(lines):
|
|
59
|
+
"""Runs the install command hidden, puts each output line on `lines`, writes them to update_check.UPDATE_LOG and returns
|
|
60
|
+
(exit code, last non-empty line). Kills the whole process tree after TIMEOUT_S."""
|
|
61
|
+
command, npx = install_command()
|
|
62
|
+
os.makedirs(os.path.dirname(update_check.UPDATE_LOG), exist_ok=True)
|
|
63
|
+
last = ""
|
|
64
|
+
with open(update_check.UPDATE_LOG, "w", encoding="utf-8") as log:
|
|
65
|
+
log.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} {' '.join(command)}\n")
|
|
66
|
+
with subprocess.Popen(command, stdin=subprocess.DEVNULL, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
|
|
67
|
+
text=True, encoding="utf-8", errors="replace", creationflags=NO_WINDOW,
|
|
68
|
+
cwd=os.path.expanduser("~"), env=node_locator.environment(prefer=npx)) as proc:
|
|
69
|
+
timer = threading.Timer(TIMEOUT_S, lambda: subprocess.run(
|
|
70
|
+
["taskkill", "/PID", str(proc.pid), "/T", "/F"], capture_output=True, creationflags=NO_WINDOW))
|
|
71
|
+
timer.start()
|
|
72
|
+
try:
|
|
73
|
+
for line in proc.stdout:
|
|
74
|
+
log.write(line)
|
|
75
|
+
log.flush()
|
|
76
|
+
if line.strip():
|
|
77
|
+
last = line.strip()
|
|
78
|
+
lines.put(last)
|
|
79
|
+
code = proc.wait()
|
|
80
|
+
finally:
|
|
81
|
+
timer.cancel()
|
|
82
|
+
log.write(f"{time.strftime('%Y-%m-%d %H:%M:%S')} exit {code}\n")
|
|
83
|
+
return code, last
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def _single_instance():
|
|
87
|
+
"""True when no other update screen is running (a named mutex held for the life of this process)."""
|
|
88
|
+
if sys.platform != "win32":
|
|
89
|
+
return True
|
|
90
|
+
import ctypes
|
|
91
|
+
kernel32 = ctypes.WinDLL("kernel32", use_last_error=True)
|
|
92
|
+
kernel32.CreateMutexW.restype = ctypes.c_void_p
|
|
93
|
+
handle = kernel32.CreateMutexW(None, False, MUTEX_NAME)
|
|
94
|
+
return bool(handle) and ctypes.get_last_error() != 183
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Splash:
|
|
98
|
+
"""The window: product name, the version change, the current step, a moving bar; no buttons and no close box."""
|
|
99
|
+
|
|
100
|
+
def __init__(self, target):
|
|
101
|
+
import tkinter as tk
|
|
102
|
+
from tkinter import ttk
|
|
103
|
+
self.tk = tk
|
|
104
|
+
self.root = root = tk.Tk()
|
|
105
|
+
root.withdraw()
|
|
106
|
+
root.title("Smart Tool")
|
|
107
|
+
root.overrideredirect(True)
|
|
108
|
+
root.attributes("-topmost", True)
|
|
109
|
+
root.protocol("WM_DELETE_WINDOW", lambda: None)
|
|
110
|
+
width, height = 500, 236
|
|
111
|
+
root.geometry(f"{width}x{height}+{(root.winfo_screenwidth() - width) // 2}+{(root.winfo_screenheight() - height) // 3}")
|
|
112
|
+
frame = tk.Frame(root, bg="#ffffff", highlightthickness=1, highlightbackground="#c9d2df")
|
|
113
|
+
frame.pack(fill="both", expand=True)
|
|
114
|
+
tk.Frame(frame, bg="#2f6fed", height=6).pack(fill="x")
|
|
115
|
+
body = tk.Frame(frame, bg="#ffffff", padx=28, pady=18)
|
|
116
|
+
body.pack(fill="both", expand=True)
|
|
117
|
+
tk.Label(body, text="Smart Tool", bg="#ffffff", fg="#2f6fed", font=("Segoe UI Semibold", 11)).pack(anchor="w")
|
|
118
|
+
self.title = tk.Label(body, text=i18n.t("Updating Smart Tool"), bg="#ffffff", fg="#1b2430",
|
|
119
|
+
font=("Segoe UI Semibold", 17))
|
|
120
|
+
self.title.pack(anchor="w", pady=(2, 0))
|
|
121
|
+
tk.Label(body, text=i18n.t("Version {old} → {new}", old=version.VERSION, new=target), bg="#ffffff",
|
|
122
|
+
fg="#5b6472", font=("Segoe UI", 10)).pack(anchor="w")
|
|
123
|
+
style = ttk.Style(root)
|
|
124
|
+
style.theme_use("clam")
|
|
125
|
+
style.configure("Smart.Horizontal.TProgressbar", troughcolor="#e8edf5", background="#2f6fed",
|
|
126
|
+
bordercolor="#e8edf5", lightcolor="#2f6fed", darkcolor="#2f6fed")
|
|
127
|
+
self.bar = ttk.Progressbar(body, mode="indeterminate", style="Smart.Horizontal.TProgressbar", length=444)
|
|
128
|
+
self.bar.pack(anchor="w", pady=(16, 8))
|
|
129
|
+
self.bar.start(12)
|
|
130
|
+
self.step = tk.Label(body, text=i18n.t("Downloading version {version}…", version=target), bg="#ffffff",
|
|
131
|
+
fg="#1b2430", font=("Segoe UI", 10), anchor="w", justify="left", wraplength=444)
|
|
132
|
+
self.step.pack(anchor="w", fill="x")
|
|
133
|
+
self.note = tk.Label(body, text=i18n.t("This can take a few minutes. You do not need to do anything."),
|
|
134
|
+
bg="#ffffff", fg="#5b6472", font=("Segoe UI", 9), anchor="w", justify="left",
|
|
135
|
+
wraplength=444)
|
|
136
|
+
self.note.pack(anchor="w", fill="x", pady=(6, 0))
|
|
137
|
+
root.deiconify()
|
|
138
|
+
|
|
139
|
+
def show_step(self, key):
|
|
140
|
+
self.step.config(text=i18n.t(key))
|
|
141
|
+
|
|
142
|
+
def finish(self, ok, title, detail, note):
|
|
143
|
+
self.bar.stop()
|
|
144
|
+
self.bar.config(mode="determinate", maximum=100, value=100 if ok else 0)
|
|
145
|
+
self.title.config(text=title, fg="#18723c" if ok else "#b52c2c")
|
|
146
|
+
self.step.config(text=detail)
|
|
147
|
+
self.note.config(text=note)
|
|
148
|
+
self.root.after(SUCCESS_CLOSE_MS if ok else FAILURE_CLOSE_MS, self.root.destroy)
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def main(argv):
|
|
152
|
+
"""Shows the screen, runs the update and reports the outcome; returns the installer's exit code."""
|
|
153
|
+
target = argv[1] if len(argv) > 1 else "latest"
|
|
154
|
+
if not _single_instance():
|
|
155
|
+
return 0
|
|
156
|
+
splash = Splash(target)
|
|
157
|
+
lines, outcome = queue.Queue(), {}
|
|
158
|
+
|
|
159
|
+
def work():
|
|
160
|
+
try:
|
|
161
|
+
outcome["code"], outcome["last"] = run_install(lines)
|
|
162
|
+
except Exception as exc:
|
|
163
|
+
outcome["code"], outcome["last"] = 1, f"{type(exc).__name__}: {exc}"
|
|
164
|
+
|
|
165
|
+
worker = threading.Thread(target=work, daemon=True)
|
|
166
|
+
worker.start()
|
|
167
|
+
|
|
168
|
+
def pump():
|
|
169
|
+
while not lines.empty():
|
|
170
|
+
step = step_for(lines.get_nowait())
|
|
171
|
+
if step:
|
|
172
|
+
splash.show_step(step)
|
|
173
|
+
if worker.is_alive():
|
|
174
|
+
splash.root.after(200, pump)
|
|
175
|
+
return
|
|
176
|
+
if outcome["code"] == 0:
|
|
177
|
+
splash.finish(True, i18n.t("Smart Tool updated"),
|
|
178
|
+
i18n.t("Version {version} is installed and running.", version=target),
|
|
179
|
+
i18n.t("This window closes by itself."))
|
|
180
|
+
return
|
|
181
|
+
detail = i18n.t("The update did not finish; version {version} is still installed.", version=version.VERSION)
|
|
182
|
+
where = i18n.t("Details in {path}", path=update_check.UPDATE_LOG)
|
|
183
|
+
splash.finish(False, i18n.t("Update not finished"), f"{detail}\n{outcome['last'][:180]}", where)
|
|
184
|
+
try:
|
|
185
|
+
toast_notice.show(i18n.t("Smart Tool update not finished"), [detail, where],
|
|
186
|
+
[(i18n.t("View log"), toast_notice.action_uri("open-update-log"))], tag="update-failed")
|
|
187
|
+
except Exception as exc:
|
|
188
|
+
splash.note.config(text=f"{splash.note.cget('text')}\n{exc}")
|
|
189
|
+
|
|
190
|
+
splash.root.after(200, pump)
|
|
191
|
+
splash.root.mainloop()
|
|
192
|
+
worker.join(timeout=1)
|
|
193
|
+
return outcome.get("code", 1)
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
if __name__ == "__main__":
|
|
197
|
+
sys.exit(main(sys.argv))
|
package/version.py
CHANGED