@zogien/native-sdk 0.11.0-fork.2 → 0.11.0-fork.4
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/README.md +1 -1
- package/build/app.zig +47 -23
- package/build/cli.zig +22 -0
- package/native-sdk.d.ts +2 -0
- package/package.json +10 -10
- package/packages/core/package-lock.json +2 -2
- package/packages/core/package.json +1 -1
- package/schemas/app.schema.json +9 -1
- package/skill-data/ts-core/SKILL.md +2 -0
- package/src/app_runner/app_assets.zig +85 -1
- package/src/app_runner/root.zig +63 -3
- package/src/app_runner/ts_core_main.zig +8 -0
- package/src/platform/macos/appkit_host.h +9 -0
- package/src/platform/macos/appkit_host.m +366 -0
- package/src/platform/macos/root.zig +29 -6
- package/src/platform/null_platform.zig +66 -0
- package/src/platform/root.zig +1 -0
- package/src/platform/types.zig +46 -0
- package/src/platform/windows/root.zig +174 -0
- package/src/platform/windows/scriptc_mingw_compat.c +248 -0
- package/src/platform/windows/webview2_host.cpp +500 -26
- package/src/primitives/app_manifest/root.zig +1 -0
- package/src/primitives/app_manifest/tests.zig +35 -0
- package/src/primitives/app_manifest/types.zig +12 -0
- package/src/primitives/app_manifest/validation.zig +44 -6
- package/src/root.zig +1 -0
- package/src/runtime/bridge_responses.zig +2 -0
- package/src/runtime/channel_sources.zig +49 -1
- package/src/runtime/core.zig +5 -0
- package/src/runtime/effects.zig +12 -1
- package/src/runtime/flow.zig +6 -0
- package/src/runtime/flow_reference.zig +6 -0
- package/src/runtime/root.zig +4 -0
- package/src/runtime/single_instance.zig +893 -0
- package/src/runtime/tests.zig +1 -0
- package/src/runtime/window_controls.zig +73 -0
- package/src/runtime/window_controls_tests.zig +192 -0
- package/src/runtime/window_storage.zig +3 -0
- package/src/tooling/manifest.zig +111 -3
- package/src/tooling/package.zig +98 -8
- package/src/tooling/raw_manifest.zig +7 -0
- package/src/tooling/root.zig +2 -0
- package/src/tooling/templates.zig +53 -3
- package/src/tooling/update.zig +228 -10
- package/src/tooling/windows_installer.zig +245 -0
- package/src/tooling/zip_writer.zig +211 -0
- package/src/updater/engine.zig +936 -0
- package/src/updater/feed.zig +261 -12
- package/src/updater/macos.zig +187 -0
- package/src/updater/root.zig +41 -0
- package/src/updater/windows.zig +470 -0
- package/src/windows_install/config.zig +247 -0
- package/src/windows_install/main.zig +690 -0
- package/src/windows_install/payload.zig +146 -0
- package/src/windows_install/switch.zig +184 -0
- package/src/windows_install/tests.zig +11 -0
- package/src/windows_install/win32.zig +248 -0
- package/tools/native-sdk/main.zig +10 -9
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
//! The Windows setup program, launcher and uninstaller of an installed
|
|
2
|
+
//! Native SDK app: one small program in three roles.
|
|
3
|
+
//!
|
|
4
|
+
//! setup.exe this program with an install payload appended:
|
|
5
|
+
//! installs the app for the current user, under
|
|
6
|
+
//! %LOCALAPPDATA%\Programs\<display name>.
|
|
7
|
+
//! <root>\<name>.exe the launcher at a stable path: finishes a staged
|
|
8
|
+
//! version switch while the app is not running, then
|
|
9
|
+
//! starts app\bin\<executable> with the same
|
|
10
|
+
//! arguments. The Start menu shortcut, file
|
|
11
|
+
//! associations and URL schemes point here.
|
|
12
|
+
//! <name>.exe --uninstall [--silent]
|
|
13
|
+
//! removes the shortcut, the registry entries and the
|
|
14
|
+
//! install folder. User data under the app's data
|
|
15
|
+
//! directories stays.
|
|
16
|
+
|
|
17
|
+
const std = @import("std");
|
|
18
|
+
const builtin = @import("builtin");
|
|
19
|
+
const install_config = @import("config.zig");
|
|
20
|
+
const payload = @import("payload.zig");
|
|
21
|
+
const install_switch = @import("switch.zig");
|
|
22
|
+
const win = @import("win32.zig");
|
|
23
|
+
|
|
24
|
+
const InstallConfig = install_config.InstallConfig;
|
|
25
|
+
|
|
26
|
+
pub const launcher_file_in_image = "native-launcher.exe";
|
|
27
|
+
const setup_zip_name = ".native-setup.zip";
|
|
28
|
+
|
|
29
|
+
const exit_ok: u8 = 0;
|
|
30
|
+
const exit_failed: u8 = 1;
|
|
31
|
+
const exit_app_running: u8 = 2;
|
|
32
|
+
const exit_cancelled: u8 = 3;
|
|
33
|
+
|
|
34
|
+
const Context = struct {
|
|
35
|
+
gpa: std.mem.Allocator,
|
|
36
|
+
io: std.Io,
|
|
37
|
+
silent: bool,
|
|
38
|
+
title: []const u8,
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
pub fn main(init: std.process.Init) u8 {
|
|
42
|
+
if (comptime builtin.os.tag != .windows) @compileError("the Native SDK setup program targets Windows");
|
|
43
|
+
var arena_state = std.heap.ArenaAllocator.init(init.gpa);
|
|
44
|
+
defer arena_state.deinit();
|
|
45
|
+
const arena = arena_state.allocator();
|
|
46
|
+
|
|
47
|
+
var argv: std.ArrayList([]const u8) = .empty;
|
|
48
|
+
var iterator = std.process.Args.Iterator.initAllocator(init.minimal.args, arena) catch return exit_failed;
|
|
49
|
+
while (iterator.next()) |argument| argv.append(arena, arena.dupe(u8, argument) catch return exit_failed) catch return exit_failed;
|
|
50
|
+
const args = if (argv.items.len > 0) argv.items[1..] else argv.items;
|
|
51
|
+
|
|
52
|
+
var context: Context = .{ .gpa = arena, .io = init.io, .silent = hasFlag(args, "--silent"), .title = "Setup" };
|
|
53
|
+
const own_path = ownPath(arena) catch return exit_failed;
|
|
54
|
+
|
|
55
|
+
if (args.len >= 1 and std.mem.eql(u8, args[0], "--apply-after")) {
|
|
56
|
+
return applyAfter(&context, own_path, args[1..]) catch |err| fail(&context, "The update could not be installed ({s}).", .{@errorName(err)});
|
|
57
|
+
}
|
|
58
|
+
if (args.len >= 1 and std.mem.eql(u8, args[0], "--uninstall-finish")) {
|
|
59
|
+
return uninstallFinish(&context, args[1..]) catch |err| fail(&context, "The app could not be removed completely ({s}).", .{@errorName(err)});
|
|
60
|
+
}
|
|
61
|
+
if (args.len >= 1 and std.mem.eql(u8, args[0], "--uninstall")) {
|
|
62
|
+
return uninstall(&context, own_path) catch |err| fail(&context, "The app could not be uninstalled ({s}).", .{@errorName(err)});
|
|
63
|
+
}
|
|
64
|
+
if (readSetupPayload(&context, own_path) catch |err| return fail(&context, "This installer could not be read ({s}).", .{@errorName(err)})) |setup| {
|
|
65
|
+
return install(&context, own_path, setup) catch |err| fail(&context, "The app could not be installed ({s}).", .{@errorName(err)});
|
|
66
|
+
}
|
|
67
|
+
return launch(&context, own_path) catch |err| fail(&context, "The app could not be started ({s}).", .{@errorName(err)});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
// Launcher
|
|
72
|
+
|
|
73
|
+
fn launch(context: *Context, own_path: []const u8) !u8 {
|
|
74
|
+
const root_path = std.fs.path.dirname(own_path) orelse return error.InvalidInstallRoot;
|
|
75
|
+
var root = try std.Io.Dir.cwd().openDir(context.io, root_path, .{});
|
|
76
|
+
defer root.close(context.io);
|
|
77
|
+
|
|
78
|
+
applyPending(context, root, root_path);
|
|
79
|
+
return startApp(context, root, root_path, commandLineTail(std.mem.span(win.GetCommandLineW())));
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/// The app asked to restart into a staged version: wait for it to exit,
|
|
83
|
+
/// switch, and start the new version.
|
|
84
|
+
fn applyAfter(context: *Context, own_path: []const u8, args: []const []const u8) !u8 {
|
|
85
|
+
if (args.len < 1) return error.InvalidArguments;
|
|
86
|
+
const pid = std.fmt.parseInt(win.DWORD, args[0], 10) catch return error.InvalidArguments;
|
|
87
|
+
if (win.OpenProcess(win.SYNCHRONIZE, 0, pid)) |handle| {
|
|
88
|
+
const exited = win.WaitForSingleObject(handle, 60_000) == win.WAIT_OBJECT_0;
|
|
89
|
+
_ = win.CloseHandle(handle);
|
|
90
|
+
// An app that did not quit keeps running, and its staged release
|
|
91
|
+
// waits for the next launch.
|
|
92
|
+
if (!exited) return exit_ok;
|
|
93
|
+
}
|
|
94
|
+
const root_path = std.fs.path.dirname(own_path) orelse return error.InvalidInstallRoot;
|
|
95
|
+
var root = try std.Io.Dir.cwd().openDir(context.io, root_path, .{});
|
|
96
|
+
defer root.close(context.io);
|
|
97
|
+
// Helper processes the app started (its services host) exit with it.
|
|
98
|
+
var waited: usize = 0;
|
|
99
|
+
while (waited < 40 and try appRunning(context, root_path)) : (waited += 1) win.Sleep(250);
|
|
100
|
+
applyPending(context, root, root_path);
|
|
101
|
+
return startApp(context, root, root_path, &.{});
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/// Moves a staged version into place while nothing runs from app\
|
|
105
|
+
/// (Windows would rename the directory under a running app), and records
|
|
106
|
+
/// the switch for the new version's updater to report.
|
|
107
|
+
fn applyPending(context: *Context, root: std.Io.Dir, root_path: []const u8) void {
|
|
108
|
+
const installed = readInstalledConfig(context, root) catch null;
|
|
109
|
+
// Launchers starting together (a restart and a click on the shortcut)
|
|
110
|
+
// take turns, so only one of them switches.
|
|
111
|
+
const lock = if (installed) |config| switchLock(context.gpa, config.bundle_id) else null;
|
|
112
|
+
defer if (lock) |handle| {
|
|
113
|
+
_ = win.ReleaseMutex(handle);
|
|
114
|
+
_ = win.CloseHandle(handle);
|
|
115
|
+
};
|
|
116
|
+
if (appRunning(context, root_path) catch true) return;
|
|
117
|
+
const before = if (installed) |config| config.version else "";
|
|
118
|
+
const outcome = install_switch.applyStaged(root, context.io) catch return;
|
|
119
|
+
if (outcome != .switched) return;
|
|
120
|
+
const after = (readInstalledConfig(context, root) catch return).version;
|
|
121
|
+
const record = std.fmt.allocPrint(context.gpa, "updated {s} {s}\n", .{ before, after }) catch return;
|
|
122
|
+
root.writeFile(context.io, .{ .sub_path = install_switch.update_result, .data = record }) catch {};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/// The per-app mutex a version switch holds, owned once this returns; null
|
|
126
|
+
/// when it cannot be created or waited for, and the switch goes ahead.
|
|
127
|
+
fn switchLock(allocator: std.mem.Allocator, bundle_id: []const u8) ?win.HANDLE {
|
|
128
|
+
const name = std.fmt.allocPrint(allocator, "Local\\native-sdk-switch-{s}", .{bundle_id}) catch return null;
|
|
129
|
+
const name_w = wide(allocator, name) catch return null;
|
|
130
|
+
const handle = win.CreateMutexW(null, 0, name_w) orelse return null;
|
|
131
|
+
const waited = win.WaitForSingleObject(handle, 30_000);
|
|
132
|
+
if (waited == win.WAIT_OBJECT_0 or waited == win.WAIT_ABANDONED) return handle;
|
|
133
|
+
_ = win.CloseHandle(handle);
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
fn startApp(context: *Context, root: std.Io.Dir, root_path: []const u8, arguments: []const u16) !u8 {
|
|
138
|
+
const config = try readInstalledConfig(context, root);
|
|
139
|
+
context.title = config.display_name;
|
|
140
|
+
const executable = try std.fs.path.join(context.gpa, &.{ root_path, install_switch.current, "bin", config.executable });
|
|
141
|
+
const process = try startProcess(context.gpa, executable, arguments);
|
|
142
|
+
_ = win.AllowSetForegroundWindow(process.dwProcessId);
|
|
143
|
+
_ = win.CloseHandle(process.hThread);
|
|
144
|
+
_ = win.CloseHandle(process.hProcess);
|
|
145
|
+
return exit_ok;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ---------------------------------------------------------------------------
|
|
149
|
+
// Setup
|
|
150
|
+
|
|
151
|
+
const Setup = struct {
|
|
152
|
+
trailer: payload.Trailer,
|
|
153
|
+
config_bytes: []const u8,
|
|
154
|
+
};
|
|
155
|
+
|
|
156
|
+
fn readSetupPayload(context: *Context, own_path: []const u8) !?Setup {
|
|
157
|
+
var file = try std.Io.Dir.cwd().openFile(context.io, own_path, .{});
|
|
158
|
+
defer file.close(context.io);
|
|
159
|
+
const size = (try file.stat(context.io)).size;
|
|
160
|
+
var buffer: [64 * 1024]u8 = undefined;
|
|
161
|
+
var reader = file.reader(context.io, &buffer);
|
|
162
|
+
|
|
163
|
+
var headers: [4096]u8 = undefined;
|
|
164
|
+
const header_len: usize = @intCast(@min(size, headers.len));
|
|
165
|
+
try reader.seekTo(0);
|
|
166
|
+
try reader.interface.readSliceAll(headers[0..header_len]);
|
|
167
|
+
const end = payload.dataEnd(headers[0..header_len], size);
|
|
168
|
+
const tail_len: usize = @intCast(@min(end, payload.trailer_len + 7));
|
|
169
|
+
var tail: [payload.trailer_len + 7]u8 = undefined;
|
|
170
|
+
try reader.seekTo(end - tail_len);
|
|
171
|
+
try reader.interface.readSliceAll(tail[0..tail_len]);
|
|
172
|
+
const trailer = payload.find(tail[0..tail_len], end) orelse return null;
|
|
173
|
+
if (trailer.config_size > install_config.max_config_bytes) return error.InvalidInstaller;
|
|
174
|
+
|
|
175
|
+
const config_bytes = try context.gpa.alloc(u8, @intCast(trailer.config_size));
|
|
176
|
+
try reader.seekTo(trailer.config_offset);
|
|
177
|
+
try reader.interface.readSliceAll(config_bytes);
|
|
178
|
+
var hasher = std.crypto.hash.sha2.Sha256.init(.{});
|
|
179
|
+
hasher.update(config_bytes);
|
|
180
|
+
var remaining = trailer.zip_size;
|
|
181
|
+
var chunk: [64 * 1024]u8 = undefined;
|
|
182
|
+
while (remaining > 0) {
|
|
183
|
+
const count: usize = @intCast(@min(remaining, chunk.len));
|
|
184
|
+
try reader.interface.readSliceAll(chunk[0..count]);
|
|
185
|
+
hasher.update(chunk[0..count]);
|
|
186
|
+
remaining -= count;
|
|
187
|
+
}
|
|
188
|
+
var digest: [32]u8 = undefined;
|
|
189
|
+
hasher.final(&digest);
|
|
190
|
+
if (!std.crypto.timing_safe.eql([32]u8, digest, trailer.sha256)) return error.InstallerDamaged;
|
|
191
|
+
return .{ .trailer = trailer, .config_bytes = config_bytes };
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
fn install(context: *Context, own_path: []const u8, setup: Setup) !u8 {
|
|
195
|
+
var parsed = try install_config.parse(context.gpa, setup.config_bytes);
|
|
196
|
+
defer parsed.deinit();
|
|
197
|
+
const config = parsed.value;
|
|
198
|
+
context.title = config.display_name;
|
|
199
|
+
|
|
200
|
+
var uninstall_key_buffer: [512]u8 = undefined;
|
|
201
|
+
const uninstall_key = try install_config.uninstallKey(&uninstall_key_buffer, config.bundle_id);
|
|
202
|
+
const root_path = try installRoot(context, config, uninstall_key);
|
|
203
|
+
try std.Io.Dir.cwd().createDirPath(context.io, root_path);
|
|
204
|
+
var root = try std.Io.Dir.cwd().openDir(context.io, root_path, .{});
|
|
205
|
+
defer root.close(context.io);
|
|
206
|
+
if (readInstalledConfig(context, root)) |installed| {
|
|
207
|
+
if (!std.mem.eql(u8, installed.bundle_id, config.bundle_id)) return error.FolderHoldsAnotherApp;
|
|
208
|
+
} else |_| {}
|
|
209
|
+
|
|
210
|
+
while (try appRunning(context, root_path)) {
|
|
211
|
+
if (context.silent) return exit_app_running;
|
|
212
|
+
const text = try std.fmt.allocPrint(context.gpa, "{s} is running. Close it, then choose Retry to continue.", .{config.display_name});
|
|
213
|
+
if (messageBox(context, text, win.MB_RETRYCANCEL | win.MB_ICONQUESTION) != win.IDRETRY) return exit_cancelled;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Stage the new version beside the current one, then switch.
|
|
217
|
+
try install_switch.recover(root, context.io);
|
|
218
|
+
try install_switch.clearStaged(root, context.io);
|
|
219
|
+
try extractImage(context, own_path, setup.trailer, root);
|
|
220
|
+
try install_switch.markStaged(root, context.io);
|
|
221
|
+
switch (try install_switch.applyStaged(root, context.io)) {
|
|
222
|
+
.switched => {},
|
|
223
|
+
.busy => return error.InstallFolderInUse,
|
|
224
|
+
.nothing_staged => return error.InstallImageMissing,
|
|
225
|
+
}
|
|
226
|
+
if (root.access(context.io, install_switch.previous, .{})) |_| {
|
|
227
|
+
root.deleteTree(context.io, install_switch.previous) catch {};
|
|
228
|
+
} else |_| {}
|
|
229
|
+
|
|
230
|
+
const launcher_path = try launcherPath(context.gpa, root_path, config.name);
|
|
231
|
+
try installLauncher(context, root_path, launcher_path);
|
|
232
|
+
const shortcut_path = try registerInstall(context, config, root, root_path, launcher_path, uninstall_key);
|
|
233
|
+
_ = shortcut_path;
|
|
234
|
+
|
|
235
|
+
if (!context.silent) {
|
|
236
|
+
const process = try startProcess(context.gpa, launcher_path, &.{});
|
|
237
|
+
_ = win.AllowSetForegroundWindow(process.dwProcessId);
|
|
238
|
+
_ = win.CloseHandle(process.hThread);
|
|
239
|
+
_ = win.CloseHandle(process.hProcess);
|
|
240
|
+
}
|
|
241
|
+
return exit_ok;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
/// Where the app installs: the folder its existing installation recorded,
|
|
245
|
+
/// or %LOCALAPPDATA%\Programs\<display name> for a first install.
|
|
246
|
+
fn installRoot(context: *Context, config: InstallConfig, uninstall_key: []const u8) ![]const u8 {
|
|
247
|
+
if (try queryRegistryString(context.gpa, uninstall_key, "InstallLocation")) |location| {
|
|
248
|
+
if (std.fs.path.isAbsolute(location)) {
|
|
249
|
+
if (std.Io.Dir.cwd().openDir(context.io, location, .{})) |dir_value| {
|
|
250
|
+
var dir = dir_value;
|
|
251
|
+
defer dir.close(context.io);
|
|
252
|
+
if (readInstalledConfig(context, dir)) |installed| {
|
|
253
|
+
if (std.mem.eql(u8, installed.bundle_id, config.bundle_id)) return location;
|
|
254
|
+
} else |_| {}
|
|
255
|
+
} else |_| {}
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
const local_app_data = try knownFolder(context.gpa, &win.FOLDERID_LocalAppData);
|
|
259
|
+
var folder_buffer: [install_config.max_text_bytes]u8 = undefined;
|
|
260
|
+
const folder = try install_config.installFolderName(&folder_buffer, config.display_name, config.bundle_id);
|
|
261
|
+
return std.fs.path.join(context.gpa, &.{ local_app_data, "Programs", folder });
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
fn extractImage(context: *Context, own_path: []const u8, trailer: payload.Trailer, root: std.Io.Dir) !void {
|
|
265
|
+
{
|
|
266
|
+
var source = try std.Io.Dir.cwd().openFile(context.io, own_path, .{});
|
|
267
|
+
defer source.close(context.io);
|
|
268
|
+
var read_buffer: [64 * 1024]u8 = undefined;
|
|
269
|
+
var reader = source.reader(context.io, &read_buffer);
|
|
270
|
+
try reader.seekTo(trailer.zip_offset);
|
|
271
|
+
var destination = try root.createFile(context.io, setup_zip_name, .{ .truncate = true });
|
|
272
|
+
defer destination.close(context.io);
|
|
273
|
+
var write_buffer: [64 * 1024]u8 = undefined;
|
|
274
|
+
var writer = destination.writer(context.io, &write_buffer);
|
|
275
|
+
var remaining = trailer.zip_size;
|
|
276
|
+
var chunk: [64 * 1024]u8 = undefined;
|
|
277
|
+
while (remaining > 0) {
|
|
278
|
+
const count: usize = @intCast(@min(remaining, chunk.len));
|
|
279
|
+
try reader.interface.readSliceAll(chunk[0..count]);
|
|
280
|
+
try writer.interface.writeAll(chunk[0..count]);
|
|
281
|
+
remaining -= count;
|
|
282
|
+
}
|
|
283
|
+
try writer.interface.flush();
|
|
284
|
+
}
|
|
285
|
+
defer root.deleteFile(context.io, setup_zip_name) catch {};
|
|
286
|
+
|
|
287
|
+
try root.createDirPath(context.io, install_switch.staged);
|
|
288
|
+
var staged = try root.openDir(context.io, install_switch.staged, .{});
|
|
289
|
+
defer staged.close(context.io);
|
|
290
|
+
var archive = try root.openFile(context.io, setup_zip_name, .{});
|
|
291
|
+
defer archive.close(context.io);
|
|
292
|
+
var buffer: [64 * 1024]u8 = undefined;
|
|
293
|
+
var reader = archive.reader(context.io, &buffer);
|
|
294
|
+
try std.zip.extract(staged, &reader, .{});
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/// Copies the launcher the image carries to the stable <root>\<name>.exe.
|
|
298
|
+
fn installLauncher(context: *Context, root_path: []const u8, launcher_path: []const u8) !void {
|
|
299
|
+
const source = try std.fs.path.join(context.gpa, &.{ root_path, install_switch.current, launcher_file_in_image });
|
|
300
|
+
const source_w = try wide(context.gpa, source);
|
|
301
|
+
const target_w = try wide(context.gpa, launcher_path);
|
|
302
|
+
var attempt: usize = 0;
|
|
303
|
+
while (win.CopyFileW(source_w, target_w, 0) == 0) : (attempt += 1) {
|
|
304
|
+
if (attempt == 20) return error.LauncherInUse;
|
|
305
|
+
win.Sleep(250);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/// Writes the shortcut, the uninstall entry and the app's registry values.
|
|
310
|
+
/// Returns the shortcut path.
|
|
311
|
+
fn registerInstall(context: *Context, config: InstallConfig, root: std.Io.Dir, root_path: []const u8, launcher_path: []const u8, uninstall_key: []const u8) ![]const u8 {
|
|
312
|
+
const programs = try knownFolder(context.gpa, &win.FOLDERID_Programs);
|
|
313
|
+
var folder_buffer: [install_config.max_text_bytes]u8 = undefined;
|
|
314
|
+
const shortcut_name = try install_config.installFolderName(&folder_buffer, config.display_name, config.bundle_id);
|
|
315
|
+
const shortcut_path = try std.fmt.allocPrint(context.gpa, "{s}\\{s}.lnk", .{ programs, shortcut_name });
|
|
316
|
+
if (try queryRegistryString(context.gpa, uninstall_key, "NativeSdkShortcut")) |previous| {
|
|
317
|
+
if (!std.ascii.eqlIgnoreCase(previous, shortcut_path)) deleteFileIfPresent(context.gpa, previous);
|
|
318
|
+
}
|
|
319
|
+
const icon_path = if (config.icon.len > 0)
|
|
320
|
+
try std.fs.path.join(context.gpa, &.{ root_path, install_switch.current, config.icon })
|
|
321
|
+
else
|
|
322
|
+
launcher_path;
|
|
323
|
+
try createShortcut(context.gpa, shortcut_path, launcher_path, root_path, icon_path, config.display_name, config.bundle_id);
|
|
324
|
+
|
|
325
|
+
const uninstall_command = try std.fmt.allocPrint(context.gpa, "\"{s}\" --uninstall", .{launcher_path});
|
|
326
|
+
const quiet_command = try std.fmt.allocPrint(context.gpa, "\"{s}\" --uninstall --silent", .{launcher_path});
|
|
327
|
+
try setRegistryString(context.gpa, uninstall_key, "DisplayName", config.display_name);
|
|
328
|
+
try setRegistryString(context.gpa, uninstall_key, "DisplayVersion", config.version);
|
|
329
|
+
if (config.publisher.len > 0) try setRegistryString(context.gpa, uninstall_key, "Publisher", config.publisher);
|
|
330
|
+
try setRegistryString(context.gpa, uninstall_key, "DisplayIcon", icon_path);
|
|
331
|
+
try setRegistryString(context.gpa, uninstall_key, "InstallLocation", root_path);
|
|
332
|
+
try setRegistryString(context.gpa, uninstall_key, "UninstallString", uninstall_command);
|
|
333
|
+
try setRegistryString(context.gpa, uninstall_key, "QuietUninstallString", quiet_command);
|
|
334
|
+
try setRegistryString(context.gpa, uninstall_key, "NativeSdkShortcut", shortcut_path);
|
|
335
|
+
try setRegistryDword(context.gpa, uninstall_key, "NoModify", 1);
|
|
336
|
+
try setRegistryDword(context.gpa, uninstall_key, "NoRepair", 1);
|
|
337
|
+
try setRegistryDword(context.gpa, uninstall_key, "EstimatedSize", @intCast(@min(try directorySizeKiB(context, root), std.math.maxInt(u32))));
|
|
338
|
+
|
|
339
|
+
for (config.registry) |entry| {
|
|
340
|
+
const with_launcher = try std.mem.replaceOwned(u8, context.gpa, entry.value, install_config.launcher_placeholder, launcher_path);
|
|
341
|
+
const value = try std.mem.replaceOwned(u8, context.gpa, with_launcher, install_config.install_placeholder, root_path);
|
|
342
|
+
try setRegistryString(context.gpa, entry.key, entry.name, value);
|
|
343
|
+
}
|
|
344
|
+
if (config.registry.len > 0) win.SHChangeNotify(win.SHCNE_ASSOCCHANGED, win.SHCNF_IDLIST, null, null);
|
|
345
|
+
return shortcut_path;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
fn directorySizeKiB(context: *Context, root: std.Io.Dir) !u64 {
|
|
349
|
+
var dir = try root.openDir(context.io, install_switch.current, .{ .iterate = true });
|
|
350
|
+
defer dir.close(context.io);
|
|
351
|
+
var walker = try dir.walk(context.gpa);
|
|
352
|
+
defer walker.deinit();
|
|
353
|
+
var total: u64 = 0;
|
|
354
|
+
while (try walker.next(context.io)) |entry| {
|
|
355
|
+
if (entry.kind != .file) continue;
|
|
356
|
+
total += (dir.statFile(context.io, entry.path, .{}) catch continue).size;
|
|
357
|
+
}
|
|
358
|
+
return (total + 1023) / 1024;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// ---------------------------------------------------------------------------
|
|
362
|
+
// Uninstall
|
|
363
|
+
|
|
364
|
+
fn uninstall(context: *Context, own_path: []const u8) !u8 {
|
|
365
|
+
const root_path = std.fs.path.dirname(own_path) orelse return error.InvalidInstallRoot;
|
|
366
|
+
var root = try std.Io.Dir.cwd().openDir(context.io, root_path, .{});
|
|
367
|
+
defer root.close(context.io);
|
|
368
|
+
const config = try readInstalledConfig(context, root);
|
|
369
|
+
context.title = config.display_name;
|
|
370
|
+
|
|
371
|
+
if (!context.silent) {
|
|
372
|
+
const text = try std.fmt.allocPrint(context.gpa, "Remove {s} from this computer? Your documents and app data are kept.", .{config.display_name});
|
|
373
|
+
if (messageBox(context, text, win.MB_YESNO | win.MB_ICONQUESTION) != win.IDYES) return exit_cancelled;
|
|
374
|
+
}
|
|
375
|
+
while (try appRunning(context, root_path)) {
|
|
376
|
+
if (context.silent) return exit_app_running;
|
|
377
|
+
const text = try std.fmt.allocPrint(context.gpa, "{s} is running. Close it, then choose Retry to continue.", .{config.display_name});
|
|
378
|
+
if (messageBox(context, text, win.MB_RETRYCANCEL | win.MB_ICONQUESTION) != win.IDRETRY) return exit_cancelled;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
// This program runs from the folder it removes, so a copy in the
|
|
382
|
+
// temporary directory finishes the job once this process exits.
|
|
383
|
+
var temp_buffer: [win.MAX_PATH_WIDE]u16 = undefined;
|
|
384
|
+
const temp_len = win.GetTempPathW(temp_buffer.len, &temp_buffer);
|
|
385
|
+
if (temp_len == 0 or temp_len >= temp_buffer.len) return error.NoTempDirectory;
|
|
386
|
+
const temp_dir = try std.unicode.wtf16LeToWtf8Alloc(context.gpa, temp_buffer[0..temp_len]);
|
|
387
|
+
const helper = try std.fmt.allocPrint(context.gpa, "{s}{s}-uninstall-{d}.exe", .{ temp_dir, config.bundle_id, win.GetCurrentProcessId() });
|
|
388
|
+
if (win.CopyFileW(try wide(context.gpa, own_path), try wide(context.gpa, helper), 0) == 0) return error.UninstallHelperCopyFailed;
|
|
389
|
+
const helper_args = try std.fmt.allocPrint(context.gpa, "--uninstall-finish \"{s}\" {d}{s}", .{ root_path, win.GetCurrentProcessId(), if (context.silent) " --silent" else "" });
|
|
390
|
+
const process = try startProcess(context.gpa, helper, try wideSlice(context.gpa, helper_args));
|
|
391
|
+
_ = win.CloseHandle(process.hThread);
|
|
392
|
+
_ = win.CloseHandle(process.hProcess);
|
|
393
|
+
return exit_ok;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
fn uninstallFinish(context: *Context, args: []const []const u8) !u8 {
|
|
397
|
+
if (args.len < 2) return error.InvalidArguments;
|
|
398
|
+
const root_path = args[0];
|
|
399
|
+
const parent = std.fmt.parseInt(win.DWORD, args[1], 10) catch return error.InvalidArguments;
|
|
400
|
+
if (win.OpenProcess(win.SYNCHRONIZE, 0, parent)) |handle| {
|
|
401
|
+
_ = win.WaitForSingleObject(handle, 10_000);
|
|
402
|
+
_ = win.CloseHandle(handle);
|
|
403
|
+
}
|
|
404
|
+
if (!std.fs.path.isAbsolute(root_path)) return error.InvalidInstallRoot;
|
|
405
|
+
var root = try std.Io.Dir.cwd().openDir(context.io, root_path, .{});
|
|
406
|
+
const config = readInstalledConfig(context, root) catch |err| {
|
|
407
|
+
root.close(context.io);
|
|
408
|
+
return err;
|
|
409
|
+
};
|
|
410
|
+
root.close(context.io);
|
|
411
|
+
context.title = config.display_name;
|
|
412
|
+
|
|
413
|
+
var uninstall_key_buffer: [512]u8 = undefined;
|
|
414
|
+
const uninstall_key = try install_config.uninstallKey(&uninstall_key_buffer, config.bundle_id);
|
|
415
|
+
if (try queryRegistryString(context.gpa, uninstall_key, "NativeSdkShortcut")) |shortcut| deleteFileIfPresent(context.gpa, shortcut);
|
|
416
|
+
for (config.registry_cleanup) |entry| {
|
|
417
|
+
if (entry.name) |name| {
|
|
418
|
+
if (entry.if_value) |expected| {
|
|
419
|
+
if (try queryRegistryString(context.gpa, entry.key, name)) |current| {
|
|
420
|
+
if (std.mem.eql(u8, current, expected)) deleteRegistryValue(context.gpa, entry.key, name);
|
|
421
|
+
}
|
|
422
|
+
} else deleteRegistryValue(context.gpa, entry.key, name);
|
|
423
|
+
} else deleteRegistryTree(context.gpa, entry.key);
|
|
424
|
+
}
|
|
425
|
+
if (config.registry_cleanup.len > 0) win.SHChangeNotify(win.SHCNE_ASSOCCHANGED, win.SHCNF_IDLIST, null, null);
|
|
426
|
+
deleteRegistryTree(context.gpa, uninstall_key);
|
|
427
|
+
|
|
428
|
+
var attempt: usize = 0;
|
|
429
|
+
while (true) : (attempt += 1) {
|
|
430
|
+
std.Io.Dir.cwd().deleteTree(context.io, root_path) catch |err| {
|
|
431
|
+
if (attempt == 40) return err;
|
|
432
|
+
win.Sleep(250);
|
|
433
|
+
continue;
|
|
434
|
+
};
|
|
435
|
+
break;
|
|
436
|
+
}
|
|
437
|
+
scheduleSelfDelete(context.gpa);
|
|
438
|
+
return exit_ok;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/// The uninstall helper runs from the temporary directory and cannot
|
|
442
|
+
/// delete its own image while it runs; a hidden command prompt removes it
|
|
443
|
+
/// a moment after this process exits.
|
|
444
|
+
fn scheduleSelfDelete(allocator: std.mem.Allocator) void {
|
|
445
|
+
const own_path = ownPath(allocator) catch return;
|
|
446
|
+
var system_buffer: [win.MAX_PATH_WIDE]u16 = undefined;
|
|
447
|
+
const system_len = win.GetSystemDirectoryW(&system_buffer, system_buffer.len);
|
|
448
|
+
if (system_len == 0 or system_len >= system_buffer.len) return;
|
|
449
|
+
const system_dir = std.unicode.wtf16LeToWtf8Alloc(allocator, system_buffer[0..system_len]) catch return;
|
|
450
|
+
const shell = std.fmt.allocPrint(allocator, "{s}\\cmd.exe", .{system_dir}) catch return;
|
|
451
|
+
const command = std.fmt.allocPrint(allocator, "/d /c \"ping -n 3 127.0.0.1 >nul & del /f /q \"{s}\"\"", .{own_path}) catch return;
|
|
452
|
+
const arguments = wideSlice(allocator, command) catch return;
|
|
453
|
+
const process = startProcessWithFlags(allocator, shell, arguments, win.CREATE_NO_WINDOW) catch return;
|
|
454
|
+
_ = win.CloseHandle(process.hThread);
|
|
455
|
+
_ = win.CloseHandle(process.hProcess);
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// ---------------------------------------------------------------------------
|
|
459
|
+
// Shared
|
|
460
|
+
|
|
461
|
+
fn readInstalledConfig(context: *Context, root: std.Io.Dir) !InstallConfig {
|
|
462
|
+
const bytes = try root.readFileAlloc(context.io, install_switch.current ++ "/" ++ install_config.file_name, context.gpa, .limited(install_config.max_config_bytes));
|
|
463
|
+
const parsed = try install_config.parse(context.gpa, bytes);
|
|
464
|
+
return parsed.value;
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
fn launcherPath(allocator: std.mem.Allocator, root_path: []const u8, name: []const u8) ![]const u8 {
|
|
468
|
+
return std.fmt.allocPrint(allocator, "{s}\\{s}.exe", .{ root_path, name });
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/// Whether any process runs an executable from <root>\app\.
|
|
472
|
+
fn appRunning(context: *Context, root_path: []const u8) !bool {
|
|
473
|
+
const prefix = try std.fmt.allocPrint(context.gpa, "{s}\\{s}\\", .{ root_path, install_switch.current });
|
|
474
|
+
const prefix_w = try std.unicode.wtf8ToWtf16LeAlloc(context.gpa, prefix);
|
|
475
|
+
var pids: [4096]win.DWORD = undefined;
|
|
476
|
+
var needed: win.DWORD = 0;
|
|
477
|
+
if (win.K32EnumProcesses(&pids, @sizeOf(@TypeOf(pids)), &needed) == 0) return false;
|
|
478
|
+
const count = needed / @sizeOf(win.DWORD);
|
|
479
|
+
const self = win.GetCurrentProcessId();
|
|
480
|
+
for (pids[0..count]) |pid| {
|
|
481
|
+
if (pid == 0 or pid == self) continue;
|
|
482
|
+
const handle = win.OpenProcess(win.PROCESS_QUERY_LIMITED_INFORMATION, 0, pid) orelse continue;
|
|
483
|
+
defer _ = win.CloseHandle(handle);
|
|
484
|
+
var image: [win.MAX_PATH_WIDE]u16 = undefined;
|
|
485
|
+
var image_len: win.DWORD = image.len;
|
|
486
|
+
if (win.QueryFullProcessImageNameW(handle, 0, &image, &image_len) == 0) continue;
|
|
487
|
+
if (startsWithIgnoreCaseWide(image[0..image_len], prefix_w)) return true;
|
|
488
|
+
}
|
|
489
|
+
return false;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
fn startsWithIgnoreCaseWide(value: []const u16, prefix: []const u16) bool {
|
|
493
|
+
if (value.len < prefix.len) return false;
|
|
494
|
+
for (value[0..prefix.len], prefix) |a, b| {
|
|
495
|
+
if (lowerWide(a) != lowerWide(b)) return false;
|
|
496
|
+
}
|
|
497
|
+
return true;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
fn lowerWide(unit: u16) u16 {
|
|
501
|
+
return if (unit >= 'A' and unit <= 'Z') unit + 32 else unit;
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
/// The arguments part of a Windows command line: everything after the
|
|
505
|
+
/// program name, as the shell quoted it.
|
|
506
|
+
pub fn commandLineTail(line: []const u16) []const u16 {
|
|
507
|
+
var index: usize = 0;
|
|
508
|
+
if (index < line.len and line[index] == '"') {
|
|
509
|
+
index += 1;
|
|
510
|
+
while (index < line.len and line[index] != '"') index += 1;
|
|
511
|
+
if (index < line.len) index += 1;
|
|
512
|
+
} else {
|
|
513
|
+
while (index < line.len and line[index] != ' ' and line[index] != '\t') index += 1;
|
|
514
|
+
}
|
|
515
|
+
while (index < line.len and (line[index] == ' ' or line[index] == '\t')) index += 1;
|
|
516
|
+
return line[index..];
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
fn startProcess(allocator: std.mem.Allocator, executable: []const u8, arguments: []const u16) !win.PROCESS_INFORMATION {
|
|
520
|
+
return startProcessWithFlags(allocator, executable, arguments, 0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
fn startProcessWithFlags(allocator: std.mem.Allocator, executable: []const u8, arguments: []const u16, flags: win.DWORD) !win.PROCESS_INFORMATION {
|
|
524
|
+
const executable_w = try std.unicode.wtf8ToWtf16LeAllocZ(allocator, executable);
|
|
525
|
+
var command: std.ArrayList(u16) = .empty;
|
|
526
|
+
try command.append(allocator, '"');
|
|
527
|
+
try command.appendSlice(allocator, executable_w);
|
|
528
|
+
try command.append(allocator, '"');
|
|
529
|
+
if (arguments.len > 0) {
|
|
530
|
+
try command.append(allocator, ' ');
|
|
531
|
+
try command.appendSlice(allocator, arguments);
|
|
532
|
+
}
|
|
533
|
+
try command.append(allocator, 0);
|
|
534
|
+
const command_line: [:0]u16 = command.items[0 .. command.items.len - 1 :0];
|
|
535
|
+
var startup: win.STARTUPINFOW = .{};
|
|
536
|
+
var process: win.PROCESS_INFORMATION = undefined;
|
|
537
|
+
if (win.CreateProcessW(executable_w.ptr, command_line.ptr, null, null, 0, win.CREATE_UNICODE_ENVIRONMENT | flags, null, null, &startup, &process) == 0) return error.ProcessStartFailed;
|
|
538
|
+
return process;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
fn ownPath(allocator: std.mem.Allocator) ![]const u8 {
|
|
542
|
+
var buffer: [win.MAX_PATH_WIDE]u16 = undefined;
|
|
543
|
+
const length = win.GetModuleFileNameW(null, &buffer, buffer.len);
|
|
544
|
+
if (length == 0 or length >= buffer.len) return error.OwnPathUnavailable;
|
|
545
|
+
return std.unicode.wtf16LeToWtf8Alloc(allocator, buffer[0..length]);
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
fn knownFolder(allocator: std.mem.Allocator, id: *const win.GUID) ![]const u8 {
|
|
549
|
+
var path: ?[*:0]u16 = null;
|
|
550
|
+
if (win.SHGetKnownFolderPath(id, 0, null, &path) < 0) return error.KnownFolderUnavailable;
|
|
551
|
+
defer win.CoTaskMemFree(path);
|
|
552
|
+
return std.unicode.wtf16LeToWtf8Alloc(allocator, std.mem.span(path.?));
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
/// A NUL-terminated wide copy of a WTF-8 string, for Win32 parameters.
|
|
556
|
+
fn wide(allocator: std.mem.Allocator, value: []const u8) ![*:0]const u16 {
|
|
557
|
+
return (try std.unicode.wtf8ToWtf16LeAllocZ(allocator, value)).ptr;
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
fn wideSlice(allocator: std.mem.Allocator, value: []const u8) ![]u16 {
|
|
561
|
+
return std.unicode.wtf8ToWtf16LeAlloc(allocator, value);
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
fn deleteFileIfPresent(allocator: std.mem.Allocator, path: []const u8) void {
|
|
565
|
+
const path_w = wide(allocator, path) catch return;
|
|
566
|
+
_ = win.DeleteFileW(path_w);
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
fn setRegistryString(allocator: std.mem.Allocator, key_path: []const u8, name: []const u8, value: []const u8) !void {
|
|
570
|
+
var key: win.HKEY = undefined;
|
|
571
|
+
if (win.RegCreateKeyExW(win.HKEY_CURRENT_USER, try wide(allocator, key_path), 0, null, 0, win.KEY_WRITE, null, &key, null) != win.ERROR_SUCCESS) return error.RegistryWriteFailed;
|
|
572
|
+
defer _ = win.RegCloseKey(key);
|
|
573
|
+
const value_w = try std.unicode.wtf8ToWtf16LeAllocZ(allocator, value);
|
|
574
|
+
const bytes = std.mem.sliceAsBytes(value_w[0 .. value_w.len + 1]);
|
|
575
|
+
const name_w: ?[*:0]const u16 = if (name.len == 0) null else try wide(allocator, name);
|
|
576
|
+
if (win.RegSetValueExW(key, name_w, 0, win.REG_SZ, bytes.ptr, @intCast(bytes.len)) != win.ERROR_SUCCESS) return error.RegistryWriteFailed;
|
|
577
|
+
}
|
|
578
|
+
|
|
579
|
+
fn setRegistryDword(allocator: std.mem.Allocator, key_path: []const u8, name: []const u8, value: u32) !void {
|
|
580
|
+
var key: win.HKEY = undefined;
|
|
581
|
+
if (win.RegCreateKeyExW(win.HKEY_CURRENT_USER, try wide(allocator, key_path), 0, null, 0, win.KEY_WRITE, null, &key, null) != win.ERROR_SUCCESS) return error.RegistryWriteFailed;
|
|
582
|
+
defer _ = win.RegCloseKey(key);
|
|
583
|
+
const bytes = std.mem.asBytes(&value);
|
|
584
|
+
if (win.RegSetValueExW(key, try wide(allocator, name), 0, win.REG_DWORD, bytes.ptr, bytes.len) != win.ERROR_SUCCESS) return error.RegistryWriteFailed;
|
|
585
|
+
}
|
|
586
|
+
|
|
587
|
+
fn queryRegistryString(allocator: std.mem.Allocator, key_path: []const u8, name: []const u8) !?[]const u8 {
|
|
588
|
+
var key: win.HKEY = undefined;
|
|
589
|
+
if (win.RegOpenKeyExW(win.HKEY_CURRENT_USER, try wide(allocator, key_path), 0, win.KEY_READ, &key) != win.ERROR_SUCCESS) return null;
|
|
590
|
+
defer _ = win.RegCloseKey(key);
|
|
591
|
+
const name_w: ?[*:0]const u16 = if (name.len == 0) null else try wide(allocator, name);
|
|
592
|
+
var kind: win.DWORD = 0;
|
|
593
|
+
var size: win.DWORD = 0;
|
|
594
|
+
if (win.RegQueryValueExW(key, name_w, null, &kind, null, &size) != win.ERROR_SUCCESS or kind != win.REG_SZ) return null;
|
|
595
|
+
const units = try allocator.alloc(u16, size / 2 + 1);
|
|
596
|
+
var read_size: win.DWORD = @intCast(units.len * 2);
|
|
597
|
+
if (win.RegQueryValueExW(key, name_w, null, &kind, std.mem.sliceAsBytes(units).ptr, &read_size) != win.ERROR_SUCCESS) return null;
|
|
598
|
+
var length: usize = read_size / 2;
|
|
599
|
+
while (length > 0 and units[length - 1] == 0) length -= 1;
|
|
600
|
+
return try std.unicode.wtf16LeToWtf8Alloc(allocator, units[0..length]);
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
/// Deletes one value, and then the key itself when nothing is left in it
|
|
604
|
+
/// (an extension key the app created only for its association).
|
|
605
|
+
fn deleteRegistryValue(allocator: std.mem.Allocator, key_path: []const u8, name: []const u8) void {
|
|
606
|
+
const key_w = wide(allocator, key_path) catch return;
|
|
607
|
+
var key: win.HKEY = undefined;
|
|
608
|
+
if (win.RegOpenKeyExW(win.HKEY_CURRENT_USER, key_w, 0, win.KEY_READ | win.KEY_WRITE, &key) != win.ERROR_SUCCESS) return;
|
|
609
|
+
const name_w: ?[*:0]const u16 = if (name.len == 0) null else (wide(allocator, name) catch {
|
|
610
|
+
_ = win.RegCloseKey(key);
|
|
611
|
+
return;
|
|
612
|
+
});
|
|
613
|
+
_ = win.RegDeleteValueW(key, name_w);
|
|
614
|
+
var subkeys: win.DWORD = 1;
|
|
615
|
+
var values: win.DWORD = 1;
|
|
616
|
+
const queried = win.RegQueryInfoKeyW(key, null, null, null, &subkeys, null, null, &values, null, null, null, null);
|
|
617
|
+
_ = win.RegCloseKey(key);
|
|
618
|
+
if (queried == win.ERROR_SUCCESS and subkeys == 0 and values == 0) _ = win.RegDeleteKeyW(win.HKEY_CURRENT_USER, key_w);
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
fn deleteRegistryTree(allocator: std.mem.Allocator, key_path: []const u8) void {
|
|
622
|
+
const key_w = wide(allocator, key_path) catch return;
|
|
623
|
+
_ = win.RegDeleteTreeW(win.HKEY_CURRENT_USER, key_w);
|
|
624
|
+
}
|
|
625
|
+
|
|
626
|
+
fn createShortcut(allocator: std.mem.Allocator, link_path: []const u8, target: []const u8, working_directory: []const u8, icon: []const u8, description: []const u8, app_user_model_id: []const u8) !void {
|
|
627
|
+
const initialized = win.CoInitializeEx(null, win.COINIT_APARTMENTTHREADED);
|
|
628
|
+
defer if (initialized >= 0) win.CoUninitialize();
|
|
629
|
+
var link_object: ?*anyopaque = null;
|
|
630
|
+
if (win.CoCreateInstance(&win.CLSID_ShellLink, null, win.CLSCTX_INPROC_SERVER, &win.IID_IShellLinkW, &link_object) < 0) return error.ShortcutFailed;
|
|
631
|
+
const link: *win.IShellLinkW = @ptrCast(@alignCast(link_object.?));
|
|
632
|
+
defer _ = link.vtable.unknown.Release(link);
|
|
633
|
+
if (link.vtable.SetPath(link, try wide(allocator, target)) < 0) return error.ShortcutFailed;
|
|
634
|
+
_ = link.vtable.SetWorkingDirectory(link, try wide(allocator, working_directory));
|
|
635
|
+
_ = link.vtable.SetIconLocation(link, try wide(allocator, icon), 0);
|
|
636
|
+
_ = link.vtable.SetDescription(link, try wide(allocator, description));
|
|
637
|
+
|
|
638
|
+
// The shortcut and the running app share an AppUserModelID, so the
|
|
639
|
+
// taskbar groups the app's windows under this shortcut and pins it.
|
|
640
|
+
var store_object: ?*anyopaque = null;
|
|
641
|
+
if (link.vtable.unknown.QueryInterface(link, &win.IID_IPropertyStore, &store_object) >= 0) {
|
|
642
|
+
const store: *win.IPropertyStore = @ptrCast(@alignCast(store_object.?));
|
|
643
|
+
defer _ = store.vtable.unknown.Release(store);
|
|
644
|
+
const id_w = try wide(allocator, app_user_model_id);
|
|
645
|
+
var value: win.PROPVARIANT = .{ .vt = win.VT_LPWSTR };
|
|
646
|
+
value.value[0] = @intFromPtr(id_w);
|
|
647
|
+
if (store.vtable.SetValue(store, &win.PKEY_AppUserModel_ID, &value) >= 0) _ = store.vtable.Commit(store);
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
var persist_object: ?*anyopaque = null;
|
|
651
|
+
if (link.vtable.unknown.QueryInterface(link, &win.IID_IPersistFile, &persist_object) < 0) return error.ShortcutFailed;
|
|
652
|
+
const persist: *win.IPersistFile = @ptrCast(@alignCast(persist_object.?));
|
|
653
|
+
defer _ = persist.vtable.unknown.Release(persist);
|
|
654
|
+
if (persist.vtable.Save(persist, try wide(allocator, link_path), 1) < 0) return error.ShortcutFailed;
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
fn messageBox(context: *Context, text: []const u8, flags: u32) i32 {
|
|
658
|
+
const text_w = wide(context.gpa, text) catch return 0;
|
|
659
|
+
const title_w = wide(context.gpa, context.title) catch return 0;
|
|
660
|
+
return win.MessageBoxW(null, text_w, title_w, flags | win.MB_SETFOREGROUND);
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
fn fail(context: *Context, comptime format: []const u8, args: anytype) u8 {
|
|
664
|
+
if (!context.silent) {
|
|
665
|
+
const text = std.fmt.allocPrint(context.gpa, format, args) catch return exit_failed;
|
|
666
|
+
_ = messageBox(context, text, win.MB_OK | win.MB_ICONERROR);
|
|
667
|
+
}
|
|
668
|
+
return exit_failed;
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
fn hasFlag(args: []const []const u8, flag: []const u8) bool {
|
|
672
|
+
for (args) |argument| {
|
|
673
|
+
if (std.mem.eql(u8, argument, flag)) return true;
|
|
674
|
+
}
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
test "the command-line tail keeps the arguments as the shell quoted them" {
|
|
679
|
+
const line = std.unicode.utf8ToUtf16LeStringLiteral("\"C:\\Program Files\\Demo\\demo.exe\" \"C:\\Docs\\a b.txt\" --flag");
|
|
680
|
+
const tail = commandLineTail(line);
|
|
681
|
+
try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("\"C:\\Docs\\a b.txt\" --flag"), tail);
|
|
682
|
+
try std.testing.expectEqual(@as(usize, 0), commandLineTail(std.unicode.utf8ToUtf16LeStringLiteral("demo.exe")).len);
|
|
683
|
+
try std.testing.expectEqualSlices(u16, std.unicode.utf8ToUtf16LeStringLiteral("x"), commandLineTail(std.unicode.utf8ToUtf16LeStringLiteral("demo.exe\tx")));
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
test "process image paths match the install folder case-insensitively" {
|
|
687
|
+
const prefix = std.unicode.utf8ToUtf16LeStringLiteral("C:\\Users\\Me\\AppData\\Local\\Programs\\Demo\\app\\");
|
|
688
|
+
try std.testing.expect(startsWithIgnoreCaseWide(std.unicode.utf8ToUtf16LeStringLiteral("c:\\users\\me\\appdata\\local\\programs\\demo\\APP\\bin\\demo.exe"), prefix));
|
|
689
|
+
try std.testing.expect(!startsWithIgnoreCaseWide(std.unicode.utf8ToUtf16LeStringLiteral("C:\\Users\\Me\\AppData\\Local\\Programs\\Demo\\app.prev\\bin\\demo.exe"), prefix));
|
|
690
|
+
}
|