Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 31 additions & 19 deletions platforms/android/app/src/main/assets/init-alpine.sh
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export TERM=xterm-256color

INSTALLING=false
FAILSAFE=false
PREPARE=false

# Parse internal flags
while [ $# -gt 0 ]; do
Expand All @@ -18,6 +19,10 @@ while [ $# -gt 0 ]; do
FAILSAFE=true
shift
;;
--prepare)
PREPARE=true
shift
;;
--)
shift
break
Expand All @@ -37,10 +42,13 @@ fi
required_packages="bash command-not-found tzdata wget"
missing_packages=""

installed_packages=" $(apk info -e $required_packages 2>/dev/null | tr '\n' ' ') "

for pkg in $required_packages; do
if ! apk info -e "$pkg" >/dev/null 2>&1; then
missing_packages="$missing_packages $pkg"
fi
case "$installed_packages" in
*" $pkg "*) ;;
*) missing_packages="$missing_packages $pkg" ;;
esac
done

if [ -n "$missing_packages" ]; then
Expand Down Expand Up @@ -289,24 +297,27 @@ _acode_preexec() {
check_binary_execution "$cmd"
}

# Preserve any existing DEBUG trap and append our handler instead of overwriting it.
# This avoids clobbering user-installed preexec hooks (starship, fzf, bash-preexec, etc.).
__acode_existing_debug_trap="$(trap -p DEBUG 2>/dev/null)"
if [[ -n "${__acode_existing_debug_trap}" ]]; then
__acode_existing_cmd="$(printf "%s" "${__acode_existing_debug_trap}" | sed -E "s/.*'((.*)?)'.*/\1/")"
else
__acode_existing_cmd=""
fi

# Only add our handler if it's not already present
if [[ "${__acode_existing_cmd}" != *"_acode_preexec"* ]]; then
if [[ -n "${__acode_existing_cmd}" ]]; then
trap "${__acode_existing_cmd}; _acode_preexec" DEBUG
# Only Android storage needs the check; it costs two subshells per command.
if [ -d /sdcard ] || [ -d /storage ]; then
# Preserve any existing DEBUG trap and append our handler instead of overwriting it.
# This avoids clobbering user-installed preexec hooks (starship, fzf, bash-preexec, etc.).
__acode_existing_debug_trap="$(trap -p DEBUG 2>/dev/null)"
if [[ -n "${__acode_existing_debug_trap}" ]]; then
__acode_existing_cmd="$(printf "%s" "${__acode_existing_debug_trap}" | sed -E "s/.*'((.*)?)'.*/\1/")"
else
trap '_acode_preexec' DEBUG
__acode_existing_cmd=""
fi

# Only add our handler if it's not already present
if [[ "${__acode_existing_cmd}" != *"_acode_preexec"* ]]; then
if [[ -n "${__acode_existing_cmd}" ]]; then
trap "${__acode_existing_cmd}; _acode_preexec" DEBUG
else
trap '_acode_preexec' DEBUG
fi
fi
unset __acode_existing_debug_trap __acode_existing_cmd
fi
unset __acode_existing_debug_trap __acode_existing_cmd

