App-Ariza.git | runner/src/ | main_win32.c
/* The Windows shell around the portable core: everything that talks to
* the operating system, and nothing that does not.
*
* What it does, in order, is the .cmd launcher's script read out loud:
*
* 1. asks Windows where this executable is, and takes the directory
* above bin/ as the bundle root — so the bundle stays relocatable
* and nothing absolute is baked in anywhere;
* 2. reads bin/<exec>.ariza beside itself for the four facts that are
* per-bundle rather than per-runner;
* 3. refuses, readably, if the interpreter is not where it should be —
* the signature of a half-unpacked archive;
* 4. applies the sidecar's environment directives in order, without
* knowing what any of them mean — `set`, `unset`, `prepend-path`,
* with `{root}` expanded to the root it found in step 1;
* 5. prints the first-run notice once, marked by a file under
* %LOCALAPPDATA%\<exec>\ — the only thing any bundle writes outside
* its own directory;
* 6. starts raku.exe on the app's script with the caller's arguments
* passed through verbatim, waits, and exits with the child's code.
*
* No cmd.exe is involved at any point, which is the reason this exists:
* a batch trampoline re-parses `%*` (mangling '^', '%x%', '!x!' and
* quote-heavy arguments), and script-execution policy — AppLocker, SRP,
* a locked-down ExecutionPolicy — blocks .cmd and .ps1 files however
* they are invoked, while leaving an .exe alone. The scripts stay in
* the bundle as readable alternatives; this is the documented one.
*
* A failure here ends the process, so nothing is freed on the way out:
* the only thing a launcher can usefully do with a broken bundle is say
* so and stop, and unwinding first would buy nobody anything. */
#ifdef _WIN32
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include <bcrypt.h>
#include <stdlib.h>
#include "ariza_runner.h"
/* What the .cmd exits with when it cannot find the interpreter. */
#define ARZ_EXIT_LAUNCH_FAILURE 1
#define ARZ_EXIT_UPDATE_HANDOFF 75
#define ARZ_ENV_UPDATES_ENABLED L"ARIZA_UPDATES_ENABLED"
#define ARZ_ENV_HANDOFF L"ARIZA_UPDATE_HANDOFF"
#define ARZ_ENV_NONCE L"ARIZA_UPDATE_NONCE"
#define ARZ_ENV_RELAUNCHED L"ARIZA_UPDATE_RELAUNCHED"
static void write_err(const wchar_t *text)
{
HANDLE h = GetStdHandle(STD_ERROR_HANDLE);
DWORD mode;
DWORD written;
int bytes;
char *utf8;
if (h == NULL || h == INVALID_HANDLE_VALUE || text == NULL)
return;
/* A console takes UTF-16 directly; a pipe or a file takes bytes,
* and UTF-8 is the only encoding worth writing them in. Asking
* GetConsoleMode which one this is costs nothing and is the
* difference between a readable message and mojibake in a
* redirected log. */
if (GetConsoleMode(h, &mode)) {
WriteConsoleW(h, text, (DWORD)arz_len(text), &written, NULL);
return;
}
bytes = WideCharToMultiByte(CP_UTF8, 0, text, -1, NULL, 0, NULL, NULL);
if (bytes <= 1)
return;
utf8 = malloc((size_t)bytes);
if (utf8 == NULL)
return;
if (WideCharToMultiByte(CP_UTF8, 0, text, -1, utf8, bytes, NULL, NULL) > 0)
WriteFile(h, utf8, (DWORD)(bytes - 1), &written, NULL);
free(utf8);
}
static void err_line(const wchar_t *a, const wchar_t *b, const wchar_t *c)
{
wchar_t *joined = arz_cat(a, b, c);
wchar_t *line;
if (joined == NULL)
return;
line = arz_cat(joined, L"\r\n", NULL);
free(joined);
if (line == NULL)
return;
write_err(line);
free(line);
}
static void fatal(const wchar_t *a, const wchar_t *b, const wchar_t *c)
{
err_line(a, b, c);
ExitProcess(ARZ_EXIT_LAUNCH_FAILURE);
}
static void fatal_oom(void)
{
fatal(L"ariza: out of memory starting the application.", NULL, NULL);
}
/* The key name, wide, for the one message that needs it. The core
* returns an enum precisely so this conversion is a switch here rather
* than an encoding decision there. */
static const wchar_t *key_wname(arz_key key)
{
switch (key) {
case ARZ_KEY_TARGET:
return L"target";
case ARZ_KEY_APP_DISPLAY:
return L"app-display";
case ARZ_KEY_APP_EXEC:
return L"app-exec";
default:
return L"";
}
}
/* A number as text, for the two messages that carry one. No swprintf:
* the CRT's wide formatting is the one piece of <stdio.h> this program
* would otherwise need, and both numbers fit in ten digits. */
static void number_to_wide(unsigned long n, wchar_t *buf, size_t cap)
{
wchar_t rev[24];
size_t j = 0;
size_t i = 0;
if (n == 0) {
rev[j++] = L'0';
} else {
while (n > 0 && j < sizeof(rev) / sizeof(rev[0])) {
rev[j++] = (wchar_t)(L'0' + (n % 10));
n /= 10;
}
}
while (j > 0 && i + 1 < cap)
buf[i++] = rev[--j];
buf[i] = L'\0';
}
static wchar_t *module_path(void)
{
DWORD cap = 512;
for (;;) {
wchar_t *buf = malloc(cap * sizeof(*buf));
DWORD n;
if (buf == NULL)
return NULL;
n = GetModuleFileNameW(NULL, buf, cap);
if (n == 0) {
free(buf);
return NULL;
}
/* n == cap means truncation, and on older Windows the buffer
* is not even terminated — grow rather than trust it. */
if (n < cap)
return buf;
free(buf);
if (cap >= 65536)
return NULL;
cap *= 2;
}
}
/* bin\<exec>.exe -> bin\<exec>.ariza. The sidecar is named after the
* executable rather than after app_exec, because the executable's name
* is the one thing known before the file is read. */
static wchar_t *sidecar_path(const wchar_t *exe)
{
size_t n = arz_len(exe);
size_t base = n;
wchar_t *stem;
wchar_t *out;
size_t i;
if (n >= 4 && exe[n - 4] == L'.' &&
(exe[n - 3] == L'e' || exe[n - 3] == L'E') &&
(exe[n - 2] == L'x' || exe[n - 2] == L'X') &&
(exe[n - 1] == L'e' || exe[n - 1] == L'E'))
base = n - 4;
stem = malloc((base + 1) * sizeof(*stem));
if (stem == NULL)
return NULL;
for (i = 0; i < base; i++)
stem[i] = exe[i];
stem[base] = L'\0';
out = arz_cat(stem, L".ariza", NULL);
free(stem);
return out;
}
static int file_exists(const wchar_t *path)
{
DWORD attrs;
if (path == NULL)
return 0;
attrs = GetFileAttributesW(path);
return attrs != INVALID_FILE_ATTRIBUTES &&
(attrs & FILE_ATTRIBUTE_DIRECTORY) == 0;
}
/* Read a UTF-8 file and hand back its text as UTF-16. `why` is set to a
* sentence fragment on failure and the return is NULL. */
static wchar_t *read_utf8_file(const wchar_t *path, const wchar_t **why)
{
HANDLE h;
LARGE_INTEGER size;
char *bytes;
DWORD got = 0;
DWORD offset = 0;
int units;
wchar_t *text;
h = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE,
NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL);
if (h == INVALID_HANDLE_VALUE) {
*why = L"it is missing or unreadable";
return NULL;
}
if (!GetFileSizeEx(h, &size)) {
CloseHandle(h);
*why = L"its size could not be read";
return NULL;
}
/* A sidecar is a few hundred bytes. A megabyte of it is not a
* sidecar, and reading whatever it is into memory helps nobody. */
if (size.QuadPart > 1024 * 1024) {
CloseHandle(h);
*why = L"it is far too large to be one";
return NULL;
}
if (size.QuadPart == 0) {
CloseHandle(h);
text = arz_dup(L"");
if (text == NULL)
*why = L"there was not enough memory to read it";
return text;
}
bytes = malloc((size_t)size.QuadPart);
if (bytes == NULL) {
CloseHandle(h);
*why = L"there was not enough memory to read it";
return NULL;
}
while (offset < (DWORD)size.QuadPart) {
if (!ReadFile(h, bytes + offset, (DWORD)size.QuadPart - offset,
&got, NULL) || got == 0) {
CloseHandle(h);
free(bytes);
*why = L"it could not be read to the end";
return NULL;
}
offset += got;
}
CloseHandle(h);
/* MB_ERR_INVALID_CHARS, so bad bytes are a refusal rather than a
* silent run of U+FFFD in a path. */
units = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, bytes,
(int)offset, NULL, 0);
if (units <= 0) {
free(bytes);
*why = L"it is not valid UTF-8";
return NULL;
}
text = malloc(((size_t)units + 1) * sizeof(*text));
if (text == NULL) {
free(bytes);
*why = L"there was not enough memory to read it";
return NULL;
}
units = MultiByteToWideChar(CP_UTF8, MB_ERR_INVALID_CHARS, bytes,
(int)offset, text, units);
free(bytes);
if (units <= 0) {
free(text);
*why = L"it is not valid UTF-8";
return NULL;
}
text[units] = L'\0';
return text;
}
static wchar_t *env_get(const wchar_t *name)
{
DWORD cap = 256;
for (;;) {
wchar_t *buf = malloc(cap * sizeof(*buf));
DWORD n;
if (buf == NULL)
return NULL;
n = GetEnvironmentVariableW(name, buf, cap);
if (n == 0) {
/* Unset, or set to the empty string: the same thing to
* everything this program does with one. */
free(buf);
return NULL;
}
if (n < cap)
return buf;
free(buf);
cap = n + 1;
}
}
static void env_set(const wchar_t *name, const wchar_t *value)
{
if (SetEnvironmentVariableW(name, value))
return;
/* A deletion is best-effort: removing a variable that was never
* there reports ERROR_ENVVAR_NOT_FOUND, which is the ordinary case
* for the `unset PERL6LIB` every ariza bundle carries, and not
* something to stop a launch over. A failed *assignment* is
* different — the child would run with the wrong environment — so
* that one is fatal. */
if (value == NULL)
return;
fatal(L"ariza: could not set ", name, L" for the application.");
}
static int env_is_one(const wchar_t *name)
{
wchar_t *value = env_get(name);
int yes = value != NULL && value[0] == L'1' && value[1] == L'\0';
free(value);
return yes;
}
static int make_dir(const wchar_t *path)
{
DWORD attrs;
if (!CreateDirectoryW(path, NULL) &&
GetLastError() != ERROR_ALREADY_EXISTS)
return 0;
attrs = GetFileAttributesW(path);
return attrs != INVALID_FILE_ATTRIBUTES &&
(attrs & FILE_ATTRIBUTE_DIRECTORY) != 0 &&
(attrs & FILE_ATTRIBUTE_REPARSE_POINT) == 0;
}
static int random_nonce(wchar_t out[65])
{
unsigned char bytes[32];
static const wchar_t hex[] = L"0123456789abcdef";
size_t i;
if (BCryptGenRandom(NULL, bytes, sizeof(bytes),
BCRYPT_USE_SYSTEM_PREFERRED_RNG) != 0)
return 0;
for (i = 0; i < sizeof(bytes); i++) {
out[i * 2] = hex[bytes[i] >> 4];
out[i * 2 + 1] = hex[bytes[i] & 15];
}
out[64] = L'\0';
SecureZeroMemory(bytes, sizeof(bytes));
return 1;
}
/* Create a runner-owned challenge directory below the managed application
* root. The updater receives only the result-file path and the nonce; it
* atomically writes that file after the install transaction commits. */
static wchar_t *handoff_challenge(const arz_config *cfg, wchar_t nonce[65],
wchar_t **challenge_dir)
{
wchar_t *base = env_get(L"LOCALAPPDATA");
wchar_t *app = NULL;
wchar_t *ariza = NULL;
wchar_t *state = NULL;
wchar_t *leaf = NULL;
wchar_t pid[24];
wchar_t *path = NULL;
*challenge_dir = NULL;
if (base == NULL || !random_nonce(nonce))
goto done;
app = arz_path_join(base, cfg->app_display);
ariza = app != NULL ? arz_path_join(app, L".ariza") : NULL;
state = ariza != NULL ? arz_path_join(ariza, L"update-v1") : NULL;
if (app == NULL || ariza == NULL || state == NULL ||
!make_dir(app) || !make_dir(ariza) || !make_dir(state))
goto done;
number_to_wide((unsigned long)GetCurrentProcessId(), pid,
sizeof(pid) / sizeof(pid[0]));
leaf = arz_cat(L"handoff-", pid, L"-");
if (leaf != NULL) {
wchar_t *with_nonce = arz_cat(leaf, nonce, NULL);
free(leaf);
leaf = with_nonce;
}
*challenge_dir = leaf != NULL ? arz_path_join(state, leaf) : NULL;
if (*challenge_dir == NULL || !CreateDirectoryW(*challenge_dir, NULL)) {
free(*challenge_dir);
*challenge_dir = NULL;
goto done;
}
path = arz_path_join(*challenge_dir, L"result");
done:
free(base);
free(app);
free(ariza);
free(state);
free(leaf);
return path;
}
static void remove_challenge(const wchar_t *path, const wchar_t *dir)
{
if (path != NULL)
DeleteFileW(path);
if (dir != NULL)
RemoveDirectoryW(dir);
}
static wchar_t *final_dir_path(const wchar_t *path)
{
HANDLE h;
DWORD cap = 512;
h = CreateFileW(path, 0,
FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_SHARE_DELETE, NULL,
OPEN_EXISTING, FILE_FLAG_BACKUP_SEMANTICS, NULL);
if (h == INVALID_HANDLE_VALUE)
return NULL;
for (;;) {
wchar_t *out = malloc((size_t)cap * sizeof(*out));
DWORD n;
if (out == NULL) {
CloseHandle(h);
return NULL;
}
n = GetFinalPathNameByHandleW(h, out, cap,
FILE_NAME_NORMALIZED | VOLUME_NAME_DOS);
if (n > 0 && n < cap) {
CloseHandle(h);
return out;
}
free(out);
if (n == 0 || n > 65535) {
CloseHandle(h);
return NULL;
}
cap = n + 1;
}
}
static int wide_equal_ci(const wchar_t *a, const wchar_t *b)
{
if (a == NULL || b == NULL)
return 0;
return CompareStringOrdinal(a, -1, b, -1, TRUE) == CSTR_EQUAL;
}
/* Only a launcher reached through the managed `current` junction may create
* an update challenge. Portable archives use the same sidecar, but must not
* acquire update state merely because their build opted into the feature. */
static int is_managed_current(const arz_config *cfg, const wchar_t *root)
{
wchar_t *base = env_get(L"LOCALAPPDATA");
wchar_t *app = NULL;
wchar_t *current = NULL;
wchar_t *root_final = NULL;
wchar_t *current_final = NULL;
int matches = 0;
if (base == NULL)
goto done;
app = arz_path_join(base, cfg->app_display);
current = app != NULL ? arz_path_join(app, L"current") : NULL;
root_final = final_dir_path(root);
current_final = final_dir_path(current);
matches = wide_equal_ci(root_final, current_final);
done:
free(base);
free(app);
free(current);
free(root_final);
free(current_final);
return matches;
}
/* Validate the candidate by comparing resolved directory identities. The
* handoff record cannot redirect execution: it names a version only, and the
* runner launches the entry point it derives below managed `current`. */
static wchar_t *validated_current_exe(const arz_config *cfg,
const arz_handoff *handoff)
{
wchar_t *base = env_get(L"LOCALAPPDATA");
wchar_t *app = NULL;
wchar_t *current = NULL;
wchar_t *versions = NULL;
wchar_t *candidate = NULL;
wchar_t *current_final = NULL;
wchar_t *candidate_final = NULL;
wchar_t *bin = NULL;
wchar_t *name = NULL;
wchar_t *exe = NULL;
if (base == NULL)
goto done;
app = arz_path_join(base, cfg->app_display);
current = app != NULL ? arz_path_join(app, L"current") : NULL;
versions = app != NULL ? arz_path_join(app, L"versions") : NULL;
candidate = versions != NULL
? arz_path_join(versions, handoff->candidate) : NULL;
current_final = final_dir_path(current);
candidate_final = final_dir_path(candidate);
if (!wide_equal_ci(current_final, candidate_final))
goto done;
bin = arz_path_join(current, L"bin");
name = arz_cat(cfg->app_exec, L".exe", NULL);
exe = bin != NULL && name != NULL ? arz_path_join(bin, name) : NULL;
if (!file_exists(exe)) {
free(exe);
exe = NULL;
}
done:
free(base);
free(app);
free(current);
free(versions);
free(candidate);
free(current_final);
free(candidate_final);
free(bin);
free(name);
return exe;
}
static wchar_t *exe_cmdline(const wchar_t *exe, const wchar_t *tail)
{
wchar_t *quoted = arz_quote_arg(exe);
wchar_t *out;
if (quoted == NULL)
return NULL;
out = tail == NULL || tail[0] == L'\0'
? arz_dup(quoted) : arz_cat(quoted, L" ", tail);
free(quoted);
return out;
}
static int spawn_and_wait(const wchar_t *application, wchar_t *command,
DWORD *code)
{
STARTUPINFOW si;
PROCESS_INFORMATION pi;
ZeroMemory(&si, sizeof(si));
si.cb = sizeof(si);
ZeroMemory(&pi, sizeof(pi));
if (!CreateProcessW(application, command, NULL, NULL, TRUE, 0, NULL,
NULL, &si, &pi))
return 0;
WaitForSingleObject(pi.hProcess, INFINITE);
if (!GetExitCodeProcess(pi.hProcess, code))
*code = ARZ_EXIT_LAUNCH_FAILURE;
CloseHandle(pi.hThread);
CloseHandle(pi.hProcess);
return 1;
}
/* The sidecar's environment directives, in file order.
*
* Order is the contract, not an implementation detail: two
* `prepend-path` lines have to end up in the order they were written,
* and a `set` after an `unset` of the same variable has to win. Each
* prepend reads the PATH the previous one left, which is why this walks
* rather than batching. */
static void apply_env_ops(const arz_config *cfg, const wchar_t *root)
{
size_t i;
for (i = 0; i < cfg->op_count; i++) {
const arz_env_op *op = &cfg->ops[i];
wchar_t *value;
switch (op->kind) {
case ARZ_OP_UNSET:
env_set(op->name, NULL);
break;
case ARZ_OP_SET:
value = arz_expand(op->value, root);
if (value == NULL)
fatal_oom();
env_set(op->name, value);
free(value);
break;
case ARZ_OP_PREPEND_PATH: {
wchar_t *current;
wchar_t *joined;
value = arz_expand(op->value, root);
if (value == NULL)
fatal_oom();
current = env_get(L"PATH");
joined = arz_env_path_prepend(value, current);
if (joined == NULL)
fatal_oom();
env_set(L"PATH", joined);
free(joined);
free(current);
free(value);
break;
}
default:
/* arz_config_parse produces no other kind; a bundle
* that somehow contains one is not one to guess at. */
fatal(L"ariza: the launcher configuration asks for"
L" something this runner does not implement.",
NULL, NULL);
}
}
}
/* One notice, once, marked by a file under %LOCALAPPDATA%\<exec>\.
* Nothing is written into the bundle itself, so a read-only or shared
* bundle behaves exactly like a private one — and every failure along
* the way is ignored, because a profile that cannot be written to is a
* reason to print the notice twice, never a reason not to launch. */
static void first_run_notice(const arz_config *cfg)
{
wchar_t *base = env_get(L"LOCALAPPDATA");
wchar_t *dir;
wchar_t *marker;
if (base == NULL)
base = env_get(L"TEMP");
dir = base != NULL ? arz_path_join(base, cfg->app_exec) : NULL;
marker = dir != NULL ? arz_path_join(dir, L".first-run") : NULL;
if (marker != NULL && file_exists(marker)) {
free(base);
free(dir);
free(marker);
return;
}
err_line(cfg->app_display,
L": first launch can take a few seconds. Later ones are instant.",
NULL);
if (dir != NULL)
CreateDirectoryW(dir, NULL);
if (marker != NULL) {
HANDLE h = CreateFileW(marker, GENERIC_WRITE, 0, NULL,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, NULL);
if (h != INVALID_HANDLE_VALUE)
CloseHandle(h);
}
free(base);
free(dir);
free(marker);
}
static BOOL WINAPI ctrl_handler(DWORD type)
{
(void)type;
/* The console sends Ctrl+C to every process attached to it, so the
* child gets its own copy and can shut itself down. Handling the
* event here — by doing nothing with it — keeps the runner alive to
* collect the child's exit code, rather than returning the shell's
* prompt while the app is still on screen. */
return TRUE;
}
int wmain(int argc, wchar_t **argv)
{
wchar_t *exe_path;
wchar_t *bin_dir;
wchar_t *root;
wchar_t *sidecar;
wchar_t *text;
const wchar_t *why = L"";
arz_config cfg;
size_t bad_line = 0;
arz_key missing;
wchar_t *raku;
wchar_t *target;
const wchar_t *tail;
wchar_t *cmdline;
DWORD code = 0;
wchar_t nonce[65];
wchar_t *handoff_path = NULL;
wchar_t *handoff_dir = NULL;
int update_challenge = 0;
(void)argc;
(void)argv;
exe_path = module_path();
if (exe_path == NULL)
fatal(L"ariza: Windows would not say where this program is,",
L" so its bundle cannot be located.", NULL);
bin_dir = arz_parent_dir(exe_path);
root = bin_dir != NULL ? arz_parent_dir(bin_dir) : NULL;
if (root == NULL)
fatal(L"ariza: \"", exe_path,
L"\" is not inside a bundle's bin directory.");
sidecar = sidecar_path(exe_path);
if (sidecar == NULL)
fatal_oom();
text = read_utf8_file(sidecar, &why);
if (text == NULL) {
/* `why` rather than one message for four causes: "it is
* missing" sends someone to their unpack, "it is not valid
* UTF-8" sends them to whatever edited it, and a launcher that
* cannot tell them apart sends them to neither. */
err_line(L"ariza: cannot read the launcher configuration at \"",
sidecar, arz_cat(L"\" — ", why, L"."));
err_line(L" Unpack the whole archive and run this program"
L" from inside it.", NULL, NULL);
ExitProcess(ARZ_EXIT_LAUNCH_FAILURE);
}
if (arz_config_parse(text, &cfg, &bad_line) != ARZ_OK) {
wchar_t number[24];
number_to_wide((unsigned long)bad_line, number,
sizeof(number) / sizeof(number[0]));
err_line(L"ariza: \"", sidecar, L"\" is not a launcher"
L" configuration this runner can read.");
err_line(L" Line ", number, L" is neither a comment nor a"
L" directive it implements; this bundle is damaged.");
ExitProcess(ARZ_EXIT_LAUNCH_FAILURE);
}
missing = arz_config_missing(&cfg);
if (missing != ARZ_KEY_NONE)
fatal(L"ariza: \"", sidecar,
arz_cat(L"\" does not set ", key_wname(missing),
L"; this bundle is damaged."));
raku = arz_interpreter_path(root);
target = arz_path_join(root, cfg.target);
if (raku == NULL || target == NULL)
fatal_oom();
if (!file_exists(raku)) {
/* Same first line, word for word, as the .cmd and .ps1
* launchers print: whichever entry point a user reached, the
* search that follows should find one answer. */
err_line(cfg.app_display,
L": this bundle is incomplete - no interpreter at \"",
arz_cat(raku, L"\"", NULL));
err_line(L" Unpack the whole archive and run this program"
L" from inside it.", NULL, NULL);
ExitProcess(ARZ_EXIT_LAUNCH_FAILURE);
}
/* Whatever the bundle said, in the order it said it. What those
* variables are for — that RAKULIB has to name an installation
* repository, that PERL6LIB has to be removed rather than emptied,
* that a bundled DLL is found by walking PATH — is knowledge held
* by the renderer that wrote the sidecar, and deliberately not by
* this program. */
apply_env_ops(&cfg, root);
/* Update-disabled bundles pay no state or randomness cost. The generated
* sidecar enables this through the existing generic `set` directive, so
* the fail-closed sidecar grammar does not change. */
update_challenge = env_is_one(ARZ_ENV_UPDATES_ENABLED) &&
!env_is_one(ARZ_ENV_RELAUNCHED) &&
is_managed_current(&cfg, root);
if (update_challenge) {
handoff_path = handoff_challenge(&cfg, nonce, &handoff_dir);
if (handoff_path != NULL) {
env_set(ARZ_ENV_HANDOFF, handoff_path);
env_set(ARZ_ENV_NONCE, nonce);
} else {
/* Without an authenticated challenge the coordinator simply
* skips updating; launching the application is still safe. */
env_set(ARZ_ENV_HANDOFF, NULL);
env_set(ARZ_ENV_NONCE, NULL);
update_challenge = 0;
}
} else {
env_set(ARZ_ENV_HANDOFF, NULL);
env_set(ARZ_ENV_NONCE, NULL);
}
first_run_notice(&cfg);
/* The arguments, exactly as the user's shell produced them: find
* where argv[0] ends and copy everything after it. */
tail = arz_cmdline_tail(GetCommandLineW());
cmdline = arz_child_cmdline(raku, target, tail);
if (cmdline == NULL)
fatal_oom();
SetConsoleCtrlHandler(ctrl_handler, TRUE);
/* lpApplicationName names the interpreter exactly, so no search
* path decides which raku.exe runs; lpCommandLine still opens with
* it, because that is what the child reads as its own argv[0].
* Handles are inherited and no creation flag is passed, so the
* child shares this console: a TUI needs the real one, and a
* redirected run needs the pipes. */
if (!spawn_and_wait(raku, cmdline, &code)) {
wchar_t buf[24];
number_to_wide((unsigned long)GetLastError(), buf,
sizeof(buf) / sizeof(buf[0]));
err_line(cfg.app_display,
L": could not start the interpreter at \"",
arz_cat(raku, L"\" (Windows error ",
arz_cat(buf, L").", NULL)));
ExitProcess(ARZ_EXIT_LAUNCH_FAILURE);
}
if (code == ARZ_EXIT_UPDATE_HANDOFF && update_challenge &&
handoff_path != NULL) {
const wchar_t *handoff_why = L"";
wchar_t *handoff_text = read_utf8_file(handoff_path, &handoff_why);
arz_handoff handoff;
int authenticated = 0;
int launched = 0;
if (handoff_text != NULL && arz_len(handoff_text) <= 4096 &&
arz_handoff_parse(handoff_text, &handoff) == ARZ_OK &&
arz_handoff_nonce_matches(&handoff, nonce)) {
wchar_t *next = validated_current_exe(&cfg, &handoff);
wchar_t *next_cmd = next != NULL ? exe_cmdline(next, tail) : NULL;
authenticated = 1;
if (next != NULL && next_cmd != NULL) {
env_set(ARZ_ENV_RELAUNCHED, L"1");
env_set(ARZ_ENV_HANDOFF, NULL);
env_set(ARZ_ENV_NONCE, NULL);
launched = spawn_and_wait(next, next_cmd, &code);
}
free(next_cmd);
free(next);
arz_handoff_free(&handoff);
}
free(handoff_text);
/* A fully authenticated record means installation committed. If
* the new entry point cannot be resolved or created, say so and run
* the old physical bundle once instead; `current` remains new for
* the next launch, but this invocation is not lost. */
if (authenticated && !launched) {
wchar_t *old_cmd;
err_line(cfg.app_display,
L": the update installed, but its launcher could not start;",
L" continuing with the previous version.");
env_set(ARZ_ENV_RELAUNCHED, L"1");
env_set(ARZ_ENV_HANDOFF, NULL);
env_set(ARZ_ENV_NONCE, NULL);
old_cmd = arz_child_cmdline(raku, target, tail);
if (old_cmd == NULL || !spawn_and_wait(raku, old_cmd, &code))
code = ARZ_EXIT_LAUNCH_FAILURE;
free(old_cmd);
}
}
remove_challenge(handoff_path, handoff_dir);
free(cmdline);
free(raku);
free(target);
free(sidecar);
free(text);
free(exe_path);
free(bin_dir);
free(root);
free(handoff_path);
free(handoff_dir);
arz_config_free(&cfg);
return (int)code;
}
#else
/* A POSIX test build compiles the core and nothing else; this file has
* no business there, and an empty translation unit is not valid C. */
typedef int arz_main_win32_is_windows_only;
#endif /* _WIN32 */