diff --git a/README.md b/README.md index 1895559..9b6638d 100644 --- a/README.md +++ b/README.md @@ -43,38 +43,36 @@ Tested on firmware 13.42 with elfldr 0.26. ## Install -1. Download `tailscale-installer.elf` from the - [latest release](../../releases/latest). -2. Send it to the console's ELF loader, once. Any payload sender works: +There is one file, `tailscale.elf`, and it is an ordinary payload: running +it starts Tailscale. + +1. Download `tailscale.elf` from the [latest release](../../releases/latest). +2. Send it to the console's ELF loader. Any payload sender works: ```bash # Linux / macOS - socat -t 60 - TCP::9021 < tailscale-installer.elf + socat -t 60 - TCP::9021 < tailscale.elf ``` ```powershell # Windows (script from this repository) - .\tools\ps5send.ps1 -File tailscale-installer.elf -PS5Host -Seconds 70 + .\tools\ps5send.ps1 -File tailscale.elf -PS5Host ``` - The installer prints what it does. It: - - stores the daemon payload as `/data/tailscale/tailscale.elf`, - - adds a **Tailscale** icon to the home screen (media section), - - starts Tailscale. - 3. Open `http://:8090` on a phone or PC. Scan the QR code or follow the link and log in to Tailscale. If your tailnet uses device approval, approve the console in the admin console. -The console now has a tailnet address, shown on the status page. - -Sending the installer again upgrades and restarts Tailscale. The login is -kept. +The console now has a tailnet address, shown on the status page. The first +run also adds a **Tailscale** icon to the home screen (media section) that +opens the status page. Tailscale runs until the console restarts. After a restart and jailbreak, -send `/data/tailscale/tailscale.elf` (or the installer) to the ELF loader -again. The installer does not change any payload autoloader; if you use one, -you can add that file to it yourself. +send `tailscale.elf` again; the login and settings are kept. If you use a +payload manager or autoloader, add the file there like any other payload. + +To update, use the new `tailscale.elf` in place of the old one. Sending it +while Tailscale is running replaces the running copy. ## Using it @@ -215,19 +213,21 @@ Files on the console: | `/data/tailscale/state/` | Tailscale's state, including the login. | | `/data/tailscale/tailscale.log` | The daemon's log, rotated at 2 MB. | | `/data/tailscale/tailscale-debug.log` | Tailscale's detailed log, up to 4 MB plus one older file. | -| `/data/tailscale/tailscale.elf` | The daemon payload. | +| `/data/tailscale/icon-installed` | Marks that the home screen icon was added. Delete it to have the icon added again on the next start. | | `/user/app/TSCL00001/` | The home screen icon. | ## Uninstall -Press **Uninstall** on the status page. It deletes the daemon payload and -stops Tailscale. It asks whether to also log out and delete the saved login. +Press **Uninstall** on the status page. It logs the console out of your +tailnet, deletes `/data/tailscale` (login, settings, logs) and stops +Tailscale. -Two things are left to do by hand: +Left to do by hand: - Delete the home screen icon (Options button, then Delete). - Remove the device in the Tailscale admin console. -- If you added the payload to an autoloader yourself, remove it there. +- If you added `tailscale.elf` to a payload manager or autoloader, remove it + there, or it starts again on the next boot. ## Troubleshooting diff --git a/installer/assets/icon0.png b/appicon/icon0.png similarity index 100% rename from installer/assets/icon0.png rename to appicon/icon0.png diff --git a/appicon/main.c b/appicon/main.c new file mode 100644 index 0000000..dc63c46 --- /dev/null +++ b/appicon/main.c @@ -0,0 +1,151 @@ +/* Home screen icon helper for Tailscale on PS5. + * + * A tiny payload that puts a "Tailscale" icon on the home screen: a media app + * whose only content is a link to the status page, which the console opens in + * its browser. The daemon's launcher carries this payload and hands it to the + * ELF loader the first time Tailscale runs. It is a separate payload so that + * the system libraries it needs are never loaded into the daemon's process. + * + * Prints "icon: ok" on success; the launcher looks for that. + * + * Build with -DASSET_DIR="path/to/appicon" and link, in this order, + * -lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService (with + * libSceAppInstUtil alone the payload is never started). */ + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +#ifndef ASSET_DIR +#error "ASSET_DIR must name the folder with param.json and icon0.png" +#endif + +#define TITLE_ID "TSCL00001" +#define APP_DIR "/user/app/" TITLE_ID + +#define INCASSET(name, file) \ + __asm__(".section .rodata\n" \ + ".balign 16\n" \ + ".global " #name "\n" #name ":\n" \ + ".incbin \"" file "\"\n" \ + ".global " #name "_end\n" #name "_end:\n" \ + ".text\n"); \ + extern const uint8_t name[]; \ + extern const uint8_t name##_end[]; + +INCASSET(param_json, ASSET_DIR "/param.json") +INCASSET(icon_png, ASSET_DIR "/icon0.png") + +int sceAppInstUtilInitialize(void); +int sceAppInstUtilTerminate(void); +int sceAppInstUtilAppInstallAll(void *); + +static int +write_file(const char *path, const uint8_t *data, size_t size) { + int fd = open(path, O_WRONLY | O_CREAT | O_TRUNC, 0644); + + if (fd < 0) { + return -1; + } + while (size > 0) { + ssize_t n = write(fd, data, size); + if (n < 0) { + if (errno == EINTR) { + continue; + } + close(fd); + return -1; + } + data += n; + size -= n; + } + return close(fd); +} + +/* Report whether the file at path already has exactly these contents. */ +static int +file_matches(const char *path, const uint8_t *data, size_t size) { + struct stat st; + uint8_t *buf; + int same = 0; + FILE *f; + + if (stat(path, &st) || (size_t)st.st_size != size || !(f = fopen(path, "rb"))) { + return 0; + } + if ((buf = malloc(size))) { + same = fread(buf, 1, size, f) == size && !memcmp(buf, data, size); + free(buf); + } + fclose(f); + return same; +} + +int +main(void) { + int (*install_title_dir)(const char *, const char *, void *) = 0; + size_t param_size = param_json_end - param_json; + size_t icon_size = icon_png_end - icon_png; + pid_t pid = getpid(); + intptr_t rootvnode; + uint32_t handle; + int err; + + setvbuf(stdout, 0, _IONBF, 0); + + /* /user/app is only writable as root outside the sandbox. */ + if ((rootvnode = kernel_get_root_vnode())) { + kernel_set_proc_rootdir(pid, rootvnode); + kernel_set_proc_jaildir(pid, 0); + } + kernel_set_ucred_uid(pid, 0); + kernel_set_ucred_ruid(pid, 0); + kernel_set_ucred_svuid(pid, 0); + kernel_set_ucred_rgid(pid, 0); + kernel_set_ucred_svgid(pid, 0); + + if (file_matches(APP_DIR "/sce_sys/param.json", param_json, param_size) && + file_matches(APP_DIR "/sce_sys/icon0.png", icon_png, icon_size)) { + printf("icon: ok (already installed)\n"); + return 0; + } + + if ((err = sceAppInstUtilInitialize())) { + printf("icon: sceAppInstUtilInitialize failed: 0x%08x\n", err); + return 1; + } + mkdir(APP_DIR, 0755); + mkdir(APP_DIR "/sce_sys", 0755); + if (write_file(APP_DIR "/sce_sys/param.json", param_json, param_size) || + write_file(APP_DIR "/sce_sys/icon0.png", icon_png, icon_size)) { + printf("icon: could not write to %s: %s\n", APP_DIR, strerror(errno)); + sceAppInstUtilTerminate(); + return 1; + } + + /* Register just this title where the firmware supports it; otherwise ask + * for a rescan of everything under /user/app. */ + if (!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) { + install_title_dir = (void *)kernel_dynlib_resolve(-1, handle, "Wudg3Xe3heE"); + } + if (install_title_dir) { + err = install_title_dir(TITLE_ID, "/user/app/", 0); + } else { + err = sceAppInstUtilAppInstallAll(0); + } + sceAppInstUtilTerminate(); + if (err) { + printf("icon: registering the app failed: 0x%08x\n", err); + return 1; + } + printf("icon: ok (installed %s)\n", TITLE_ID); + return 0; +} diff --git a/installer/assets/param.json b/appicon/param.json similarity index 100% rename from installer/assets/param.json rename to appicon/param.json diff --git a/docs/BUILDING.md b/docs/BUILDING.md index 52e1a5a..717f4ff 100644 --- a/docs/BUILDING.md +++ b/docs/BUILDING.md @@ -51,18 +51,18 @@ These folders are not in the repository. ## Building the payloads ```powershell -# daemon payload: C launcher + Go program -> out\tailscale.elf -.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.0 - -# installer -> out\tailscale-installer.elf (embeds out\tailscale.elf) -.\tools\build-installer.ps1 +# C launcher + Go program + home screen icon helper -> out\tailscale.elf +.\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.1 -HomeIcon ``` +`-HomeIcon` also builds `appicon\` into `out\appicon.elf` and embeds it in +the launcher. + ## Sending to the console ```powershell $env:PS5_HOST = '192.168.1.50' # your console -.\tools\ps5send.ps1 -File out\tailscale-installer.elf -Seconds 70 +.\tools\ps5send.ps1 -File out\tailscale.elf ``` `ps5send.ps1` prints whatever the payload writes back. diff --git a/docs/TECHNICAL.md b/docs/TECHNICAL.md index e693cda..67fff2c 100644 --- a/docs/TECHNICAL.md +++ b/docs/TECHNICAL.md @@ -44,11 +44,18 @@ specification. - A payload that is sent again stops the running instance (through the status page, or failing that by the pid it recorded) and takes over. -**The installer** (`installer/`, C) embeds `tailscale.elf`. It writes it to -`/data/tailscale/tailscale.elf`, registers a home screen app whose -`param.json` has a `deeplinkUri` to the status page, and starts the daemon by -sending it to the ELF loader on `127.0.0.1:9021`. It does not modify any -payload autoloader. +**The icon helper** (`appicon/`, C) is a second, tiny payload embedded in the +launcher. The first time `tailscale.elf` runs, the launcher sends it to the +ELF loader on `127.0.0.1:9021`, where it runs as a process of its own, +registers a home screen app whose `param.json` has a `deeplinkUri` to the +status page, reports the result and exits. The launcher then writes +`/data/tailscale/icon-installed` and never does it again. It is a separate +payload so that the system libraries it needs are never loaded into the +long-running daemon process, where their threads could receive signals meant +for the Go runtime. + +There is no installer. Nothing is copied anywhere and no payload autoloader +is touched: the payload is run from wherever the user keeps it. ## The PS5 as a Go target @@ -144,6 +151,9 @@ Linking `libSceAppInstUtil` alone leaves the payload stopped before it runs. It needs `-lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService`, in that order, as in the SDK's `install_app` sample. +The daemon cannot remove the icon; that is left to the user (Options, then +Delete, on the home screen). + ## Known problems - Once, switching the console from Wi-Fi to Ethernet during a Moonlight diff --git a/docs/status-page.png b/docs/status-page.png index 9d5ecc8..20ac98d 100644 Binary files a/docs/status-page.png and b/docs/status-page.png differ diff --git a/installer/main.c b/installer/main.c deleted file mode 100644 index bb29944..0000000 --- a/installer/main.c +++ /dev/null @@ -1,310 +0,0 @@ -/* Tailscale installer payload for jailbroken PS5s. - * - * Stores the Tailscale daemon payload on the console, adds a home screen - * icon that opens the status page, and starts the daemon through the ELF - * loader on this console. It does not touch any payload autoloader. Build - * with tools\build-installer.ps1, which sets DAEMON_ELF and - * ASSET_DIR and links the system libraries the app installer needs. */ - -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include -#include -#include -#include -#include - -#include - -#ifndef DAEMON_ELF -#error "DAEMON_ELF must name the daemon payload to embed" -#endif - -#define DATA_DIR "/data/tailscale" -#define DAEMON_NAME "tailscale.elf" -#define LOADER_PORT 9021 - -extern const uint8_t daemon_elf[]; -extern const uint8_t daemon_elf_end[]; - -__asm__(".section .rodata\n" - ".balign 16\n" - ".global daemon_elf\n" - "daemon_elf:\n" - ".incbin \"" DAEMON_ELF "\"\n" - ".global daemon_elf_end\n" - "daemon_elf_end:\n" - ".text\n"); - -/* The home screen launcher: a media app whose only content is a link, which - * the console opens in its browser. ASSET_DIR is set by the build. */ -#ifndef ASSET_DIR -#error "ASSET_DIR must name the folder with param.json and icon0.png" -#endif -#define LAUNCHER_TITLE_ID "TSCL00001" -#define LAUNCHER_DIR "/user/app/" LAUNCHER_TITLE_ID - -#define INCASSET(name, file) \ - __asm__(".section .rodata\n" \ - ".balign 16\n" \ - ".global " #name "\n" #name ":\n" \ - ".incbin \"" file "\"\n" \ - ".global " #name "_end\n" #name "_end:\n" \ - ".text\n"); \ - extern const uint8_t name[]; \ - extern const uint8_t name##_end[]; - -INCASSET(launcher_param_json, ASSET_DIR "/param.json") -INCASSET(launcher_icon_png, ASSET_DIR "/icon0.png") - -int sceAppInstUtilInitialize(void); -int sceAppInstUtilTerminate(void); -int sceAppInstUtilAppInstallAll(void *); - -typedef struct notify_request { - char useless1[45]; - char message[3075]; -} notify_request_t; - -int sceKernelSendNotificationRequest(int, notify_request_t *, size_t, int); - -static void -notify(const char *fmt, ...) { - notify_request_t req; - va_list args; - - memset(&req, 0, sizeof(req)); - va_start(args, fmt); - vsnprintf(req.message, sizeof(req.message), fmt, args); - va_end(args); - sceKernelSendNotificationRequest(0, &req, sizeof(req), 0); -} - -static int -write_all(int fd, const uint8_t *data, size_t size) { - while (size > 0) { - ssize_t n = write(fd, data, size); - if (n < 0) { - if (errno == EINTR) { - continue; - } - return -1; - } - data += n; - size -= n; - } - return 0; -} - -/* Write a file through a temporary name so a failed write never leaves a - * truncated payload behind. */ -static int -write_file(const char *path, const uint8_t *data, size_t size) { - char tmp[512]; - int fd; - - snprintf(tmp, sizeof(tmp), "%s.tmp", path); - if ((fd = open(tmp, O_WRONLY | O_CREAT | O_TRUNC, 0755)) < 0) { - return -1; - } - if (write_all(fd, data, size)) { - close(fd); - unlink(tmp); - return -1; - } - close(fd); - if (rename(tmp, path)) { - unlink(tmp); - return -1; - } - return 0; -} - -static int -install_daemon(const char *dir) { - char path[512]; - - mkdir(dir, 0755); - snprintf(path, sizeof(path), "%s/%s", dir, DAEMON_NAME); - if (write_file(path, daemon_elf, daemon_elf_end - daemon_elf)) { - printf(" could not write %s: %s\n", path, strerror(errno)); - return -1; - } - printf(" wrote %s (%.1f MB)\n", path, (daemon_elf_end - daemon_elf) / 1048576.0); - return 0; -} - -/* Report whether the file at path already has exactly these contents. */ -static int -file_matches(const char *path, const uint8_t *data, size_t size) { - struct stat st; - uint8_t *buf; - int same = 0; - FILE *f; - - if (stat(path, &st) || (size_t)st.st_size != size || !(f = fopen(path, "rb"))) { - return 0; - } - if ((buf = malloc(size))) { - same = fread(buf, 1, size, f) == size && !memcmp(buf, data, size); - free(buf); - } - fclose(f); - return same; -} - -/* Put a "Tailscale" icon on the home screen that opens the status page in - * the console's browser. Does nothing if it is already there and current. - * Returns 0 on success. */ -static int -install_launcher_app(void) { - int (*install_title_dir)(const char *, const char *, void *) = 0; - size_t param_size = launcher_param_json_end - launcher_param_json; - size_t icon_size = launcher_icon_png_end - launcher_icon_png; - uint32_t handle; - int err; - - if (file_matches(LAUNCHER_DIR "/sce_sys/param.json", launcher_param_json, param_size) && - file_matches(LAUNCHER_DIR "/sce_sys/icon0.png", launcher_icon_png, icon_size)) { - printf(" already installed\n"); - return 0; - } - - if ((err = sceAppInstUtilInitialize())) { - printf(" sceAppInstUtilInitialize failed: 0x%08x\n", err); - return -1; - } - mkdir(LAUNCHER_DIR, 0755); - mkdir(LAUNCHER_DIR "/sce_sys", 0755); - if (write_file(LAUNCHER_DIR "/sce_sys/param.json", launcher_param_json, param_size) || - write_file(LAUNCHER_DIR "/sce_sys/icon0.png", launcher_icon_png, icon_size)) { - printf(" could not write to %s: %s\n", LAUNCHER_DIR, strerror(errno)); - sceAppInstUtilTerminate(); - return -1; - } - - /* Register just this title where the firmware supports it; otherwise ask - * for a rescan of everything under /user/app. */ - if (!kernel_dynlib_handle(-1, "libSceAppInstUtil.sprx", &handle)) { - install_title_dir = (void *)kernel_dynlib_resolve(-1, handle, "Wudg3Xe3heE"); - } - if (install_title_dir) { - err = install_title_dir(LAUNCHER_TITLE_ID, "/user/app/", 0); - } else { - err = sceAppInstUtilAppInstallAll(0); - } - sceAppInstUtilTerminate(); - if (err) { - printf(" registering the app failed: 0x%08x\n", err); - return -1; - } - printf(" installed (%s), opens http://127.0.0.1:8090/\n", LAUNCHER_TITLE_ID); - return 0; -} - -/* Hand the daemon to the ELF loader on this console and relay what it prints - * for a while, so that the login link reaches whoever sent the installer. */ -static int -start_daemon(int relay_seconds) { - struct sockaddr_in addr = {0}; - struct timeval start, now; - char buf[4096]; - int fd; - - if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { - return -1; - } - addr.sin_family = AF_INET; - addr.sin_port = htons(LOADER_PORT); - addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); - if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) { - close(fd); - return -1; - } - if (write_all(fd, daemon_elf, daemon_elf_end - daemon_elf)) { - close(fd); - return -1; - } - - gettimeofday(&start, 0); - for (;;) { - struct timeval tv = {1, 0}; - fd_set rfds; - - gettimeofday(&now, 0); - if (now.tv_sec - start.tv_sec >= relay_seconds) { - break; - } - FD_ZERO(&rfds); - FD_SET(fd, &rfds); - if (select(fd + 1, &rfds, 0, 0, &tv) <= 0) { - continue; - } - ssize_t n = read(fd, buf, sizeof(buf)); - if (n <= 0) { - break; - } - if (write_all(STDOUT_FILENO, (uint8_t *)buf, n)) { - break; - } - } - close(fd); - return 0; -} - -int -main(void) { - pid_t pid = getpid(); - intptr_t rootvnode; - - setvbuf(stdout, 0, _IONBF, 0); - - /* Run as root outside the sandbox so /data and USB drives are writable. */ - if ((rootvnode = kernel_get_root_vnode())) { - kernel_set_proc_rootdir(pid, rootvnode); - kernel_set_proc_jaildir(pid, 0); - } - kernel_set_ucred_uid(pid, 0); - kernel_set_ucred_ruid(pid, 0); - kernel_set_ucred_svuid(pid, 0); - kernel_set_ucred_rgid(pid, 0); - kernel_set_ucred_svgid(pid, 0); - -#ifdef LAUNCHER_ONLY - /* Test build: only (re)install the home screen icon. */ - printf("Home screen icon:\n"); - return install_launcher_app() ? 1 : 0; -#endif - - printf("Tailscale for PS5 installer\n\n"); - - printf("Daemon payload:\n"); - if (install_daemon(DATA_DIR)) { - notify("Tailscale install failed:\ncould not write to %s", DATA_DIR); - return 1; - } - - printf("Home screen icon:\n"); - install_launcher_app(); - - printf("\nStarting Tailscale...\n"); - if (start_daemon(45)) { - printf("Could not reach the ELF loader on port %d: %s\n", LOADER_PORT, strerror(errno)); - notify("Tailscale is installed but could not be started:\nno ELF loader on port %d.", LOADER_PORT); - return 1; - } - - printf("\nDone. The status page is on port 8090 of this console.\n" - "Tailscale runs until the console restarts. To start it again, send\n" - "%s/%s to the ELF loader.\n", - DATA_DIR, DAEMON_NAME); - return 0; -} diff --git a/launcher/homeicon.c b/launcher/homeicon.c new file mode 100644 index 0000000..307a296 --- /dev/null +++ b/launcher/homeicon.c @@ -0,0 +1,128 @@ +/* Installs the home screen icon the first time the payload runs. + * + * The icon is installed by a separate small payload (appicon/), embedded + * here and handed to the ELF loader on this console, so that the system + * libraries it needs never end up in this process. Build with + * -DICON_HELPER="path/to/appicon.elf"; without it this file does nothing. */ + +#include "homeicon.h" + +#ifdef ICON_HELPER + +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +#define DATA_DIR "/data/tailscale" +/* Records that the icon has been installed, and which version of it. Once + * it exists the icon is left alone, so an icon the user deletes from the + * home screen stays deleted. Remove the file to get the icon back. */ +#define ICON_MARKER DATA_DIR "/icon-installed" +#define ICON_VERSION "1\n" +#define LOADER_PORT 9021 + +extern const uint8_t icon_helper[]; +extern const uint8_t icon_helper_end[]; + +__asm__(".section .rodata\n" + ".balign 16\n" + ".global icon_helper\n" + "icon_helper:\n" + ".incbin \"" ICON_HELPER "\"\n" + ".global icon_helper_end\n" + "icon_helper_end:\n" + ".text\n"); + +static int +marker_is_current(void) { + char buf[16] = {0}; + int fd = open(ICON_MARKER, O_RDONLY); + + if (fd < 0) { + return 0; + } + read(fd, buf, sizeof(buf) - 1); + close(fd); + return !strcmp(buf, ICON_VERSION); +} + +void +home_icon_install_once(void) { + struct sockaddr_in addr = {0}; + struct timeval tv = {1, 0}; + const uint8_t *data = icon_helper; + size_t left = icon_helper_end - icon_helper; + char reply[512] = {0}; + size_t got = 0; + int fd; + + if (marker_is_current()) { + return; + } + mkdir(DATA_DIR, 0755); + + if ((fd = socket(AF_INET, SOCK_STREAM, 0)) < 0) { + return; + } + addr.sin_family = AF_INET; + addr.sin_port = htons(LOADER_PORT); + addr.sin_addr.s_addr = htonl(INADDR_LOOPBACK); + if (connect(fd, (struct sockaddr *)&addr, sizeof(addr))) { + /* No ELF loader on the usual port: go without an icon this time. */ + close(fd); + return; + } + while (left > 0) { + ssize_t n = write(fd, data, left); + if (n <= 0) { + close(fd); + return; + } + data += n; + left -= n; + } + + /* The helper reports "icon: ok" or what went wrong, then exits, which + * closes the connection. Give it 20 seconds. */ + setsockopt(fd, SOL_SOCKET, SO_RCVTIMEO, &tv, sizeof(tv)); + for (int tries = 0; tries < 20 && got < sizeof(reply) - 1; tries++) { + const char *line = strstr(reply, "icon: "); + if (line && strchr(line, '\n')) { + break; + } + ssize_t n = read(fd, reply + got, sizeof(reply) - 1 - got); + if (n == 0) { + break; + } + if (n > 0) { + got += n; + } + } + close(fd); + + if (strstr(reply, "icon: ok")) { + if ((fd = open(ICON_MARKER, O_WRONLY | O_CREAT | O_TRUNC, 0644)) >= 0) { + write(fd, ICON_VERSION, sizeof(ICON_VERSION) - 1); + close(fd); + } + fprintf(stderr, "launcher: home screen icon installed\n"); + } else { + fprintf(stderr, "launcher: home screen icon not installed: %s\n", got ? reply : "no reply from the helper"); + } +} + +#else + +void +home_icon_install_once(void) { +} + +#endif diff --git a/launcher/homeicon.h b/launcher/homeicon.h new file mode 100644 index 0000000..a8c30cc --- /dev/null +++ b/launcher/homeicon.h @@ -0,0 +1,5 @@ +#pragma once + +/* Put the Tailscale icon on the home screen if that has not been done yet. + * Never fails: without an icon everything else still works. */ +void home_icon_install_once(void); diff --git a/launcher/main.c b/launcher/main.c index e2c2402..7379aa3 100644 --- a/launcher/main.c +++ b/launcher/main.c @@ -10,6 +10,7 @@ #include #include "goload.h" +#include "homeicon.h" #ifndef GO_IMAGE #error "GO_IMAGE must name the Go binary to embed" @@ -134,6 +135,8 @@ main(int argc, char **argv) { kernel_set_ucred_rgid(pid, 0); kernel_set_ucred_svgid(pid, 0); + home_icon_install_once(); + if (leave_realtime_class()) { /* Without this a runaway goroutine could hang the console, so do not * take the chance. */ diff --git a/tools/build-installer.ps1 b/tools/build-installer.ps1 deleted file mode 100644 index 4a9c978..0000000 --- a/tools/build-installer.ps1 +++ /dev/null @@ -1,27 +0,0 @@ -# Build the installer payload around an already built daemon payload. -# .\tools\build-installer.ps1 [-Daemon out\tailscale.elf] [-Send] -param( - [string]$Daemon = 'out\tailscale.elf', - [string]$Out = 'out\tailscale-installer.elf', - [switch]$Send, - [int]$Seconds = 70 -) - -$ErrorActionPreference = 'Stop' -. (Join-Path $PSScriptRoot 'env.ps1') - -$daemonPath = (Resolve-Path (Join-Path $DevRoot $Daemon)).Path -replace '\\', '/' -$assets = (Join-Path $DevRoot 'installer\assets') -replace '\\', '/' -$outPath = Join-Path $DevRoot $Out - -# The app installer library only loads when these come with it, in this -# order (as in the SDK's install_app sample). With libSceAppInstUtil alone -# the payload never starts: the loader leaves it stopped. -Invoke-PS5CC -O2 -Wall "-DDAEMON_ELF=`"$daemonPath`"" "-DASSET_DIR=`"$assets`"" ` - -lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService ` - -o $outPath (Join-Path $DevRoot 'installer\main.c') - -Write-Host ("built {0} ({1:N1} MB)" -f $outPath, ((Get-Item $outPath).Length / 1MB)) -if ($Send) { - & (Join-Path $PSScriptRoot 'ps5send.ps1') -File $outPath -Seconds $Seconds -} diff --git a/tools/build-payload.ps1 b/tools/build-payload.ps1 index 52c534a..10cf695 100644 --- a/tools/build-payload.ps1 +++ b/tools/build-payload.ps1 @@ -1,4 +1,5 @@ # Build a Go program for the PS5 and wrap it in the launcher payload. +# .\tools\build-payload.ps1 -GoDir tsd -Name tailscale -Version 0.4.1 -HomeIcon # .\tools\build-payload.ps1 -GoDir probe-go -Name probe [-DebugLoader] [-Watchdog 120] [-Send] param( [Parameter(Mandatory = $true)][string]$GoDir, @@ -9,6 +10,7 @@ param( [string]$MaxProcs = '', # GOMAXPROCS for the Go program (launcher default: 4) [string]$GoDebug = '', # GODEBUG value baked into the launcher [int]$Watchdog = 0, # test builds: kill the process after this many seconds + [switch]$HomeIcon, # embed the helper that adds the home screen icon on first run [switch]$DebugLoader, # print loader details and early crash registers [switch]$KeepSymbols, [switch]$Send, @@ -39,7 +41,20 @@ if ($DebugLoader) { $ccArgs += '-DGOLOAD_DEBUG' } if ($Watchdog -gt 0) { $ccArgs += "-DGOLOAD_WATCHDOG=$Watchdog" } if ($MaxProcs) { $ccArgs += "-DGO_MAXPROCS=`"$MaxProcs`"" } if ($GoDebug) { $ccArgs += "-DGO_DEBUG=`"$GoDebug`"" } -$ccArgs += @('-o', $elf, (Join-Path $DevRoot 'launcher\main.c'), (Join-Path $DevRoot 'launcher\goload.c')) +if ($HomeIcon) { + # The icon helper is a payload of its own. The app installer library only + # loads when these come with it, in this order (as in the SDK's + # install_app sample); with libSceAppInstUtil alone the payload is never + # started. + $helper = Join-Path $out 'appicon.elf' + $assets = (Join-Path $DevRoot 'appicon') -replace '\\', '/' + Invoke-PS5CC -O2 -Wall "-DASSET_DIR=`"$assets`"" ` + -lSceIpmi -lSceAppInstUtil -lSceUserService -lSceSystemService ` + -o $helper (Join-Path $DevRoot 'appicon\main.c') + $ccArgs += "-DICON_HELPER=`"$($helper -replace '\\', '/')`"" +} +$ccArgs += @('-o', $elf, (Join-Path $DevRoot 'launcher\main.c'), (Join-Path $DevRoot 'launcher\goload.c'), + (Join-Path $DevRoot 'launcher\homeicon.c')) Invoke-PS5CC @ccArgs Write-Host ("built {0} ({1:N1} MB)" -f $elf, ((Get-Item $elf).Length / 1MB)) diff --git a/tsd/main.go b/tsd/main.go index b0da42a..e98f4ab 100644 --- a/tsd/main.go +++ b/tsd/main.go @@ -122,6 +122,9 @@ type daemon struct { quit chan struct{} quitOnce sync.Once + // removeDataOnExit is set by Uninstall: delete the data directory once + // everything that writes to it has shut down. + removeDataOnExit bool } func (d *daemon) run() error { @@ -204,6 +207,13 @@ func (d *daemon) run() error { d.logf("shutdown timed out") } d.logf("stopped") + + d.mu.Lock() + remove := d.removeDataOnExit + d.mu.Unlock() + if remove { + os.RemoveAll(dataDir) + } return nil } diff --git a/tsd/status.html b/tsd/status.html index e2d1c09..572acf7 100644 --- a/tsd/status.html +++ b/tsd/status.html @@ -239,11 +239,8 @@ $('btn-sunshine').onclick = async () => { await act('/api/sunshine?host=' + encodeURIComponent($('sunshine-select').value)); $('sunshine-select').dataset.signature = ''; }; -$('btn-uninstall').onclick = () => { - if (!confirm('Remove Tailscale from this PS5? It stops now and its payload is deleted.')) return; - const purge = confirm('Also log out and delete the saved login?\n\nOK: delete everything.\nCancel: keep the login, so reinstalling reconnects without logging in again.'); - act('/api/uninstall' + (purge ? '?purge=1' : '')); -}; +$('btn-uninstall').onclick = () => act('/api/uninstall', + 'Remove Tailscale from this PS5?\n\nThis logs the console out of your tailnet, deletes its settings and logs, and stops Tailscale.'); $('logbox').addEventListener('toggle', refresh); refresh(); diff --git a/tsd/uninstall.go b/tsd/uninstall.go deleted file mode 100644 index 7783e1e..0000000 --- a/tsd/uninstall.go +++ /dev/null @@ -1,30 +0,0 @@ -package main - -import ( - "errors" - "io/fs" - "os" - "path/filepath" -) - -// daemonFileName is the name the installer stores the daemon payload under, -// in the data directory. -const daemonFileName = "tailscale.elf" - -// uninstall deletes the installed daemon payload. With purge it also deletes -// the Tailscale state and config, which forgets the login. The running -// process is not affected. -func uninstall(purge bool) error { - var errs []error - if err := os.Remove(filepath.Join(dataDir, daemonFileName)); err != nil && !errors.Is(err, fs.ErrNotExist) { - errs = append(errs, err) - } - if purge { - for _, name := range []string{"state", "config.json", ".cache"} { - if err := os.RemoveAll(filepath.Join(dataDir, name)); err != nil { - errs = append(errs, err) - } - } - } - return errors.Join(errs...) -} diff --git a/tsd/web.go b/tsd/web.go index 2427371..8aaa204 100644 --- a/tsd/web.go +++ b/tsd/web.go @@ -302,22 +302,20 @@ func (d *daemon) stop() { d.quitOnce.Do(func() { close(d.quit) }) } -// handleUninstall removes the installed payload, then stops. With ?purge=1 it -// first logs the console out of the tailnet and deletes the saved state as -// well. +// handleUninstall logs the console out of the tailnet and stops the daemon, +// which deletes its data directory (login, settings, logs) on the way out. +// The payload file itself is wherever the user keeps it, and the home screen +// icon can only be deleted from the home screen. func (d *daemon) handleUninstall(w http.ResponseWriter, r *http.Request) { - purge := r.URL.Query().Get("purge") == "1" - if purge && d.lc != nil { + if d.lc != nil { if err := d.lc.Logout(r.Context()); err != nil { d.logf("uninstall: logout: %v", err) } } - if err := uninstall(purge); err != nil { - d.logf("uninstall: %v", err) - http.Error(w, err.Error(), http.StatusInternalServerError) - return - } - d.logf("uninstalled (purge=%v)", purge) + d.logf("uninstall requested from the status page") + d.mu.Lock() + d.removeDataOnExit = true + d.mu.Unlock() notify("Tailscale was removed from this PS5.") io.WriteString(w, "uninstalled\n") d.stop()