# Command-not-found handler
command_not_found_handle() {
Expand Down Expand Up @@ -352,7 +363,8 @@ fi

chmod +x "$ALPINE_ROOT/initrc"

if [ "$FAILSAFE" != true ]; then
# --prepare only sets up the environment; the caller serves the terminals
if [ "$FAILSAFE" != true ] && [ "$PREPARE" != true ]; then
#everytime a terminal is started initrc will run
"$PREFIX/axs" -c "bash --rcfile /initrc -i"
fi
41 changes: 32 additions & 9 deletions platforms/ios/Alpine/Bridge/AlpineRuntime.c
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,14 @@
#include "fs/devices.h"
#include "fs/path.h"
#include "fs/real.h"
#include "fs/sockrestart.h"
#include "AlpineSpawn.h"

extern int fakefs_bind_mount(const char *, const char *, bool);
extern int do_wait(int, pid_t_, struct siginfo_ *, struct rusage_ *, int);
static struct task *init_task;
static AlpineExitCallback on_exit;
static int attach_descriptors(void *argument);
static void process_exit(struct task *task, int status);
static struct fd *host_descriptor(int number);
static void *failed_process(void *task);
Expand Down Expand Up @@ -90,19 +93,23 @@ int alpine_chmod(const char *path, unsigned int mode) {
}

int alpine_start(const char *command, const char *environment, int input, int output, int error) {
int descriptors[] = {input, output, error};
return alpine_spawn(command, environment, attach_descriptors, descriptors);
}

int alpine_spawn(const char *command, const char *environment, int (*attach)(void *), void *argument) {
current = init_task;
int result = become_new_init_child();
if (result < 0) { current = NULL; return result; }
int descriptors[] = {input, output, error};
for (int index = 0; index < 3; index++) {
current->files->files[index] = host_descriptor(descriptors[index]);
result = attach(argument);
if (result >= 0) {
size_t length = strlen(command);
char *arguments = calloc(1, length + 14);
memcpy(arguments, "/bin/sh\0-c\0", 11);
memcpy(arguments + 11, command, length);
result = do_execve("/bin/sh", 3, arguments, environment);
free(arguments);
}
size_t length = strlen(command);
char *arguments = calloc(1, length + 14);
memcpy(arguments, "/bin/sh\0-c\0", 11);
memcpy(arguments + 11, command, length);
result = do_execve("/bin/sh", 3, arguments, environment);
free(arguments);
int pid = current->pid;
sigset_t wake, previous;
sigemptyset(&wake);
Expand Down Expand Up @@ -185,6 +192,22 @@ int alpine_unmount(void) {
return result;
}

void alpine_suspend(void) {
sockrestart_on_suspend();
}

void alpine_resume(void) {
sockrestart_on_resume();
}

static int attach_descriptors(void *argument) {
int *descriptors = argument;
for (int index = 0; index < 3; index++) {
current->files->files[index] = host_descriptor(descriptors[index]);
}
return 0;
}

static void process_exit(struct task *task, int status) {
if (on_exit != NULL) on_exit(task->tgid, (status & 0x7f) ? 128 + (status & 0x7f) : status >> 8);
}
Expand Down
5 changes: 5 additions & 0 deletions platforms/ios/Alpine/Bridge/AlpineSpawn.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
#pragma once

// Runs `/bin/sh -c command` as a new child of PID 1. `attach` runs as the new
// task before exec to install its stdio; a negative result fails the spawn.
int alpine_spawn(const char *command, const char *environment, int (*attach)(void *), void *argument);
136 changes: 136 additions & 0 deletions platforms/ios/Alpine/Bridge/AlpineTerminal.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,136 @@
#include "AcodeAlpine.h"
#include <stdio.h>
#include <stdlib.h>
#include "kernel/errno.h"
#include "kernel/init.h"
#include "kernel/signal.h"
#include "kernel/task.h"
#include "fs/devices.h"
#include "fs/tty.h"
#include "AlpineSpawn.h"

// The guest side is a regular /dev/pts slave, so shells get job control,
// termios and SIGWINCH. The host side is this driver instead of a pty master,
// which keeps terminal I/O out of emulated sockets and polling.
struct AlpineTerminal {
struct tty *tty;
int rows;
int cols;
void *context;
AlpineTerminalCallbacks callbacks;
lock_t lock;
cond_t drained;
bool closed;
};

static int terminal_write(struct tty *tty, const void *data, size_t length, bool blocking);
static void terminal_cleanup(struct tty *tty);
static int attach_terminal(void *argument);

static const struct tty_driver_ops terminal_ops = {
.write = terminal_write,
.cleanup = terminal_cleanup,
};
// pty_open_fake fills in the slot table and major number.
static struct tty_driver terminal_driver = {.ops = &terminal_ops};

int alpine_terminal_start(const char *command, const char *environment, int rows, int cols,
void *context, AlpineTerminalCallbacks callbacks, AlpineTerminal **handle) {
*handle = NULL;
AlpineTerminal *terminal = calloc(1, sizeof(AlpineTerminal));
if (terminal == NULL) { callbacks.release(context); return _ENOMEM; }
*terminal = (AlpineTerminal) {.rows = rows, .cols = cols, .context = context, .callbacks = callbacks};
lock_init(&terminal->lock);
cond_init(&terminal->drained);
int pid = alpine_spawn(command, environment, attach_terminal, terminal);
if (terminal->tty == NULL) {
callbacks.release(context);
cond_destroy(&terminal->drained);
free(terminal);
} else if (pid < 0) {
alpine_terminal_close(terminal);
} else {
*handle = terminal;
}
return pid;
}

long alpine_terminal_input(AlpineTerminal *terminal, const char *data, size_t length) {
return tty_input(terminal->tty, data, length, false);
}

void alpine_terminal_resize(AlpineTerminal *terminal, int rows, int cols) {
struct tty *tty = terminal->tty;
lock(&tty->lock);
tty->winsize = (struct winsize_) {.row = rows, .col = cols};
pid_t_ foreground = tty->fg_group;
unlock(&tty->lock);
if (foreground != 0) send_group_signal(foreground, SIGWINCH_, SIGINFO_NIL);
}

void alpine_terminal_drained(AlpineTerminal *terminal) {
lock(&terminal->lock);
notify(&terminal->drained);
unlock(&terminal->lock);
}

void alpine_terminal_close(AlpineTerminal *terminal) {
lock(&terminal->lock);
terminal->closed = true;
notify(&terminal->drained);
unlock(&terminal->lock);
struct tty *tty = terminal->tty;
lock(&tty->lock);
pid_t_ session = tty->session;
pid_t_ foreground = tty->fg_group;
tty_hangup(tty);
unlock(&tty->lock);
// Matches Linux when the master side of a pty goes away.
if (foreground != 0) send_group_signal(foreground, SIGHUP_, SIGINFO_NIL);
if (session != 0 && session != foreground) send_group_signal(session, SIGHUP_, SIGINFO_NIL);
lock(&ttys_lock);
tty_release(tty);
unlock(&ttys_lock);
}

// Runs on guest threads. The tty lock may be held (echo), so it must not re-enter the tty.
// A blocking writer waits like on a full pipe while the client is behind. wait_for
// returns _EINTR for pending guest signals, so Ctrl-C and SIGKILL still get through,
// and nothing has been written yet when it does.
static int terminal_write(struct tty *tty, const void *data, size_t length, bool blocking) {
AlpineTerminal *terminal = tty->data;
if (blocking) {
int result = 0;
lock(&terminal->lock);
while (result == 0 && !terminal->closed && terminal->callbacks.congested(terminal->context)) {
struct timespec recheck = {.tv_nsec = 100 * 1000 * 1000};
result = wait_for(&terminal->drained, &terminal->lock, &recheck);
if (result == _ETIMEDOUT) result = 0;
}
unlock(&terminal->lock);
if (result < 0) return result;
}
terminal->callbacks.output(terminal->context, data, length);
return 0;
}

// Runs once the host and every guest descriptor have released the tty.
static void terminal_cleanup(struct tty *tty) {
AlpineTerminal *terminal = tty->data;
terminal->callbacks.release(terminal->context);
cond_destroy(&terminal->drained);
free(terminal);
}

// Runs as the new session leader, so opening the slave makes it the controlling tty.
static int attach_terminal(void *argument) {
AlpineTerminal *terminal = argument;
struct tty *tty = pty_open_fake(&terminal_driver);
if (IS_ERR(tty)) return (int) PTR_ERR(tty);
tty->data = terminal;
tty->winsize = (struct winsize_) {.row = terminal->rows, .col = terminal->cols};
terminal->tty = tty;
char path[32];
snprintf(path, sizeof(path), "/dev/pts/%d", tty->num);
return create_stdio(path, TTY_PSEUDO_SLAVE_MAJOR, tty->num);
}
27 changes: 24 additions & 3 deletions platforms/ios/Alpine/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,26 @@

The existing Acode xterm UI connects to the same AXS HTTP/WebSocket API used on
Android. ARM64 Linux programs run inside ios-linuxkit's interpreter; they are not
launched as unsigned iOS executables. The native Executor services use guest
processes and pipes. Apple Network.framework supplies the raw process WebSocket
transport for `Executor.spawnStream`.
launched as unsigned iOS executables.

On iOS that API is served by the app, not by AXS inside the guest. Only the
shells are emulated:

```
xterm.js ──ws/http──▶ TerminalServer (Swift, 127.0.0.1:8767)
└─ TerminalSession ──▶ Bridge/AlpineTerminal.c ──▶ /dev/pts/N ──▶ bash
```

`AlpineTerminal.c` registers a tty driver whose output goes straight to Swift
and whose input, resize and hangup go straight to the guest tty. Terminal I/O
therefore never uses emulated sockets, epoll or threads, and the guest pty still
provides job control, termios and `SIGWINCH`. `init-alpine.sh --prepare` runs
once per boot; each terminal then starts `bash --rcfile /initrc -i` directly.
The AXS binary stays in the guest as `axs` for users and plugins.

The native Executor services use guest processes and pipes. Apple
Network.framework supplies the raw process WebSocket transport for
`Executor.spawnStream`.

## Sources

Expand Down Expand Up @@ -55,6 +72,10 @@ SIGUSR1` in Xcode's debug console, then `continue`.
- `Bridge/AlpineRuntime.c` owns guest init, stdio, process cleanup and mounts.
SIGUSR1 is unblocked only while creating guest threads, then the host worker's
signal mask is restored.
- `Bridge/AlpineTerminal.c` connects guest pseudo-terminals to Swift through
`pty_open_fake`, as iSH does for its terminal view.
- `alpine_suspend`/`alpine_resume` run upstream `sockrestart` when the app moves
to the background and back, so guest servers keep their listening sockets.

The guest root lives at `Library/Alpine`, outside every host bind mount. `/public`,
`/home` and `/root` share Acode's Terminal Public directory. `/acode` maps app data,
Expand Down
3 changes: 2 additions & 1 deletion platforms/ios/Alpine/Vendor/ios-linuxkit/meson.build
Original file line number Diff line number Diff line change
Expand Up @@ -320,7 +320,8 @@ if meson.is_cross_build()
include_directories: [include_directories('deps'), include_directories('deps/libarchive/libarchive')],
c_args: ['-DHAVE_CONFIG_H', '-I' + meson.get_external_property('sdk_path') + '/usr/include/libxml2', '-w'])
acode_alpine = static_library('acode_alpine',
['../../Bridge/AlpineRuntime.c', '../../Bridge/AlpineFaults.c', '../../Bridge/AlpineArchive.c', 'tools/fakefs.c'],
['../../Bridge/AlpineRuntime.c', '../../Bridge/AlpineTerminal.c', '../../Bridge/AlpineFaults.c',
'../../Bridge/AlpineArchive.c', 'tools/fakefs.c'],
include_directories: [includes, include_directories('deps/libarchive/libarchive')],
c_args: ['-I' + meson.project_source_root() + '/../../include'])
endif
23 changes: 23 additions & 0 deletions platforms/ios/Alpine/include/AcodeAlpine.h
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,14 @@
#include <stddef.h>

typedef void (*AlpineExitCallback)(int pid, int status);
typedef struct AlpineTerminal AlpineTerminal;
// `output` and `congested` run on guest threads. `congested` tells blocking writers
// to wait until alpine_terminal_drained; `release` runs once the terminal is gone.
typedef struct {
void (*output)(void *context, const char *data, size_t length);
bool (*congested)(void *context);
void (*release)(void *context);
} AlpineTerminalCallbacks;

// Calls that access guest state are serialized by AlpineRuntime.queue.
int alpine_boot(const char *root, AlpineExitCallback callback);
Expand All @@ -17,3 +25,18 @@ bool alpine_idle(void);
int alpine_unmount(void);
bool alpine_import(const char *archive, const char *root, char *error, size_t capacity);
void alpine_install_fault_handlers(void);
// Recreate guest listening sockets that iOS reclaims while the app is suspended.
void alpine_suspend(void);
void alpine_resume(void);

// Runs a command on a new pseudo-terminal and returns its pid. The context is
// owned by the terminal from this call on, including when starting fails.
int alpine_terminal_start(const char *command, const char *environment, int rows, int cols,
void *context, AlpineTerminalCallbacks callbacks, AlpineTerminal **terminal);
// Input, resize, drained and close are thread-safe and do not need AlpineRuntime.queue.
long alpine_terminal_input(AlpineTerminal *terminal, const char *data, size_t length);
void alpine_terminal_resize(AlpineTerminal *terminal, int rows, int cols);
void alpine_terminal_drained(AlpineTerminal *terminal);
// Releases waiting writers, hangs up the session and drops the host's reference;
// the handle is invalid afterwards.
void alpine_terminal_close(AlpineTerminal *terminal);
Loading