ios_system/ios_system.m

3742 lines
177 KiB
Objective-C

//
// ios_system.m
//
// Created by Nicolas Holzschuch on 17/11/2017.
// Copyright © 2017 N. Holzschuch. All rights reserved.
//
#import <UIKit/UIKit.h>
#include "ios_system.h"
// ios_system(cmd): Executes the command in "cmd". The goal is to be a drop-in replacement for system(), as much as possible.
// We assume cmd is the command. If vim has prepared '/bin/sh -c "(command -arguments) < inputfile > outputfile",
// it is easier to remove the "/bin/sh -c" part before calling ios_system than inside ios_system.
// See example in (iVim) os_unix.c
//
// ios_executable(cmd): returns true if the command is one of the commands defined in ios_system, and can be executed.
// This is because mch_can_exe (called by executable()) checks for the existence of binaries with the same name in the
// path. Our commands don't exist in the path.
//
// ios_popen(cmd, type): returns a FILE*, executes cmd, and thread_output into input of cmd (if type=="w") or
// the reverse (if type == "r").
#include <pthread.h>
#include <sys/stat.h>
#include <libgen.h> // for basename()
#include <dlfcn.h> // for dlopen()/dlsym()/dlclose()
#include <glob.h> // for wildcard expansion
// Sideloading: when you compile yourself, as opposed to uploading on the app store
// If true, all commands are enabled + debug messages if dylib not found.
// If false, you get a smaller set, but compliance with AppStore rules.
// *Must* be false in the main branch releases.
// Commands that can be enabled only if sideLoading: chgrp, chown, df, id, w.
bool sideLoading = false;
// Should the main thread be joined (which means it takes priority over other tasks)?
// Default value is true, which makes sense for shell-like applications.
// Should be set to false if significant user interaction is carried by the app and
// the app takes responsibility for waiting for the command to terminate.
bool joinMainThread = true;
static NSString* ios_bookmarkDictionaryName = @"bookmarkNames";
// Include file for getrlimit/setrlimit:
#include <sys/resource.h>
static struct rlimit limitFilesOpen;
extern void display_alert(NSString* title, NSString* message);
extern __thread int __db_getopt_reset;
__thread FILE* thread_stdin;
__thread FILE* thread_stdout;
__thread FILE* thread_stderr;
__thread void* thread_context;
FILE* ios_stdin(void) {
return thread_stdin;
}
FILE* ios_stdout(void) {
return thread_stdout;
}
FILE* ios_stderr(void) {
return thread_stderr;
}
void* ios_context(void) {
return thread_context;
}
// Parameters for each session. We can have multiple sessions running in parallel.
typedef struct _sessionParameters {
bool isMainThread; // are we on the first command?
char currentDir[MAXPATHLEN];
char previousDirectory[MAXPATHLEN];
char localMiniRoot[MAXPATHLEN];
pthread_t current_command_root_thread; // thread ID of first command
pthread_t lastThreadId; // thread ID of last command.
pthread_t mainThreadId; // thread ID of parent command, if any (e.g. vim, which starts "sh -c cd dir && flake8 file")
FILE* stdin;
FILE* stdout;
FILE* stderr;
FILE* tty;
void* context;
int global_errno;
int numCommandsAllocated;
int numCommand;
char** commandName;
char columns[5];
char lines[5];
bool activePager;
} sessionParameters;
static void initSessionParameters(sessionParameters* sp) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
sp->isMainThread = TRUE;
sp->current_command_root_thread = 0;
sp->lastThreadId = 0;
sp->mainThreadId = 0;
NSString* currentDirectory = [fileManager currentDirectoryPath];
strcpy(sp->currentDir, [currentDirectory UTF8String]);
strcpy(sp->previousDirectory, [currentDirectory UTF8String]);
sp->localMiniRoot[0] = 0;
sp->global_errno = 0;
sp->stdin = stdin;
sp->stdout = stdout;
sp->stderr = stderr;
sp->tty = stdin;
sp->context = nil;
sp->numCommandsAllocated = 10; // 10 slots available to store commands, will realloc if more needed.
sp->commandName = malloc(sizeof(char*) * sp->numCommandsAllocated);
for (int i = 0; i < sp->numCommandsAllocated; i++) {
sp->commandName[i] = malloc(sizeof(char) * NAME_MAX);
}
sp->commandName[0][0] = 0;
sp->numCommand = 0;
strcpy(sp->columns, "80");
strcpy(sp->lines, "80");
sp->activePager = FALSE;
}
void ios_setBookmarkDictionaryName(NSString* name) {
ios_bookmarkDictionaryName = name;
}
const char* ios_getBookmarkedVersion(const char* p) {
// p is a directory. Get the bookmarked version to make it shorter:
NSString* pathString = [NSString stringWithUTF8String:p];
NSString* privatePrefix = @"/private";
if ([pathString hasPrefix:privatePrefix]) {
pathString = [pathString substringFromIndex:[privatePrefix length]];
}
NSString *homePath;
homePath = [[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject] stringByDeletingLastPathComponent];
if ([homePath hasPrefix:privatePrefix]) {
homePath = [homePath substringFromIndex:[privatePrefix length]];
}
// NSLog(@"ios_getBookmarkedVersion: %s %s", homePath.UTF8String, pathString.UTF8String);
if ([pathString hasPrefix:homePath]) {
pathString = [pathString stringByReplacingOccurrencesOfString:homePath withString:@"~"];
return pathString.UTF8String;
}
if (ios_bookmarkDictionaryName == nil) {
return p;
}
NSDictionary *tildeExpansionDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ios_bookmarkDictionaryName];
if (tildeExpansionDictionary == nil) {
return p;
}
NSString* foundString = @"";
for (NSString* bookmark in tildeExpansionDictionary) {
NSString* bookmarkPath = tildeExpansionDictionary[bookmark];
if ([bookmarkPath hasPrefix:privatePrefix]) {
bookmarkPath = [bookmarkPath substringFromIndex:[privatePrefix length]];
}
if ([pathString hasPrefix:bookmarkPath]) {
NSString* testString = [pathString stringByReplacingOccurrencesOfString:bookmarkPath withString:[@"~" stringByAppendingString: bookmark]];
if ((foundString.length == 0) || (testString.length < foundString.length))
foundString = testString;
}
}
if (foundString.length > 0)
return foundString.UTF8String;
return p;
}
static NSMutableDictionary* sessionList;
static NSMutableDictionary* aliasDictionary;
// pointer to sessionParameters. thread-local variable so the entire system is thread-safe.
// The sessionParameters pointer is shared by all threads in the same session.
static __thread sessionParameters* currentSession;
// Python3 multiple interpreters:
// limit to 6 = 1 kernel, 4 notebooks, one extra.
static const int MaxPythonInterpreters = 6; // const so we can allocate an array
int numPythonInterpreters = MaxPythonInterpreters; // Apps can overwrite this
static bool PythonIsRunning[MaxPythonInterpreters];
static int currentPythonInterpreter = 0;
static bool showPythonInterpreterAlert = true;
// Same with perl:
static const int MaxPerlInterpreters = 3; // const so we can allocate an array
// cpan starts perl Makefile.PL, which starts perl -e print Version, so at least 3.
int numPerlInterpreters = MaxPerlInterpreters; // Apps can overwrite this
static bool PerlIsRunning[MaxPerlInterpreters];
static int currentPerlInterpreter = 0;
// same with TeX, with a twist:
static const int MaxTeXInterpreters = 2; // const so we can allocate an array
// (La)TeX can start another (La)TeX command for TikZ
int numTeXInterpreters = MaxTeXInterpreters; // Apps can overwrite this
static bool TeXIsRunning[MaxTeXInterpreters];
static int currentTeXInterpreter = 0;
NSArray *TeXcommands = nil; // initialized later
// Multiple dash:
// limit to 6 (for now)
static const int MaxDashCommands = 6; // const so we can allocate an array
int numDashCommands = MaxDashCommands; // Apps can overwrite this
static bool dashIsRunning[MaxDashCommands];
static int currentDashCommand = 0;
// pointers for sh sessions:
char* sh_session = "sh_session";
// replace system-provided exit() by our own:
void ios_exit(int n) {
if (currentSession != NULL) {
currentSession->global_errno = n;
}
pthread_exit(NULL);
}
void set_session_errno(int n) {
if (currentSession != NULL) {
currentSession->global_errno = n;
}
}
// Replace standard abort and exit functions with ours:
// We also do this using #define, but this is for the unmodified code.
void abort(void) {
ios_exit(1);
}
void exit(int n) {
ios_exit(n);
}
void _exit(int n) {
ios_exit(n);
}
//
void ios_signal(int signal) {
// This function is probably obsolete now. If we keep using it, remember that currentSession is not necessarily the currentSession
// (if currentSession started sh_session, then we might be sending the signal to the wrong session).
// Signals the threads of the current session:
if (currentSession != NULL) {
if (currentSession->current_command_root_thread != NULL) {
pthread_kill(currentSession->current_command_root_thread, signal);
}
if (currentSession->lastThreadId != NULL) {
pthread_kill(currentSession->lastThreadId, signal);
}
if (currentSession->mainThreadId != NULL) {
pthread_kill(currentSession->mainThreadId, signal);
}
}
}
NSString *ios_getLogicalPWD(const void* sessionId) {
id sessionKey = @((NSUInteger)sessionId);
if (sessionList == nil) {
return nil;
}
sessionParameters *session = (sessionParameters*)[[sessionList objectForKey: sessionKey] pointerValue];
if (session == nil) {
return nil;
}
return @(session->currentDir);
}
#undef getenv
void ios_setWindowSize(int width, int height, const void* sessionId) {
// You can set the window size for a session that is not currently running (e.g. because "sh_session" is running).
// So we set it without calling ios_switchSession:
sessionParameters* resizedSession;
id sessionKey = @((NSUInteger)sessionId);
if (sessionList == nil) {
return;
}
resizedSession = (sessionParameters*)[[sessionList objectForKey: sessionKey] pointerValue];
if (resizedSession == nil) {
return;
}
sprintf(resizedSession->columns, "%d", MIN(width, 9999));
sprintf(resizedSession->lines, "%d", MIN(height, 9999));
// Also send SIGWINCH to the main thread of resizedSession:
if (resizedSession->current_command_root_thread != NULL) {
pthread_kill(resizedSession->current_command_root_thread, SIGWINCH);
}
if (resizedSession->lastThreadId != NULL) {
pthread_kill(resizedSession->lastThreadId, SIGWINCH);
}
if (resizedSession->mainThreadId != NULL) {
pthread_kill(resizedSession->mainThreadId, SIGWINCH);
}
}
extern char* libc_getenv(const char* variableName);
char * ios_getenv(const char *name) {
// intercept calls to getenv("COLUMNS") / getenv("LINES")
if (strcmp(name, "COLUMNS") == 0) {
return currentSession->columns;
}
if (strcmp(name, "LINES") == 0) {
return currentSession->lines;
}
if (strcmp(name, "ROWS") == 0) {
return currentSession->lines;
}
if (strcmp(name, "PWD") == 0) {
return currentSession->currentDir;
}
return libc_getenv(name);
}
void ios_IsMainThread(bool value) {
currentSession->isMainThread = value;
}
int ios_getCommandStatus() {
if (currentSession != NULL) return currentSession->global_errno;
else return 0;
}
extern const char* ios_progname(void) {
if (currentSession != NULL) {
if (currentSession->numCommand <= 0)
return currentSession->commandName[0];
else
return currentSession->commandName[currentSession->numCommand - 1];
}
else return getprogname();
}
const char* ios_expandtilde(const char *login) {
// expand "~something" with the content of userPreference dictionary (to be set by each app)
// About the same behaviour as:
// struct passwd *pw = getpwnam(name);
// return pw ? pw->pw_dir : 0;
NSDictionary *tildeExpansionDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ios_bookmarkDictionaryName];
if (tildeExpansionDictionary != nil) {
NSString* name = [NSString stringWithUTF8String:login];
NSString* expandedPath = tildeExpansionDictionary[name];
if (expandedPath != nil) {
return [expandedPath UTF8String];
}
}
return NULL;
}
typedef struct _functionParameters {
int argc;
char** argv;
char** argv_ref;
int (*function)(int ac, char** av);
FILE *stdin, *stdout, *stderr;
void* context;
void* dlHandle;
bool isPipeIn;
bool isPipeOut;
bool isPipeErr;
bool backgroundCommand;
int numInterpreter;
bool storeRootThread;
sessionParameters* session;
} functionParameters;
extern pthread_mutex_t pid_mtx;
extern _Atomic(int) cleanup_counter;
extern void ios_releaseBackgroundThread(pthread_t thread);
extern void startedPreparingWebAssemblyCommand(void);
static void cleanup_function(void* parameters) {
// This function is called when pthread_exit() or ios_kill() is called
pthread_t current_thread = pthread_self();
functionParameters *p = (functionParameters *) parameters;
bool backgroundCommand = p->backgroundCommand;
char* commandName = p->argv[0];
char* currentSessionCommandName = NULL;
if (currentSession->numCommand <= 0)
currentSessionCommandName = currentSession->commandName[0];
else
currentSessionCommandName = currentSession->commandName[currentSession->numCommand - 1];
NSLog(@"cleanup_function: %s thread_id %x pid: %d stdin %d stdout %d stderr %d isPipeOut %d", commandName, current_thread, ios_currentPid(), fileno(p->stdin), fileno(p->stdout), fileno(p->stderr), p->isPipeOut);
NSLog(@"currentSession->commandName: %s root_thread: %x", currentSessionCommandName, currentSession->current_command_root_thread);
NSLog(@"Num commands stored: %d", currentSession->numCommand);
if ((strcmp(commandName, "less") == 0) || (strcmp(commandName, "more") == 0)) {
if ((strlen(currentSessionCommandName) > 0)
&& (strcmp(currentSessionCommandName, "less") != 0)
&& (strcmp(currentSessionCommandName, "more") != 0)) {
// Command was "root_command | sthg | less". We need to kill root command.
// If less itself started another command, then currentSession->commandName is "".
// Unless less / more was started as a pager, in which case don't kill root command (e.g. for man and ipython help).
pthread_kill(currentSession->current_command_root_thread, SIGINT);
while (fgetc(thread_stdin) != EOF) { } // flush input, otherwise previous command gets blocked.
} else {
// but for python or ipython help(), flush the content of stdin:
if ((currentSession->numCommand > 1) &&
((strncmp(currentSession->commandName[currentSession->numCommand - 2], "ipython", 7) == 0) ||
(strncmp(currentSession->commandName[currentSession->numCommand - 2], "isympy", 6) == 0) ||
(strncmp(currentSession->commandName[currentSession->numCommand - 2], "python", 6) == 0))) {
while (fgetc(thread_stdin) != EOF) { } // flush input to help() command
}
}
currentSession->activePager = FALSE;
}
// If the command was started as a pipe, we wait for the first command to finish sending data
// There is an exception for ssh, which can be started by scp or sftp. They will wait for it.
if ((!joinMainThread) && p->isPipeOut && (strcmp(commandName, "ssh") != 0)) {
if (currentSession->current_command_root_thread != 0) {
if (currentSession->current_command_root_thread != current_thread) {
NSLog(@"Thread %x is waiting for root_thread of currentSession: %x \n", current_thread, currentSession->current_command_root_thread);
while ((currentSession->current_command_root_thread != 0) && (currentSession->current_command_root_thread != current_thread)) { }
NSLog(@"Thread %x is done waiting for root_thread of currentSession: %x \n", current_thread, currentSession->current_command_root_thread);
} else {
NSLog(@"Terminating root_thread of currentSession %x \n", current_thread);
currentSession->current_command_root_thread = 0;
}
}
}
fcntl(fileno(thread_stdin), F_SETNOSIGPIPE);
fcntl(fileno(thread_stdout), F_SETNOSIGPIPE);
fcntl(fileno(thread_stderr), F_SETNOSIGPIPE);
fflush(thread_stdin);
fflush(thread_stdout);
fflush(thread_stderr);
// release parameters:
NSLog(@"Terminating command: %s thread_id %x stdin %d stdout %d stderr %d isPipeOut %d", commandName, current_thread, fileno(p->stdin), fileno(p->stdout), fileno(p->stderr), p->isPipeOut);
// Specific to run multiple python3 interpreters:
NSString* commandNameString = [NSString stringWithCString: commandName encoding:NSUTF8StringEncoding];
// Can we close stdin too?
bool mustCloseStdin = fileno(p->stdin) != fileno(stdin);
if (strncmp(commandName, "python", 6) == 0) {
// It could be one of the multiple python3 interpreters
PythonIsRunning[p->numInterpreter] = false;
mustCloseStdin = false;
}
// Same with multiple perl or TeX interpreters:
else if (strncmp(commandName, "perl", 4) == 0) {
NSLog(@"Ending a Perl interpreter: %d", p->numInterpreter);
PerlIsRunning[p->numInterpreter] = false;
} else if ([TeXcommands containsObject: commandNameString]) {
NSLog(@"Ending a TeX command: %d", p->numInterpreter);
TeXIsRunning[p->numInterpreter] = false;
} else if (strcmp(commandName, "dash") == 0) {
NSLog(@"Ending a dash command: %d", p->numInterpreter);
dashIsRunning[p->numInterpreter] = false;
}
if (currentSession->numCommand > 0)
currentSession->numCommand -= 1;
else
currentSession->commandName[0][0] = 0;
// if (strcmp(currentSession->commandName, commandName) == 0) {
// currentSession->commandName[0] = 0;
// }
bool isSh = strcmp(p->argv[0], "sh") == 0;
bool isWasm = strcmp(p->argv[0], "wasm") == 0;
for (int i = 0; i < p->argc; i++) free(p->argv_ref[i]);
free(p->argv_ref);
free(p->argv);
bool isLastThread = (currentSession->lastThreadId == current_thread);
// Required for Jupyter. Must check for Blink/LibTerm/iVim:
// Is that the issue in iVim?
bool mustCloseStderr = (fileno(p->stderr) != fileno(stderr)) && (fileno(p->stderr) != fileno(p->stdout));
if (!isSh) {
mustCloseStderr &= p->isPipeErr;
if (currentSession != nil) {
mustCloseStderr &= fileno(p->stderr) != fileno(currentSession->stderr);
mustCloseStderr &= fileno(p->stderr) != fileno(currentSession->stdout);
}
}
// Some programs stop waiting as soon as stdout/stderr close (which makes sense)
cleanup_counter++;
while (pthread_mutex_trylock(&pid_mtx) != 0) { } // Someone else has the lock, so we wait.
pthread_mutex_unlock(&pid_mtx);
if (mustCloseStderr) {
NSLog(@"Closing stderr (mustCloseStderr): %d \n", fileno(p->stderr));
int res = fclose(p->stderr);
}
bool mustCloseStdout = fileno(p->stdout) != fileno(stdout);
if (!isSh) {
mustCloseStdout &= p->isPipeOut;
if (currentSession != nil) {
mustCloseStdout &= fileno(p->stdout) != fileno(currentSession->stdout);
}
}
if (mustCloseStdout) {
NSLog(@"Closing stdout (mustCloseStdout): %d \n", fileno(p->stdout));
int res = fclose(p->stdout);
}
if (!isSh) {
mustCloseStdin &= p->isPipeIn;
if (currentSession != nil) {
mustCloseStdin &= fileno(p->stdin) != fileno(currentSession->stdin);
}
// we cannot close stdin for wasm commands:
mustCloseStdin &= !isWasm;
// commands started by Python: Python will close stdin (Lua and Perl? not broken, AFAIK)
if ((currentSession->numCommand > 0) && (strncmp(currentSession->commandName[currentSession->numCommand - 1], "python", 6) == 0)) {
// NSLog(@"Command started by Python, not closing stdin: %d \n", fileno(p->stdin));
mustCloseStdin &= false;
}
}
if (mustCloseStdin) {
NSLog(@"Closing stdin (mustCloseStdin): %d \n", fileno(p->stdin));
int res = fclose(p->stdin);
}
if ((p->dlHandle != RTLD_SELF) && (p->dlHandle != RTLD_MAIN_ONLY)
&& (p->dlHandle != RTLD_DEFAULT) && (p->dlHandle != RTLD_NEXT))
dlclose(p->dlHandle);
free(parameters); // This was malloc'ed in ios_system
if (isLastThread) {
NSLog(@"Terminating lastthread of currentSession %x lastThreadId %x pid: %d\n", current_thread, currentSession->lastThreadId, ios_currentPid());
currentSession->lastThreadId = 0;
} else {
NSLog(@"Current thread %x lastthread %x pid: %d\n", pthread_self(), currentSession->lastThreadId, ios_currentPid());
}
if (backgroundCommand) {
// If it's a background command, call ios_releaseBackgroundThread:
// NSLog(@"Releasing a backgroundCommand\n");
ios_releaseBackgroundThread(current_thread);
} else {
ios_releaseThread(current_thread);
}
if (currentSession->current_command_root_thread == current_thread) {
currentSession->current_command_root_thread = 0;
}
if (currentSession->mainThreadId == current_thread) {
currentSession->mainThreadId = 0;
}
cleanup_counter--;
NSLog(@"returning from cleanup_function\n");
}
// Avoir calling crash_handler several times:
static __thread bool crash_handler_called = false;
void crash_handler(int sig) {
if (thread_stderr == NULL) thread_stderr = stderr;
if (!crash_handler_called) {
crash_handler_called = true;
if (sig == SIGSEGV) {
fputs("segmentation fault\n", thread_stderr);
} else if (sig == SIGBUS) {
fputs("bus error\n", thread_stderr);
} else if (sig == SIGPIPE) {
fputs("pipe error\n", thread_stderr);
return;
}
ios_exit(1);
}
}
static void* run_function(void* parameters) {
functionParameters *p = (functionParameters *) parameters;
NSLog(@"Storing thread_id: %x pid: %d isPipeOut: %x isPipeErr: %x stdin %d stdout %d stderr %d command= %s\n", pthread_self(), ios_currentPid(), p->isPipeOut, p->isPipeErr,
(p->stdin == nil) ? -1 : fileno(p->stdin),
(p->stdout == nil) ? -1 : fileno(p->stdout),
(p->stderr == nil) ? -1 : fileno(p->stderr), p->argv[0]);
ios_storeThreadId(pthread_self());
if (p->storeRootThread && (p->session != NULL)) {
NSLog(@"Storing thread_id: %x as root_thread\n", pthread_self());
p->session->current_command_root_thread = pthread_self();
}
// NSLog(@"Starting command: %s thread_id %x", p->argv[0], pthread_self());
// re-initialize for getopt:
// TODO: move to __thread variable for optind too
optind = 1;
opterr = 1;
optreset = 1;
__db_getopt_reset = 1;
thread_stdin = p->stdin;
thread_stdout = p->stdout;
thread_stderr = p->stderr;
thread_context = p->context;
currentSession = p->session;
if ((strcmp(p->argv[0], "less") == 0) || (strcmp(p->argv[0], "more") == 0)) {
if (currentSession != nil) currentSession->activePager = TRUE;
}
signal(SIGSEGV, crash_handler);
signal(SIGBUS, crash_handler);
signal(SIGPIPE, crash_handler);
// Because some commands change argv, keep a local copy for release.
p->argv_ref = (char **)malloc(sizeof(char*) * (p->argc + 1));
for (int i = 0; i < p->argc; i++) p->argv_ref[i] = p->argv[i];
pthread_cleanup_push(cleanup_function, parameters);
@try
{
int retval = p->function(p->argc, p->argv);
if (currentSession != nil) currentSession->global_errno = retval;
}
@catch (NSException *exception)
{
// Print exception information.
NSLog( @"NSException caught" );
NSLog( @"Name: %@", exception.name);
NSLog( @"Reason: %@", exception.reason );
fprintf(thread_stderr, "Command %s was interrupted because it triggered a system exception: %s: %s\n", p->argv[0], exception.name, exception.reason);
return NULL;
}
@finally
{
// Cleanup, in both success and fail cases
pthread_cleanup_pop(1);
return NULL;
}
}
static NSString* miniRoot = nil; // limit operations to below a certain directory (~, usually).
static NSArray<NSString*> *allowedPaths = nil;
static NSDictionary *commandList = nil;
NSArray *backgroundCommandList = nil;
// do recompute directoriesInPath only if $PATH has changed
static NSString* fullCommandPath = @"";
static NSArray *directoriesInPath;
void initializeEnvironment() {
// setup a few useful environment variables
// Initialize paths for application files, including history.txt and keys
NSString *docsPath;
if (miniRoot == nil) docsPath = [NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject];
else docsPath = miniRoot;
// Where the executables are stored: $PATH + ~/Library/bin + ~/Documents/bin
// Add content of old PATH to this. PATH *is* defined in iOS, surprising as it may be.
// I'm not going to erase it, so we just add ourselves.
// Sometimes, we go through main several times, so make sure we only append to PATH once
NSString* checkingPath = [NSString stringWithCString:getenv("PATH") encoding:NSUTF8StringEncoding];
if (! [fullCommandPath isEqualToString:checkingPath]) {
fullCommandPath = checkingPath;
}
if (![fullCommandPath containsString:@"Documents/bin"]) {
NSString *binPath = [docsPath stringByAppendingPathComponent:@"bin"];
fullCommandPath = [[binPath stringByAppendingString:@":"] stringByAppendingString:fullCommandPath];
setenv("PATH", fullCommandPath.UTF8String, 1); // 1 = override existing value
}
setenv("APPDIR", [[NSBundle mainBundle] resourcePath].UTF8String, 1);
setenv("PATH_LOCALE", docsPath.UTF8String, 0); // CURL config in ~/Documents/ or [Cloud Drive]/
setenv("TERM", "xterm", 1); // 1 = override existing value
setenv("TMPDIR", NSTemporaryDirectory().UTF8String, 0); // tmp directory
setenv("CLICOLOR", "1", 1);
setenv("LSCOLORS", "ExFxBxDxCxegedabagacad", 0); // colors for ls on black background
// We can't write in $HOME so we need to set the position of config files:
setenv("SSH_HOME", docsPath.UTF8String, 0); // SSH keys in ~/Documents/.ssh/ or [Cloud Drive]/.ssh
setenv("DIG_HOME", docsPath.UTF8String, 0); // .digrc is in ~/Documents/.digrc or [Cloud Drive]/.digrc
setenv("CURL_HOME", docsPath.UTF8String, 0); // CURL config in ~/Documents/ or [Cloud Drive]/
setenv("SSL_CERT_FILE", [docsPath stringByAppendingPathComponent:@"cacert.pem"].UTF8String, 0); // SLL cacert.pem in ~/Documents/cacert.pem or [Cloud Drive]/cacert.pem
// iOS already defines "HOME" as the home dir of the application
for (int i = 0; i < MaxPythonInterpreters; i++) PythonIsRunning[i] = false;
for (int i = 0; i < MaxPerlInterpreters; i++) PerlIsRunning[i] = false;
NSString *libPath = [NSSearchPathForDirectoriesInDomains(NSLibraryDirectory, NSUserDomainMask, YES) lastObject];
// environment variables for Python:
setenv("PYTHONHOME", libPath.UTF8String, 0); // Python files are in ~/Library/lib/python[23].x/
// XDG setup directories (~/Library/Caches, ~/Library/Preferences):
setenv("XDG_CACHE_HOME", [libPath stringByAppendingPathComponent:@"Caches"].UTF8String, 0);
setenv("XDG_CONFIG_HOME", [libPath stringByAppendingPathComponent:@"Preferences"].UTF8String, 0);
setenv("XDG_DATA_HOME", libPath.UTF8String, 0);
// if we use Python, we define a few more environment variables:
setenv("PYTHONEXECUTABLE", "python3", 0); // Python executable name for python3
setenv("PYZMQ_BACKEND", "cffi", 0);
// Configuration files are in $HOME (and hidden)
setenv("JUPYTER_CONFIG_DIR", [docsPath stringByAppendingPathComponent:@".jupyter"].UTF8String, 0);
setenv("IPYTHONDIR", [docsPath stringByAppendingPathComponent:@".ipython"].UTF8String, 0);
setenv("MPLCONFIGDIR", [docsPath stringByAppendingPathComponent:@".config/matplotlib"].UTF8String, 0);
// hg config file in ~/Documents/.hgrc
setenv("HGRCPATH", [docsPath stringByAppendingPathComponent:@".hgrc"].UTF8String, 0);
if (![fullCommandPath containsString:@"Library/bin"]) {
NSString *binPath = [libPath stringByAppendingPathComponent:@"bin"];
fullCommandPath = [[binPath stringByAppendingString:@":"] stringByAppendingString:fullCommandPath];
}
if (!sideLoading) {
// If we're not sideloading, executeables will also be in the Application directory
NSString *mainBundlePath = [[NSBundle mainBundle] resourcePath];
NSString *mainBundleLibPath = [mainBundlePath stringByAppendingPathComponent:@"Library"];
// if we're not sideloading, all "executable" files are in the AppDir:
// $APPDIR/Library/bin3
NSString *binPath = [mainBundleLibPath stringByAppendingPathComponent:@"bin3"];
fullCommandPath = [[binPath stringByAppendingString:@":"] stringByAppendingString:fullCommandPath];
// $APPDIR/Library/bin
binPath = [mainBundleLibPath stringByAppendingPathComponent:@"bin"];
fullCommandPath = [[binPath stringByAppendingString:@":"] stringByAppendingString:fullCommandPath];
// $APPDIR/bin
binPath = [mainBundlePath stringByAppendingPathComponent:@"bin"];
fullCommandPath = [[binPath stringByAppendingString:@":"] stringByAppendingString:fullCommandPath];
}
directoriesInPath = [fullCommandPath componentsSeparatedByString:@":"];
setenv("PATH", fullCommandPath.UTF8String, 1); // 1 = override existing value
// Store the maximum number of file descriptors allowed:
getrlimit(RLIMIT_NOFILE, &limitFilesOpen);
// Initialize the array with the name of TeX commands:
TeXcommands = @[@"amstex", @"cslatex", @"csplain", @"eplain", @"etex", @"jadetex", @"latex", @"mex", @"mllatex", @"mltex", @"pdfsclatex", @"pdfcsplain", @"pdfetex", @"pdfjadetex", @"pdflatex", @"pdfmex", @"pdftex", @"pdfxmltex", @"tex", @"texsis", @"utf8mex", @"xmltex", @"texlua", @"texluac", @"dvilualatex", @"dviluatex", @"lualatex", @"luatex", @"luahbtex",
@"amstexA", @"cslatexA", @"csplainA", @"eplainA", @"etexA", @"jadetexA", @"latexA", @"mexA", @"mllatexA", @"mltexA", @"pdfsclatexA", @"pdfcsplainA", @"pdfetexA", @"pdfjadetexA", @"pdflatexA", @"pdfmexA", @"pdftexA", @"pdfxmltexA", @"texA", @"texsisA", @"utf8mexA", @"xmltexA", @"texluaA", @"texluacA", @"dvilualatexA", @"dviluatexA", @"lualatexA", @"luatexA", @"luahbtexA"];
}
NSString * pathJoin(NSString * segmentA, NSString * segmentB);
static char* unquoteArgument(char* argument);
static char* parseArgument(char* argument, char* command) {
// expand all environment variables, convert "~" to $HOME (only if localFile)
// we also pass the shell command for some specific behaviour (don't do this for that command)
NSString* argumentString = [NSString stringWithCString:argument encoding:NSUTF8StringEncoding];
// NSLog(@"parsing argument, argumentString= %s", argumentString.UTF8String);
// If command == "export", first extract the value string here.
NSString* variableName;
if (strcmp(command, "export") == 0) {
char* equalSign=strchr(argument,'=');
if (equalSign && (strlen(equalSign) > 0)) {
char* argumentCString=equalSign+1;
argumentCString = unquoteArgument(argumentCString);
variableName = [argumentString substringToIndex:(equalSign - argument)];
argumentString = [NSString stringWithCString:argumentCString encoding:NSUTF8StringEncoding];
// NSLog(@"parsing argument, variable name= %s argument= %s", variableName.UTF8String, argumentString.UTF8String);
} else {
// No equal sign, or nothing after. export_main will take care of this.
return argument;
}
}
// 1) expand environment variables, + "~" (not wildcards ? and *)
bool cannotExpand = false;
while ([argumentString containsString:@"$"] && !cannotExpand) {
// It has environment variables inside. Work on them one by one.
// position of first "$" sign:
NSRange r1 = [argumentString rangeOfString:@"$"];
// position of first "/" after this $ sign:
NSRange r2 = [argumentString rangeOfString:@"/" options:NULL range:NSMakeRange(r1.location + r1.length, [argumentString length] - r1.location - r1.length)];
// position of first ":" after this $ sign:
NSRange r3 = [argumentString rangeOfString:@":" options:NULL range:NSMakeRange(r1.location + r1.length, [argumentString length] - r1.location - r1.length)];
if ((r2.location == NSNotFound) && (r3.location == NSNotFound)) r2.location = [argumentString length];
else if ((r2.location == NSNotFound) || (r3.location < r2.location)) r2.location = r3.location;
NSRange rSub = NSMakeRange(r1.location + r1.length, r2.location - r1.location - r1.length);
NSString *variable_string = [argumentString substringWithRange:rSub];
const char* variable = ios_getenv([variable_string UTF8String]);
if (variable) {
// Okay, so this one exists.
NSString* replacement_string = [NSString stringWithCString:variable encoding:NSUTF8StringEncoding];
variable_string = [[NSString stringWithCString:"$" encoding:NSUTF8StringEncoding] stringByAppendingString:variable_string];
argumentString = [argumentString stringByReplacingOccurrencesOfString:variable_string withString:replacement_string];
} else cannotExpand = true; // found a variable we can't expand. stop trying for this argument
}
// 2) Tilde conversion: replace "~" with $HOME
// If there are multiple users on iOS, this code will need to be changed.
// We also expand ~bookmarkName to the path for that bookmark.
// 2a) ~ expansion. (old behaviour, kept as is for compatibility)
if([argumentString hasPrefix:@"~"]) {
// So it begins with "~". We can't use stringByExpandingTildeInPath because apps redefine HOME
NSString* replacement_string;
if (miniRoot == nil)
replacement_string = [NSString stringWithCString:(getenv("HOME")) encoding:NSUTF8StringEncoding];
else replacement_string = miniRoot;
if (([argumentString hasPrefix:@"~/"]) || ([argumentString hasPrefix:@"~:"]) || ([argumentString length] == 1)) {
NSString* test_string = @"~";
argumentString = [argumentString stringByReplacingOccurrencesOfString:test_string withString:replacement_string options:NULL range:NSMakeRange(0, 1)];
} else {
// 2b) expand "~something" with the content of userPreference dictionary (to be set by each app)
NSDictionary *tildeExpansionDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ios_bookmarkDictionaryName];
if (tildeExpansionDictionary != nil) {
NSCharacterSet* separators = [NSCharacterSet characterSetWithCharactersInString:@":/"];
NSArray<NSString*>* components = [argumentString componentsSeparatedByCharactersInSet:separators];
NSString* name = [components[0] substringFromIndex:1]; // remove the "~"
NSString* expandedPath = tildeExpansionDictionary[name];
if (expandedPath != nil) {
argumentString = [argumentString stringByReplacingOccurrencesOfString:components[0] withString:expandedPath options:NULL range:NSMakeRange(0, [components[0] length])];
}
}
}
}
// Also convert ":~something" in PATH style variables
// We don't use these yet, but we could.
// We do this expansion only for setenv and export
if ((strcmp(command, "setenv") == 0) || (strcmp(command, "export") == 0)) {
// This is something we need to avoid if the command is "scp" or "sftp"
if ([argumentString containsString:@":~"]) {
NSString* homeDir;
if (miniRoot == nil) homeDir = [NSString stringWithCString:(getenv("HOME")) encoding:NSUTF8StringEncoding];
else homeDir = miniRoot;
// Only 1 possibility: ":~" (same as $HOME)
if (homeDir.length > 0) {
NSString* replacement_string = [@":" stringByAppendingString:homeDir];
if ([argumentString containsString:@":~/"]) {
NSString* test_string = @":~/";
replacement_string = [replacement_string stringByAppendingString:[NSString stringWithCString:"/" encoding:NSUTF8StringEncoding]];
argumentString = [argumentString stringByReplacingOccurrencesOfString:test_string withString:replacement_string];
} else if ([argumentString hasSuffix:@":~"]) {
NSString* test_string = @":~";
argumentString = [argumentString stringByReplacingOccurrencesOfString:test_string withString:replacement_string options:NULL range:NSMakeRange([argumentString length] - 2, 2)];
} else if ([argumentString hasSuffix:@":"]) {
NSString* test_string = @":";
argumentString = [argumentString stringByReplacingOccurrencesOfString:test_string withString:replacement_string options:NULL range:NSMakeRange([argumentString length] - 2, 2)];
}
}
NSDictionary *tildeExpansionDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ios_bookmarkDictionaryName];
if (tildeExpansionDictionary != nil) {
// TODO: add :~bookmarkName/ :~bookmarkName
NSArray<NSString*>* components = [argumentString componentsSeparatedByString:@":~"];
NSString* result = components[0];
for (int i = 1; i < components.count; i++) {
NSString* stringToAdd = components[i];
NSArray<NSString*>* names = [components[i] componentsSeparatedByString:@"/"];
NSString* test_string = names[0];
NSString* replacement_string = tildeExpansionDictionary[names[0]];
if (replacement_string != nil) {
// we found a name to expand
stringToAdd = [stringToAdd stringByReplacingOccurrencesOfString:test_string withString:replacement_string];
result = [[result stringByAppendingString:@":"] stringByAppendingString:stringToAdd];
} else {
result = [[result stringByAppendingString:@":~"] stringByAppendingString:stringToAdd];
}
}
argumentString = result;
}
}
}
if ([argumentString hasPrefix:@"../"] || [argumentString hasPrefix:@"./.."] || [argumentString isEqualToString:@".."]) {
argumentString = pathJoin(@(currentSession->currentDir), argumentString);
}
if (strcmp(command, "export") == 0) {
argumentString = [[variableName stringByAppendingString:@"="] stringByAppendingString:argumentString];
}
const char* newArgument = [argumentString UTF8String];
// NSLog(@"After parsing: %s", newArgument);
if (strcmp(argument, newArgument) == 0) return argument; // nothing changed
// Make sure the argument is reallocated, so it can be free-ed
char* returnValue = realloc(argument, strlen(newArgument) + 1);
strcpy(returnValue, newArgument);
return returnValue;
}
static const char* ios_expandFilename(const char *filename) {
// expand a filename for opening if it begins with "~" or contains an environment variable
if (strlen(filename) == 0) return filename;
NSString* nameString = [NSString stringWithUTF8String:filename];
if([nameString hasPrefix:@"~"]) {
// So it begins with "~". We can't use stringByExpandingTildeInPath because apps redefine HOME
NSString* replacement_string;
if (miniRoot == nil)
replacement_string = [NSString stringWithCString:(getenv("HOME")) encoding:NSUTF8StringEncoding];
else replacement_string = miniRoot;
if (([nameString hasPrefix:@"~/"]) || ([nameString length] == 1)) {
NSString* test_string = @"~";
nameString = [nameString stringByReplacingOccurrencesOfString:test_string withString:replacement_string options:NULL range:NSMakeRange(0, 1)];
} else {
// 2b) expand "~something" with the content of userPreference dictionary (to be set by each app)
NSDictionary *tildeExpansionDictionary = [[NSUserDefaults standardUserDefaults] dictionaryForKey:ios_bookmarkDictionaryName];
if (tildeExpansionDictionary != nil) {
NSCharacterSet* separators = [NSCharacterSet characterSetWithCharactersInString:@":/"];
NSArray<NSString*>* components = [nameString componentsSeparatedByCharactersInSet:separators];
NSString* name = [components[0] substringFromIndex:1]; // remove the "~"
NSString* expandedPath = tildeExpansionDictionary[name];
if (expandedPath != nil) {
nameString = [nameString stringByReplacingOccurrencesOfString:components[0] withString:expandedPath options:NULL range:NSMakeRange(0, [components[0] length])];
}
}
}
}
bool cannotExpand = false;
while ([nameString containsString:@"$"] && !cannotExpand) {
// It has environment variables inside. Work on them one by one.
// position of first "$" sign:
NSRange r1 = [nameString rangeOfString:@"$"];
// position of first "/" after this $ sign:
NSRange r2 = [nameString rangeOfString:@"/" options:NULL range:NSMakeRange(r1.location + r1.length, [nameString length] - r1.location - r1.length)];
if (r2.location == NSNotFound) r2.location = [nameString length];
NSRange rSub = NSMakeRange(r1.location + r1.length, r2.location - r1.location - r1.length);
NSString *variable_string = [nameString substringWithRange:rSub];
const char* variable = ios_getenv([variable_string UTF8String]);
if (variable) {
// Okay, so this one exists.
NSString* replacement_string = [NSString stringWithCString:variable encoding:NSUTF8StringEncoding];
variable_string = [[NSString stringWithCString:"$" encoding:NSUTF8StringEncoding] stringByAppendingString:variable_string];
nameString = [nameString stringByReplacingOccurrencesOfString:variable_string withString:replacement_string];
} else cannotExpand = true; // found a variable we can't expand. stop trying for this fileName
}
return [nameString UTF8String];
}
static void initializeCommandList()
{
// Loads command names and where to find them (digital library, function name) from plist dictionaries:
//
// Syntax for the dictionaris:
// key = command name, followed by an array of 4 components:
// 1st component: name of digital library (will be passed to dlopen(), can be SELF for RTLD_SELF or MAIN for RTLD_MAIN_ONLY)
// 2nd component: name of function to be called
// 3rd component: chain sent to getopt (for arguments in autocomplete)
// 4th component: takes a file/directory as argument
//
// Example:
// <key>rlogin</key>
// <array>
// <string>libnetwork_ios.dylib</string>
// <string>rlogin_main</string>
// <string>468EKLNS:X:acde:fFk:l:n:rs:uxy</string>
// <string>no</string>
// </array>
if (commandList != nil) return;
NSError *error;
NSString* applicationDirectory = [[NSBundle mainBundle] resourcePath];
NSString* commandDictionary = [applicationDirectory stringByAppendingPathComponent:@"commandDictionary.plist"];
NSURL *locationURL = [NSURL fileURLWithPath:commandDictionary isDirectory:NO];
if ([locationURL checkResourceIsReachableAndReturnError:&error] == NO) { NSLog(@"%@", [error localizedDescription]); return; }
NSData* loadedFromFile = [NSData dataWithContentsOfFile:commandDictionary options:0 error:&error];
if (!loadedFromFile) { NSLog(@"%@", [error localizedDescription]); return; }
commandList = [NSPropertyListSerialization propertyListWithData:loadedFromFile options:NSPropertyListImmutable format:NULL error:&error];
if (!commandList) { NSLog(@"%@", [error localizedDescription]); return; }
// replaces the following command, marked as deprecated in the doc:
// commandList = [NSDictionary dictionaryWithContentsOfFile:commandDictionary];
if (sideLoading) {
// more commands, for sideloaders (commands that won't pass AppStore rules, or with licensing issues):
NSString* extraCommandsDictionary = [applicationDirectory stringByAppendingPathComponent:@"extraCommandsDictionary.plist"];
locationURL = [NSURL fileURLWithPath:extraCommandsDictionary isDirectory:NO];
if ([locationURL checkResourceIsReachableAndReturnError:&error] == NO) { NSLog(@"%@", [error localizedDescription]); return; }
NSData* extraLoadedFromFile = [NSData dataWithContentsOfFile:extraCommandsDictionary options:0 error:&error];
if (!extraLoadedFromFile) { NSLog(@"%@", [error localizedDescription]); return; }
NSDictionary* extraCommandList = [NSPropertyListSerialization propertyListWithData:extraLoadedFromFile options:NSPropertyListImmutable format:NULL error:&error];
if (!extraCommandList) { NSLog(@"%@", [error localizedDescription]); return; }
// merge the two dictionaries:
NSMutableDictionary *mutableDict = [commandList mutableCopy];
[mutableDict addEntriesFromDictionary:extraCommandList];
commandList = [mutableDict copy];
}
}
int ios_setMiniRoot(NSString* mRoot) {
BOOL isDir;
NSFileManager *fileManager = [[NSFileManager alloc] init];
if (![fileManager fileExistsAtPath:mRoot isDirectory:&isDir]) {
return 0;
}
if (!isDir) {
return 0;
}
// fileManager has different ways of expressing the same directory.
// We need to actually change to the directory to get its "real name".
NSString* currentDir = [fileManager currentDirectoryPath];
if (![fileManager changeCurrentDirectoryPath:mRoot]) {
return 0;
}
// also don't set the miniRoot if we can't go in there
// get the real name for miniRoot:
miniRoot = [fileManager currentDirectoryPath];
// Back to where we we before:
[fileManager changeCurrentDirectoryPath:currentDir];
if (currentSession != nil) {
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, [miniRoot UTF8String]);
}
return 1; // mission accomplished
}
// Called when
int ios_setMiniRootURL(NSURL* mRoot) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
if (currentSession == NULL) {
currentSession = malloc(sizeof(sessionParameters));
initSessionParameters(currentSession);
}
strcpy(currentSession->localMiniRoot, [mRoot.path UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, [[mRoot path] UTF8String]);
[fileManager changeCurrentDirectoryPath:[mRoot path]];
return 1; // mission accomplished
}
int ios_setAllowedPaths(NSArray<NSString *> *paths) {
allowedPaths = paths;
return 1;
}
BOOL __allowed_cd_to_path(NSString *path) {
// NSLog(@"__allowed_cd_to_path: %@ miniRoot: %@\n", path, miniRoot);
if (miniRoot == nil) {
return YES;
}
if ([path hasPrefix:miniRoot]) {
return YES;
}
if (strlen(currentSession->localMiniRoot) != 0) {
NSString *localMiniRootPath = [NSString stringWithCString:currentSession->localMiniRoot encoding:NSUTF8StringEncoding];
// NSLog(@"__allowed_cd_to_path: localMiniRoot: %s\n", localMiniRootPath);
if (localMiniRootPath && [path hasPrefix:localMiniRootPath]) {
return YES;
}
}
for (NSString *dir in allowedPaths) {
if ([path hasPrefix:dir]) {
return YES;
}
}
// NSLog(@"__allowed_cd_to_path: failure, returning NO\n");
return NO;
}
void __cd_to_dir(NSString *newDir, NSFileManager *fileManager) {
BOOL isDir;
// Check for permission and existence:
if (![fileManager fileExistsAtPath:newDir isDirectory:&isDir]) {
fprintf(thread_stderr, "cd: %s: no such file or directory\n", [newDir UTF8String]);
return;
}
if (!isDir) {
fprintf(thread_stderr, "cd: %s: not a directory\n", [newDir UTF8String]);
return;
}
if (![fileManager isReadableFileAtPath:newDir] ||
![fileManager changeCurrentDirectoryPath:newDir]) {
fprintf(thread_stderr, "cd: %s: permission denied\n", [newDir UTF8String]);
return;
}
// We managed to change the directory.
// Was that allowed?
// Allowed "cd" = below miniRoot *or* below localMiniRoot
NSString* resultDir = [fileManager currentDirectoryPath];
if (__allowed_cd_to_path(resultDir)) {
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, [newDir UTF8String]);
return;
}
fprintf(thread_stderr, "cd: %s: permission denied\n", [newDir UTF8String]);
// If the user tried to go above the miniRoot, set it to miniRoot
if ([miniRoot hasPrefix:resultDir]) {
[fileManager changeCurrentDirectoryPath:miniRoot];
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
} else {
// go back to where we were before:
[fileManager changeCurrentDirectoryPath:[NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding]];
}
}
// For some Unix commands that call fchdir (including vim):
#undef fchdir
int ios_fchdir(const int fd) {
// NSLog(@"Locking for thread %x in ios_fchdir\n", pthread_self());
while (cleanup_counter > 0) { } // Don't chdir while a command is ending.
// We cannot have someone change the current directory while a command is starting or terminating.
// hence the mutex_lock here.
pthread_mutex_lock(&pid_mtx);
int result = fchdir(fd);
if (result < 0) {
// NSLog(@"Unlocking for thread %x in ios_fchdir\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return result;
}
// We managed to change the directory. Update currentSession as well:
// Was that allowed?
// Allowed "cd" = below miniRoot *or* below localMiniRoot
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString* resultDir = [fileManager currentDirectoryPath];
// NSLog(@"Inside fchdir, path: %s for session: %s\n", resultDir.UTF8String, (char*)currentSession->context);
if (__allowed_cd_to_path(resultDir)) {
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, [resultDir UTF8String]);
errno = 0;
// NSLog(@"Unlocking for thread %x in ios_fchdir\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return 0;
}
errno = EACCES; // Permission denied
// If the user tried to go above the miniRoot, set it to miniRoot
if ([miniRoot hasPrefix:resultDir]) {
[fileManager changeCurrentDirectoryPath:miniRoot];
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
} else {
// go back to where we were before:
[fileManager changeCurrentDirectoryPath:[NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding]];
}
// NSLog(@"Unlocking for thread %x in ios_fchdir\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return -1;
}
int ios_fchdir_nolock(const int fd) {
// NSLog(@"fchdir_nolock: %x thread %x\n", fd, pthread_self());
// Same function as fchdir, except it does not lock. To be called when resetting directory after fork().
while (cleanup_counter > 0) { } // Don't chdir while a command is ending.
int result = fchdir(fd);
if (result < 0) {
return result;
}
// We managed to change the directory. Update currentSession as well:
// Was that allowed?
// Allowed "cd" = below miniRoot *or* below localMiniRoot
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString* resultDir = [fileManager currentDirectoryPath];
// NSLog(@"fchdir_nolock, success: %s\n", resultDir.UTF8String);
if (__allowed_cd_to_path(resultDir)) {
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, [resultDir UTF8String]);
errno = 0;
return 0;
}
errno = EACCES; // Permission denied
// If the user tried to go above the miniRoot, set it to miniRoot
if ([miniRoot hasPrefix:resultDir]) {
[fileManager changeCurrentDirectoryPath:miniRoot];
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
} else {
// go back to where we were before:
[fileManager changeCurrentDirectoryPath:[NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding]];
}
// NSLog(@"fchdir_nolock, failure\n");
return -1;
}
int chdir_nolock(const char* path) {
// NSLog(@"chdir_nolock: %s thread %x\n", path, pthread_self());
// Same function as chdir, except it does not lock. To be called from ios_releaseThread*()
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString* newDir = @(path);
BOOL isDir;
// Check for permission and existence:
if (![fileManager fileExistsAtPath:newDir isDirectory:&isDir]) {
errno = ENOENT; // No such file or directory
return -1;
}
if (!isDir) {
errno = ENOTDIR; // Not a directory
return -1;
}
if (![fileManager isReadableFileAtPath:newDir] ||
![fileManager changeCurrentDirectoryPath:newDir]) {
errno = EACCES; // Permission denied
return -1;
}
// We managed to change the directory.
// Was that allowed?
// Allowed "cd" = below miniRoot *or* below localMiniRoot
NSString* resultDir = [fileManager currentDirectoryPath];
if (__allowed_cd_to_path(resultDir)) {
strcpy(currentSession->currentDir, [resultDir UTF8String]);
NSLog(@"allowed directory change, returning\n");
errno = 0;
return 0;
}
errno = EACCES; // Permission denied
// If the user tried to go above the miniRoot, set it to miniRoot
if ([miniRoot hasPrefix:resultDir]) {
[fileManager changeCurrentDirectoryPath:miniRoot];
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
} else {
// go back to where we were before:
[fileManager changeCurrentDirectoryPath:[NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding]];
}
return -1;
}
// For some Unix commands that call chdir:
// Is also called at the end of the execution of each command
int chdir(const char* path) {
while (cleanup_counter > 0) { } // Don't chdir while a command is ending.
// NSLog(@"Locking for thread %x in chdir, cd %s\n", pthread_self(), path);
// We cannot have someone change the current directory while a command is starting or terminating.
// hence the mutex_lock here.
pthread_mutex_lock(&pid_mtx);
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSString* newDir = @(path);
BOOL isDir;
// Check for permission and existence:
if (![fileManager fileExistsAtPath:newDir isDirectory:&isDir]) {
errno = ENOENT; // No such file or directory
// NSLog(@"Unlocking for thread %x in chdir (no such directory)\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return -1;
}
if (!isDir) {
errno = ENOTDIR; // Not a directory
// NSLog(@"Unlocking for thread %x in chdir (not a directory)\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return -1;
}
if (![fileManager isReadableFileAtPath:newDir] ||
![fileManager changeCurrentDirectoryPath:newDir]) {
errno = EACCES; // Permission denied
// NSLog(@"Unlocking for thread %x in chdir (not readable)\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
return -1;
}
// We managed to change the directory.
// Was that allowed?
// Allowed "cd" = below miniRoot *or* below localMiniRoot
NSString* resultDir = [fileManager currentDirectoryPath];
// NSLog(@"After changing directory, result= %s\n", resultDir.UTF8String);
if (__allowed_cd_to_path(resultDir)) {
if (currentSession != NULL) {
strcpy(currentSession->currentDir, [resultDir UTF8String]);
}
// NSLog(@"Unlocking for thread %x in chdir (allowed)\n", pthread_self());
pthread_mutex_unlock(&pid_mtx);
errno = 0;
return 0;
}
errno = EACCES; // Permission denied
if (currentSession == NULL) {
return -1 ;
}
// If the user tried to go above the miniRoot, set it to miniRoot
if ([miniRoot hasPrefix:resultDir]) {
[fileManager changeCurrentDirectoryPath:miniRoot];
strcpy(currentSession->currentDir, [miniRoot UTF8String]);
strcpy(currentSession->previousDirectory, currentSession->currentDir);
} else {
// go back to where we were before:
[fileManager changeCurrentDirectoryPath:[NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding]];
}
// NSLog(@"Unlocking for thread %lx in chdir (not allowed)\n", (unsigned long)pthread_self());
pthread_mutex_unlock(&pid_mtx);
return -1;
}
int too_many_scripts(int argc, char** argv) {
// Call an actual command in order to go through run_function / cleanup_function
// But not something as hardcore as causing a "Command not found" error:
if (currentSession->global_errno == 0) {
return 0; // show the warning only once for PythonNum commands stored:
}
fprintf(thread_stderr, "%s: too many scripts already running\n", argv[0]);
NSLog(@"%s: command not found\n", argv[0]);
return currentSession->global_errno;
}
int command_not_found(int argc, char** argv) {
// Call an actual command in order to go through run_function / cleanup_function
fprintf(thread_stderr, "%s: command not found\n", argv[0]);
NSLog(@"%s: command not found\n", argv[0]);
currentSession->global_errno = 127;
return 127;
// TODO: this should also raise an exception, for python scripts
}
int xcode_select(int argc, char** argv) {
// Replacement for xcode-select so config.guess scripts work
currentSession->global_errno = 1;
errno = 1;
return 1;
}
int sw_vers(int argc, char** argv) {
// Small command to make tlmgr happy
// tlmgr calls "sw_vers -productVersion". We return the latest OSX version, for simplicity
fprintf(thread_stdout, "11.5.2");
fflush(thread_stdout);
return 0;
}
extern void newPreviousDirectory(void);
int cd_main(int argc, char** argv) {
if (currentSession == NULL) {
return 1;
}
NSFileManager *fileManager = [[NSFileManager alloc] init];
if (argc > 1) {
NSString* newDir = @(argv[1]);
if (strcmp(argv[1], "-") == 0) {
// "cd -" option to pop back to previous directory
newDir = @(currentSession->previousDirectory);
}
newDir = pathJoin(@(currentSession->currentDir), newDir);
// Store directory usage for autocomplete:
// It should not be a dictionary. NSArray? NSMutableArray?
// Need to store directoryname + number of times = sounds a lot like a Swift dictionary.
// But not Objective-C? Weird.
// Do it in Swift (new dictionary each time), then store it, then move to Objective-C?
void (*function)(NSString*) = NULL;
function = dlsym(RTLD_MAIN_ONLY, "storeDirectoryUsed");
if (function != NULL) {
NSString *key = @(ios_getBookmarkedVersion(newDir.UTF8String));
function(key);
}
__cd_to_dir(newDir, fileManager);
} else { // [cd] Help, I'm lost, bring me back home
if (miniRoot != nil) {
[fileManager changeCurrentDirectoryPath:miniRoot];
} else {
[fileManager changeCurrentDirectoryPath:[NSSearchPathForDirectoriesInDomains(NSDocumentDirectory, NSUserDomainMask, YES) lastObject]];
}
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, fileManager.currentDirectoryPath.UTF8String);
}
newPreviousDirectory(); // If a command is running, this changes the directory it goes back to.
return 0;
}
NSString* getoptString(NSString* commandName) {
if (commandList == nil) initializeCommandList();
NSArray* commandStructure = [commandList objectForKey: commandName];
if (commandStructure != nil) return commandStructure[2];
else return @"";
}
NSString* operatesOn(NSString* commandName) {
if (commandList == nil) initializeCommandList();
NSArray* commandStructure = [commandList objectForKey: commandName];
if (commandStructure != nil) return commandStructure[3];
else return @"";
}
int ios_executable(const char* inputCmd) {
// returns 1 if this is one of the commands we define in ios_system, 0 otherwise
if (commandList == nil) initializeCommandList();
// Take basename in case someone put a path before:
NSArray* valuesFromDict = [commandList objectForKey: [NSString stringWithCString:basename(inputCmd) encoding:NSUTF8StringEncoding]];
// we could dlopen() here, but that would defeat the purpose
if (valuesFromDict == nil) return 0;
else return 1;
}
// Where to direct input/output of the next thread:
static __thread FILE* child_stdin = NULL;
static __thread FILE* child_stdout = NULL;
static __thread FILE* child_stderr = NULL;
FILE* ios_popen(const char* inputCmd, const char* type) {
// NSLog(@"ios_popen: %s mode %s", inputCmd, type);
// Save existing streams:
int fd[2] = {0};
const char* command = inputCmd;
// skip past all spaces
while ((command[0] == ' ') && strlen(command) > 0) command++;
if (pipe(fd) < 0) { return NULL; } // Nothing we can do if pipe fails
// F_SETNOSIGPIPE: don't cause a signal 13 if the pipe is already closed
fcntl(fd[0], F_SETNOSIGPIPE);
fcntl(fd[1], F_SETNOSIGPIPE);
// NOTES: fd[0] is set up for reading, fd[1] is set up for writing
// fpout = fdopen(fd[1], "w");
// fpin = fdopen(fd[0], "r");
if (type[0] == 'w') {
// open pipe for reading
child_stdin = fdopen(fd[0], "r");
// launch command: if the command fails, return NULL.
int returnValue = ios_system(command);
if (returnValue == 0)
return fdopen(fd[1], "w");
} else if (type[0] == 'r') {
// open pipe for writing
// set up streams for thread
child_stdout = fdopen(fd[1], "w");
// launch command: if the command fails, return NULL.
int returnValue = ios_system(command);
if (returnValue == 0)
return fdopen(fd[0], "r");
}
// pipe creation failed, command starting failed:
return NULL;
}
// small function, behaves like strstr but skips quotes (Yury Korolev)
char *strstrquoted(char* str1, char* str2) {
if (str1 == NULL || str2 == NULL) {
return NULL;
}
size_t len1 = strlen(str1);
size_t len2 = strlen(str2);
if (len1 < len2) {
return NULL;
}
if (strcmp(str1, str2) == 0) {
return str1;
}
char quotechar = 0;
int esclen = 0;
int matchlen = 0;
for (int i = 0; i < len1; i++) {
char ch = str1[i];
if (quotechar) {
if (ch == '\\') {
esclen++;
continue;
}
if (ch == quotechar) {
if (esclen % 2 == 1) {
esclen = 0;
continue;
}
quotechar = 0;
esclen = 0;
continue;
}
esclen = 0;
continue;
}
if (ch == '"' || ch == '\'') {
if (esclen % 2 == 0) {
quotechar = ch;
}
matchlen = 0;
esclen = 0;
continue;
}
if (ch == '\\') {
esclen++;
}
if (str2[matchlen] == ch) {
matchlen++;
if (matchlen == len2) {
return str1 + i - matchlen + 1;
}
continue;
}
matchlen = 0;
}
return NULL;
}
static char* concatenateArgv(char* const argv[]) {
int argc = 0;
int cmdLength = 0;
// concatenate all arguments into a big command.
// We need this because some programs call execv() with a single string: "ssh hg@bitbucket.org 'hg -R ... --stdio'"
// So we rely on ios_system to break them into chunks.
while(argv[argc] != NULL) { cmdLength += strlen(argv[argc]) + 1; argc++;}
if (argc == 0) return NULL; // safeguard check
char* cmd = malloc((cmdLength + 3 * argc + 1) * sizeof(char)); // space for quotes
strcpy(cmd, argv[0]);
argc = 1;
char recordSeparator = 0x1e;
while (argv[argc] != NULL) {
if (strstrquoted(argv[argc], " ")) {
// argument contains spaces. Enclose it into quotes:
if (strstr(argv[argc], "\"") == NULL) { // We're looking for quotes, so strstr, not strstrquoted
// argument does not contain ". Enclose with "
strcat(cmd, " \"");
strcat(cmd, argv[argc]);
strcat(cmd, "\"");
argc++;
continue;
} else if (strstr(argv[argc], "'") == NULL) { // We're looking for quotes, so strstr, not strstrquoted
// argument does not contain '. Enclose with '
strcat(cmd, " '");
strcat(cmd, argv[argc]);
strcat(cmd, "'");
argc++;
continue;
} else if (strchr(argv[argc], recordSeparator) == NULL) {
// Argument contains spaces and double and single quotes. We use recordSeparator to mark begin and end:
strcat(cmd, " ");
strncat(cmd, &recordSeparator, 1);
strcat(cmd, argv[argc]);
strncat(cmd, &recordSeparator, 1);
argc++;
continue;
}
NSLog(@"Argument contains spaces, double quotes, single quotes and recordSeparator");
}
strcat(cmd, " ");
strcat(cmd, argv[argc]);
argc++;
}
return cmd;
}
int pbpaste(int argc, char** argv) {
if (argc == 1) {
// We can paste strings and URLs.
if ([UIPasteboard generalPasteboard].hasStrings) {
fprintf(thread_stdout, "%s", [[UIPasteboard generalPasteboard].string UTF8String]);
if (![[UIPasteboard generalPasteboard].string hasSuffix:@"\n"]) fprintf(thread_stdout, "\n");
return 0;
}
if ([UIPasteboard generalPasteboard].hasURLs) {
fprintf(thread_stdout, "%s\n", [[[UIPasteboard generalPasteboard].URL absoluteString] UTF8String]);
return 0;
}
} else {
fprintf(thread_stderr, "Usage: pbpaste\nPastes the content of the copy buffer (strings or urls).");
}
return 1;
}
int pbcopy(int argc, char** argv) {
if (argc == 1) {
// no arguments, listen to stdin
const int bufsize = 1024;
char buffer[bufsize];
NSMutableData* data = [[NSMutableData alloc] init];
ssize_t count = 0;
while ((count = read(fileno(thread_stdin), buffer, bufsize-1))) {
[data appendBytes:buffer length:count];
}
NSString* result = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
if (!result) {
return 1;
}
[UIPasteboard generalPasteboard].string = result;
} else {
if ((argv[1][0] == '-') && ((strcmp(argv[1], "-h") == 0) || (strcmp(argv[1], "--help") == 0))) {
fprintf(thread_stderr, "Usage: pbcopy arguments\ncommand > pbcopy\nCopies either its arguments or input to the copy buffer.");
return 0;
}
// threre are arguments, concatenate and paste:
char* cmd = concatenateArgv(argv + 1);
[UIPasteboard generalPasteboard].string = @(cmd);
free(cmd);
}
return 0;
}
int true_main(int argc, char** argv) {
return 0;
}
int type_main(int argc, char** argv) {
// Minimalist implementation of type to keep make happy
if (argc < 2) { return 1; }
fprintf(thread_stdout, "%s is %s\n", argv[1], argv[1]);
return 0;
}
int alias_main(int argc, char** argv) {
// Syntax: alias command="new command" or alias command "new command" (both must work)
// alias -h or alias --help: print help
// alias (no arguments): print list of aliases
// alias (single argument): print corresponding alias
NSString* usage = @"usage: alias command new command\n\talias command=new command\n\t!^ = first argument\n\t!* = all arguments";
if (aliasDictionary == nil) {
aliasDictionary = [NSMutableDictionary new];
}
if (argc <= 1) {
// no arguments: print list of aliases
for (NSString* command in aliasDictionary) {
NSArray<NSString*>* aliasArray = aliasDictionary[command];
fprintf(thread_stdout, "%s\t", command.UTF8String);
fprintf(thread_stdout, "%s", aliasArray[0].UTF8String);
if ([aliasArray[2] isEqualToString: @"afterFirst"]) {
fprintf(thread_stdout, " !^ %s", aliasArray[1].UTF8String);
} else if ([aliasArray[2] isEqualToString: @"afterLast"]) {
fprintf(thread_stdout, " !* %s", aliasArray[1].UTF8String);
}
fprintf(thread_stdout, "\n");
}
return 0;
}
if (argv[1][0] == '-') {
if ((strncmp(argv[1], "-h", 2) != 0) && (strncmp(argv[1], "--help", 6) != 0)) {
fprintf(thread_stderr, "alias: unrecognized option %s\n", argv[1]);
}
fprintf(thread_stderr, "%s\n", usage.UTF8String);
return 0;
}
char* equalSign = strchr(argv[1], '=');
NSString* command = nil;
if ((equalSign == NULL) && (argc == 2)) {
// single command, show alias:
command = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
NSLog(@"Extracting alias for command %s = %@.", argv[1], command);
NSArray<NSString*>* aliasArray = aliasDictionary[command];
if (aliasArray == nil) { return 0; }
fprintf(thread_stdout, "%s", aliasArray[0].UTF8String);
if ([aliasArray[2] isEqualToString: @"afterFirst"]) {
fprintf(thread_stdout, " !^ %s", aliasArray[1].UTF8String);
} else if ([aliasArray[2] isEqualToString: @"afterLast"]) {
fprintf(thread_stdout, " !* %s ", aliasArray[1].UTF8String);
}
fprintf(thread_stdout, "\n");
return 0;
}
NSMutableArray<NSString *> *commandArray = [[NSMutableArray alloc] init];
if (equalSign != NULL) {
// There is an equal sign in the second argument. Split into alias / command:
equalSign[0] = 0;
char* alias = equalSign + 1;
command = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
commandArray[0] = [NSString stringWithCString:alias encoding:NSUTF8StringEncoding];;
} else {
command = [NSString stringWithCString:argv[1] encoding:NSUTF8StringEncoding];
}
if (argc >= 3) {
// We keep the benefit of the parsing that was already done:
for (int i = 0; i < argc - 2; i++) {
[commandArray addObject: [NSString stringWithCString:argv[i + 2] encoding:NSUTF8StringEncoding]];
}
}
if ((command == nil) || (commandArray == nil) || (commandArray.count == 0)) {
// Something went wrong
return 1;
}
if ((equalSign != NULL) || (commandArray.count == 1)) {
// If there was an equal sign, so there might be some extra quotes:
// Observed decomposition with equal sign: "ll=\"ls" + "-l\""
// If there is a single command, we separate it as well: alias ls "ls -l"
if ([commandArray[0] hasPrefix:@"\""] && [[commandArray lastObject] hasSuffix:@"\""]) {
commandArray[0] = [commandArray[0] substringFromIndex:1];
commandArray[commandArray.count - 1] = [[commandArray lastObject] substringToIndex:[[commandArray lastObject] length] -1];
} else if ([commandArray[0] hasPrefix:@"'"] && [[commandArray lastObject] hasSuffix:@"'"]) {
commandArray[0] = [commandArray[0] substringFromIndex:1];
commandArray[commandArray.count - 1] = [[commandArray lastObject] substringToIndex:[[commandArray lastObject] length] -1];
}
}
if (commandArray.count == 1) {
char* aliasCommandCString = strdup(commandArray[0].UTF8String);
char* pointerToFree = aliasCommandCString;
char* nextSpace = strstrquoted(aliasCommandCString, " ");
int i = 0;
while (nextSpace != NULL) {
*nextSpace = 0;
commandArray[i] = [NSString stringWithCString:aliasCommandCString encoding:NSUTF8StringEncoding];
// NSLog(@"Adding %s to array.", aliasCommandCString);
aliasCommandCString = nextSpace + 1;
if (*aliasCommandCString == 0) {
break;
}
nextSpace = strstrquoted(aliasCommandCString, " ");
i += 1;
}
if (*aliasCommandCString != 0) {
// NSLog(@"Adding %s to array.", aliasCommandCString);
commandArray[i] = [NSString stringWithCString:aliasCommandCString encoding:NSUTF8StringEncoding];
}
free(pointerToFree);
}
NSString* before = @"";
NSString* after = @"";
NSString* position = @"";
if (([commandArray containsObject:@"!^"]) && ([commandArray containsObject:@"!*"])) {
fprintf(thread_stderr, "alias: can't pecify both !^ and !*, sorry.\n", argv[1]);
return 1;
} else if ([commandArray containsObject:@"!^"]) {
position = @"afterFirst";
bool foundMarker = false;
for (NSString* component in commandArray) {
if ([component isEqualToString: @"!^"]) { foundMarker = true; continue; }
if (!foundMarker) {
before = [before stringByAppendingString: component];
before = [before stringByAppendingString: @" "];
} else {
after = [after stringByAppendingString: component];
after = [after stringByAppendingString: @" "];
}
}
} else if ([commandArray containsObject:@"!*"]) {
position = @"afterLast";
bool foundMarker = false;
for (NSString* component in commandArray) {
if ([component isEqualToString:@"!*"]) {
foundMarker = true;
continue;
}
if (!foundMarker) {
before = [before stringByAppendingString: component];
before = [before stringByAppendingString: @" "];
} else {
after = [after stringByAppendingString: component];
after = [after stringByAppendingString: @" "];
}
}
} else {
for (NSString* component in commandArray) {
before = [before stringByAppendingString: component];
before = [before stringByAppendingString: @" "];
}
}
before = [before stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]];
after = [after stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]];
NSArray<NSString *> *result = @[before, after, position];
aliasDictionary[command] = result;
return 0;
}
NSString* aliasedCommand(NSString* command) {
if ([command hasPrefix:@"\\"]) {
// \command = cancel aliasing
return [command substringFromIndex:1];
}
if (aliasDictionary == nil) {
return command;
}
NSArray<NSString*>* aliasArray = aliasDictionary[command];
if (aliasArray == nil) {
return command;
}
NSString* result = aliasArray[0];
if ([aliasArray[2] isEqualToString: @"afterFirst"]) {
result = [[result stringByAppendingString:@" !^ "] stringByAppendingString: aliasArray[1]];
} else if ([aliasArray[2] isEqualToString: @"afterLast"]) {
result = [[result stringByAppendingString:@" !* "] stringByAppendingString: aliasArray[1]];
}
return result;
}
int unalias_main(int argc, char** argv) {
NSString* usage = @"usage: unalias [command|-a]";
if (aliasDictionary == nil) {
aliasDictionary = [NSMutableDictionary new];
}
if ((argc == 1) || ((argv[1][0] == '-') && (strncmp(argv[1], "-a", 2) != 0))) {
fprintf(thread_stderr, "%s\n", usage.UTF8String);
return 0;
}
if (strncmp(argv[1], "-a", 2) == 0) {
[aliasDictionary removeAllObjects];
return 0;
}
for (int i = 1; i < argc; i++) {
NSString* command = [NSString stringWithCString:argv[i] encoding:NSUTF8StringEncoding];
[aliasDictionary removeObjectForKey: command];
}
return 0;
}
// Auxiliary function for sh_main. Given a string of characters (command1 && command2),
// split it into the sub commands and execute each of them in sequence:
static int splitCommandAndExecute(char* command) {
// Remember to use fork / waitpid to wait for the commands to finish
if (command == NULL) return 0;
char* maxPointer = command + strlen(command);
int returnValue = 0;
while (command[0] != 0) {
// NSLog(@"stdout %x \n", fileno(thread_stdout));
// NSLog(@"stderr %x \n", fileno(thread_stderr));
char* nextAnd = strstrquoted(command, "&&");
char* nextOr = strstrquoted(command, "||");
if ((nextAnd == NULL) && (nextOr == NULL)) {
// Only one command left
pid_t pid = ios_fork();
returnValue = ios_system(command);
NSLog(@"Started command, stored last_thread= %x pid: %d", currentSession->lastThreadId, pid);
ios_waitpid(pid);
break;
}
long nextCommandPosition = 0;
bool andNextCommand = false;
if (nextAnd != NULL) {
nextCommandPosition = nextAnd - command;
andNextCommand = true;
}
if (nextOr != NULL) {
if (nextOr - command < nextCommandPosition) {
nextCommandPosition = nextOr - command;
andNextCommand = false;
}
}
command[nextCommandPosition] = NULL; // terminate string
pid_t pid = ios_fork();
returnValue = ios_system(command);
// NSLog(@"Started command (2), stored last_thread= %x", currentSession->lastThreadId);
ios_waitpid(pid);
if (andNextCommand && (returnValue != 0)) {
// && + the command returned error, we return:
break;
} else if (!andNextCommand && (returnValue == 0)) {
// || + the command worked, we return:
break;
}
command += (nextCommandPosition + 2); // char after "&&" or "||"
while ((command[0] == ' ') && (command < maxPointer)) command++; // skip spaces
if (command > maxPointer) return 0; // happens if the command ends with && or ||
}
return returnValue;
}
sessionParameters* parentSession = NULL;
// TODO: we *do* have multiple sh sessions running. We will need some way to make this safe.
//
NSString* parentDir;
int sh_main(int argc, char** argv) {
// NOT an actual shell.
// for commands that call other commands as "sh -c command" or "sh -c command1 && command2"
// NSLog(@"sh_main, stdout %d \n", fileno(thread_stdout));
// NSLog(@"sh_main, stderr %d \n", fileno(thread_stderr));
if ((argc < 2) || (strncmp(argv[1], "-h", 2) == 0)) {
fprintf(thread_stderr, "Not an actual shell. sh is provided for compatibility with commands that call other commands.\n");
fprintf(thread_stderr, "Usage: sh [-flags] [VAR=value] command: executes command (all flags are ignored, environment variable VAR is set to value).\n");
fprintf(thread_stderr, " sh [-flags] command1 && command2 [&& command3 && ...]: executes the commands, in order, until one returns error.\n");
fprintf(thread_stderr, " sh [-flags] command1 || command2 [|| command3 || ...]: executes the commands, in order, until one returns OK.\n");
argv[0][0] = 'h'; // prevent termination in cleanup_function
return 0;
}
char** command = argv + 1; // skip past "sh"
while ((command[0] != NULL) && (command[0][0] == '-')) { command++; } // skip past all flags
// Anything after "sh" that contains an equal sign must be an environment variable. Pass it to ios_setenv.
while (command[0] != NULL) {
char* position = strstrquoted(command[0],"=");
if (position == NULL) { break; }
char* firstSpace = strstrquoted(command[0]," ");
if ((firstSpace!=NULL) && (firstSpace < position)) { break; }
firstSpace = strstrquoted(position," ");
if (firstSpace != NULL) { *firstSpace = 0; }
*position = 0;
ios_setenv(command[0], position+1, 1);
if (firstSpace != NULL) {
command[0] = firstSpace + 1;
}
else { command++; }
}
if (command[0] == NULL) {
argv[0][0] = 'h'; // prevent termination in cleanup_function
return 0;
}
// Did we redirect output? (most of the time, yes)
if ((fileno(currentSession->stdout) == fileno(thread_stdout)) ||
(fileno(currentSession->stderr) == fileno(thread_stderr)) ||
(fileno(currentSession->stdout) == fileno(thread_stderr))) {
NSLog(@"prevent termination in cleanup_function");
argv[0][0] = 'h'; // prevent termination in cleanup_function
}
// If there is a single command (no && or ||), no need to create a new session.
int i = 0;
while ((command[i] != NULL) && (strstrquoted(command[i], "&&") == NULL) && (strstrquoted(command[i], "||") == NULL)) i++;
if (command[i] == NULL) {
// Just one command:
char* newCommand = concatenateArgv(command);
// Only one command left
argv[0][0] = 'h'; // prevent termination?
pid_t pid = ios_fork();
int returnValue = ios_system(newCommand);
ios_waitpid(pid);
free(newCommand);
return returnValue;
}
// If we reach this point, we have multiple commands to execute.
// Store current sesssion, create a new session specific for this, execute commands
id sessionKey = @((NSUInteger)sh_session);
if (sessionList != nil) {
sessionParameters* runningShellSession = (sessionParameters*)[[sessionList objectForKey: sessionKey] pointerValue];
if (runningShellSession != NULL) {
if ((runningShellSession->lastThreadId != 0) && (runningShellSession->lastThreadId != pthread_self())) {
fprintf(thread_stderr, "Sorry, you cannot run sh while another sh command is running\n");
NSLog(@"There is another sh session running: last_thread= %x", runningShellSession->lastThreadId);
argv[0][0] = 'h'; // prevent termination in cleanup_function
return 1;
} else {
NSLog(@"There is another sh session running: last_thread= %x us= %x. Continuing.", runningShellSession->lastThreadId, pthread_self());
}
}
}
NSFileManager *fileManager = [[NSFileManager alloc] init];
NSLog(@"parentSession = %x currentSession = %x currentDir = %s\n", parentSession, currentSession, [fileManager currentDirectoryPath].UTF8String);
if (currentSession->context == sh_session) {
NSLog(@"We cannot have a sh command starting a sh command");
return 1; // We cannot have a sh command starting a sh command.
}
if (parentSession == NULL) {
parentSession = currentSession;
parentDir = [fileManager currentDirectoryPath];
}
ios_switchSession(sh_session); // create a new session
// NSLog(@"after switchSession, currentDir = %s\n", [fileManager currentDirectoryPath].UTF8String);
currentSession->isMainThread = false;
currentSession->context = sh_session;
currentSession->stdin = thread_stdin;
currentSession->stdout = thread_stdout;
currentSession->stderr = thread_stderr;
currentSession->current_command_root_thread = NULL;
currentSession->lastThreadId = NULL;
currentSession->mainThreadId = parentSession->mainThreadId;
// Need to loop twice: over each argument, and inside each argument.
// &&: keep computing until one command is in error
// ||: keep computing until one command is not in error
// Remember to use fork / waitpid to wait for the commands to finish
int returnValue = 0;
while (command[0] != NULL) {
int i = 0;
while ((command[i] != NULL) && (strcmp(command[i], "&&") != 0) && (strcmp(command[i], "||") != 0)) i++;
if (command[i] == NULL) {
char* lastCommand = concatenateArgv(command);
returnValue = splitCommandAndExecute(lastCommand);
free(lastCommand);
break;
}
bool andNextCommand = (strcmp(command[i], "&&") == 0); // should we continue?
command[i] = NULL;
char* newCommand = concatenateArgv(command);
returnValue = splitCommandAndExecute(newCommand);
free(newCommand);
if (andNextCommand && (returnValue != 0)) {
// && + the command returned error, we return:
break;
} else if (!andNextCommand && (returnValue == 0)) {
// || + the command worked, we return:
break;
}
command += (i+1);
}
// NSLog(@"Closing shell session; last_thread= %x root= %x", currentSession->lastThreadId, currentSession->current_command_root_thread);
if (![parentDir isEqualToString:[fileManager currentDirectoryPath]]) {
// NSLog(@"Reset current Dir to= %s instead of %s", parentDir.UTF8String, [fileManager currentDirectoryPath].UTF8String);
[fileManager changeCurrentDirectoryPath:parentDir];
}
ios_closeSession(sh_session);
currentSession = parentSession;
parentSession = NULL;
return returnValue;
}
int ios_execv(const char *path, char* const argv[]) {
// path and argv[0] are the same (not in theory, but in practice, since Python wrote the command)
// start "child" with the child streams:
char* cmd = concatenateArgv(argv);
int returnValue = ios_system(cmd);
free(cmd);
return returnValue;
}
int ios_execve(const char *path, char* const argv[], char* envp[]) {
// save the environment (done) and current dir (TODO)
storeEnvironment(envp);
int returnValue = ios_execv(path, argv);
// The environment will be restored to previous value when the thread Id is released.
return returnValue;
}
extern char** environmentVariables(pid_t pid);
NSArray* environmentAsArray() {
char** env_pid = environmentVariables(ios_currentPid());
NSMutableArray<NSString*> *dictionary = [[NSMutableArray alloc]init];
int i = 0;
while (env_pid[i] != NULL) {
NSString* variable = [NSString stringWithCString:env_pid[i] encoding:NSUTF8StringEncoding];
[dictionary addObject:variable];
i++;
}
return [dictionary copy];
}
pthread_t ios_getLastThreadId() {
if (!currentSession) return nil;
return (currentSession->lastThreadId);
}
/*
* Public domain dup2() lookalike
* by Curtis Jackson @ AT&T Technologies, Burlington, NC
* electronic address: burl!rcj
* Edited for iOS by N. Holzschuch.
* The idea is that dup2(fd, [012]) is usually called between fork and exec.
*
* dup2 performs the following functions:
*
* Check to make sure that fd1 is a valid open file descriptor.
* Check to see if fd2 is already open; if so, close it.
* Duplicate fd1 onto fd2; checking to make sure fd2 is a valid fd.
* Return fd2 if all went well; return BADEXIT otherwise.
*/
int ios_dup2(int fd1, int fd2)
{
NSLog(@"ios_dup2: %d %d", fd1, fd2);
// iOS specifics: trying to access stdin/stdout/stderr?
if (fd1 < 3) {
// specific cases like dup2(STDOUT_FILENO, STDERR_FILENO)
FILE* stream1 = NULL;
switch (fd1) {
case 0: stream1 = child_stdin; break;
case 1: stream1 = child_stdout; break;
case 2: stream1 = child_stderr; break;
}
switch (fd2) {
case 0:
if (stream1 != NULL) {
child_stdin = stream1; return fd2;
} else break;
case 1:
if (stream1 != NULL) {
child_stdout = stream1; return fd2;
} else break;
case 2:
if (stream1 != NULL) {
child_stderr = stream1; return fd2;
} else break;
}
}
// We can have fileno(thread_stdin) == 1. Most likely fd2 == 1 means stdout in that case.
if (fd2 == 0) {
child_stdin = fdopen(fd1, "rb");
} else if (fd2 == 1) {
child_stdout = fdopen(fd1, "wb");
} else if (fd2 == 2) {
if ((child_stdout != NULL) && (fileno(child_stdout) == fd1)) child_stderr = child_stdout;
if ((child_stdout != NULL) && (fileno(thread_stdout) == fd1)) child_stderr = child_stdout;
else if (fd1 == 1) {
child_stderr = thread_stdout;
} else child_stderr = fdopen(fd1, "wb");
} else if (thread_stdin != NULL && fd2 == fileno(thread_stdin)) {
child_stdin = fdopen(fd1, "rb");
} else if (thread_stdout != NULL && fd2 == fileno(thread_stdout)) {
child_stdout = fdopen(fd1, "wb");
} else if (thread_stderr != NULL && fd2 == fileno(thread_stderr)) {
if ((child_stdout != NULL) && (fileno(child_stdout) == fd1)) child_stderr = child_stdout;
else child_stderr = fdopen(fd1, "wb");
}
else if (fd1 != fd2) {
if (fcntl(fd1, F_GETFL) < 0)
return -1;
if (fcntl(fd2, F_GETFL) >= 0)
close(fd2);
if (fcntl(fd1, F_DUPFD, fd2) < 0)
return -1;
}
return fd2;
}
/* Normally, a command that wants to send output to different streams calls dup2, then fork and exec.
ios_system() is ready for that. Sometimes, command (eg in dash) just call dup2 and expect the
output to be redirected. This function sends the result of dup2 to stdin, stdout, stderr and stores
the previous streams so they can be restored later.
*/
void ios_activateChildStreams(FILE** old_stdin, FILE** old_stdout, FILE ** old_stderr) {
if (child_stdin != NULL) {
*old_stdin = thread_stdin;
thread_stdin = child_stdin;
child_stdin = NULL;
}
if (child_stdout != NULL) {
*old_stdout = thread_stdout;
thread_stdout = child_stdout;
child_stdout = NULL;
}
if (child_stderr != NULL) {
*old_stderr = thread_stderr;
thread_stderr = child_stderr;
child_stderr = NULL;
}
}
int ios_kill()
{
if (currentSession == NULL) return ESRCH;
if (currentSession->current_command_root_thread > 0) {
struct sigaction query_action;
if ((sigaction (SIGINT, NULL, &query_action) >= 0) &&
(query_action.sa_handler != SIG_DFL) &&
(query_action.sa_handler != SIG_IGN)) {
/* A programmer-defined signal handler is in effect. */
// This might be problematic with multiple commands running at the same time that all define SIGINT
// ...such as ls.
// !! this is called from the main thread. So make sure the signal handler does *not* call phtread_exit();
query_action.sa_handler(SIGINT);
// kill(getpid(), SIGINT); // infinite loop?
} else {
// Send pthread_cancel with the given signal to the current main thread, if there is one.
if (currentSession->current_command_root_thread != NULL)
return pthread_cancel(currentSession->current_command_root_thread);
}
}
// No process running
return ESRCH;
}
extern pthread_t ios_getThreadId(pid_t pid);
int ios_killpid(pid_t pid, int sig) {
if (ios_getThreadId(pid) > 0) {
return pthread_kill(ios_getThreadId(pid), sig);
}
return 0;
}
void ios_switchSession(const void* sessionId) {
char* sessionName = (char*) sessionId;
if ((currentSession != nil) && (parentSession != nil)) {
if ((currentSession->context == sh_session) && (parentSession->context == sessionName)) {
// If we are running a sh_session inside the requested sessionId, there is no need to change:
return;
}
}
if ((currentSession != nil) && (currentSession->context != NULL) && (strcmp(currentSession->context, sessionName) == 0)) {
// Already inside this session: do nothing
return;
}
NSFileManager *fileManager = [[NSFileManager alloc] init];
id sessionKey = @((NSUInteger)sessionId);
if (sessionList == nil) {
sessionList = [NSMutableDictionary new];
if (currentSession != NULL) [sessionList setObject: [NSValue valueWithPointer:currentSession] forKey: sessionKey];
}
currentSession = (sessionParameters*)[[sessionList objectForKey: sessionKey] pointerValue];
if (currentSession == NULL) {
sessionParameters* newSession = malloc(sizeof(sessionParameters));
initSessionParameters(newSession);
[sessionList setObject: [NSValue valueWithPointer:newSession] forKey: sessionKey];
currentSession = newSession;
} else {
NSString* currentSessionDir = [NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding];
if (![currentSessionDir isEqualToString:[fileManager currentDirectoryPath]]) {
[fileManager changeCurrentDirectoryPath:currentSessionDir];
}
// Da fuck???? Yeah, that would hurt. Why is it there?
currentSession->stdin = stdin;
currentSession->stdout = stdout;
currentSession->stderr = stderr;
}
}
void ios_setDirectoryURL(NSURL* workingDirectoryURL) {
NSFileManager *fileManager = [[NSFileManager alloc] init];
[fileManager changeCurrentDirectoryPath:[workingDirectoryURL path]];
if (currentSession != NULL) {
NSString* currentSessionDir = [NSString stringWithCString:currentSession->currentDir encoding:NSUTF8StringEncoding];
if ([currentSessionDir isEqualToString:[fileManager currentDirectoryPath]]) return;
strcpy(currentSession->previousDirectory, currentSession->currentDir);
strcpy(currentSession->currentDir, [[workingDirectoryURL path] UTF8String]);
}
}
void ios_closeSession(const void* sessionId) {
// delete information associated with current session:
if (sessionList == nil) return;
id sessionKey = @((NSUInteger)sessionId);
[sessionList removeObjectForKey: sessionKey];
currentSession = NULL;
}
int ios_isatty(int fd) {
if (currentSession == NULL) return 0;
// 2 possibilities: 0, 1, 2 (classical) or fileno(thread_stdout)
if (thread_stdin != NULL) {
if ((fd == STDIN_FILENO) || (fd == fileno(currentSession->stdin)) || (fd == fileno(thread_stdin)))
return (fileno(thread_stdin) == fileno(currentSession->stdin));
}
if (thread_stdout != NULL) {
if ((fd == STDOUT_FILENO) || (fd == fileno(currentSession->stdout)) || (fd == fileno(thread_stdout))) {
return (fileno(thread_stdout) == fileno(currentSession->stdout));
}
}
if (thread_stderr != NULL) {
if ((fd == STDERR_FILENO) || (fd == fileno(currentSession->stderr)) || (fd == fileno(thread_stderr)))
return (fileno(thread_stderr) == fileno(currentSession->stderr));
}
return 0;
}
void ios_setStreams(FILE* _stdin, FILE* _stdout, FILE* _stderr) {
if (currentSession == NULL) return;
currentSession->stdin = _stdin;
currentSession->stdout = _stdout;
currentSession->stderr = _stderr;
}
void ios_settty(FILE* _tty) {
if (currentSession == NULL) return;
currentSession->tty = _tty;
}
int ios_gettty() {
if (currentSession == NULL) return -1;
if (currentSession->tty == NULL) return -1;
return fileno(currentSession->tty);
}
// Allows commands that are not usually tty-based to get the tty (for password input in ssh/scp/sftp):
int ios_opentty(void) {
if (currentSession == nil) { return -1; }
if (currentSession->tty == NULL) return -1;
currentSession->activePager = true;
return fileno(currentSession->tty);
}
void ios_closetty(void) {
if (currentSession == nil) { return; }
currentSession->activePager = false;
}
int ios_activePager() {
// All commands that read from tty instead of stdin:
if (currentSession == nil) { return 0; }
char* currentSessionCommandName;
if (currentSession->numCommand <= 0)
currentSessionCommandName = currentSession->commandName[0];
else
currentSessionCommandName = currentSession->commandName[currentSession->numCommand - 1];
if ((strcmp(currentSessionCommandName, "less") == 0) ||
(strcmp(currentSessionCommandName, "more") == 0)) {
return 1;
}
if (currentSession->activePager) { return 1; }
return 0;
}
void ios_stopInteractive(void) {
// Some commands, like sftp, start as "interactive" (they handle all input), then become non-interactive (they let the shell handle input)
// This could be merged with opentty / closetty above, but stopInteractive involves one more trip to WKWebView->evaluateJS, so it's better not to call it too often.
void (*function)() = NULL;
function = dlsym(RTLD_MAIN_ONLY, "stopInteractive");
if (function != NULL) {
function();
} else {
NSLog(@"Could not find function stopInteractive");
}
}
int ios_storeInteractive(void) {
// Some commands, like dash, can be started from inside interactive or non-interactive commands. They need to restore the status afterwards.
int (*function)() = NULL;
function = dlsym(RTLD_MAIN_ONLY, "storeInteractive");
if (function != NULL) {
return function();
} else {
NSLog(@"Could not find function storeInteractive");
return 0;
}
}
void ios_startInteractive(void) {
// With aliasing, we can have commands that are interactive and not detected by the command-line interpreter.
void (*function)() = NULL;
function = dlsym(RTLD_MAIN_ONLY, "startInteractive");
if (function != NULL) {
function();
} else {
NSLog(@"Could not find function startInteractive");
}
}
static int isInteractive(const char* command) {
// let interactiveRegexp = /^vim|^ipython|^less|^more|^ssh|^scp|^sftp|^jump|\|&? *less|\|&? *more|^man|^pico/;
if (strncmp(command, "vim", 3) == 0) return true;
if (strncmp(command, "pico", 4) == 0) return true;
if (strncmp(command, "ipython", 7) == 0) return true;
if (strncmp(command, "isympy", 6) == 0) return true;
if (strncmp(command, "less", 4) == 0) return true;
if (strncmp(command, "more", 4) == 0) return true;
if (strncmp(command, "ssh", 3) == 0) return true;
if (strncmp(command, "scp", 3) == 0) return true;
if (strncmp(command, "man", 3) == 0) return true;
if (strncmp(command, "sftp", 4) == 0) return true;
if (strncmp(command, "jump", 4) == 0) return true;
return false;
}
void ios_setContext(const void *context) {
if (currentSession == NULL) return;
currentSession->context = context;
}
void* ios_getContext() {
if (currentSession == NULL) return NULL;
return currentSession->context;
}
// For customization:
// replaces a function (e.g. ls_main) with another one, provided by the user (ls_mine_main)
// if the function does not exist, add it to the list
// if "allOccurences" is true, search for all commands that share the same function, replace them too.
// ("compress" and "uncompress" both point to compress_main. You probably want to replace both, but maybe
// you just happen to have a very fast uncompress, different from compress).
// We work with function names, not function pointers.
void replaceCommand(NSString* commandName, NSString* functionName, bool allOccurences) {
// Does that function exist / is reachable? We've had problems with stripping.
int (*function)(int ac, char** av) = NULL;
function = dlsym(RTLD_MAIN_ONLY, functionName.UTF8String);
if (!function) {
NSLog(@"replaceCommand: %@ does not exist", functionName);
return; // if not, we don't replace.
}
if (commandList == nil) initializeCommandList();
NSArray* oldValues = [commandList objectForKey: commandName];
NSString* oldFunctionName = nil;
if (oldValues != nil) oldFunctionName = oldValues[1];
NSMutableDictionary *mutableDict = [commandList mutableCopy];
mutableDict[commandName] = [NSArray arrayWithObjects: @"MAIN", functionName, @"", @"file", nil];
if ((oldFunctionName != nil) && allOccurences) {
// scan through all dictionary entries
for (NSString* existingCommand in mutableDict.allKeys) {
NSArray* currentPosition = [mutableDict objectForKey: existingCommand];
if ([currentPosition[1] isEqualToString:oldFunctionName])
[mutableDict setValue: [NSArray arrayWithObjects: @"MAIN", functionName, @"", @"file", nil] forKey: existingCommand];
}
}
commandList = [mutableDict copy]; // back to non-mutable version
}
// For customization:
// Add an entire plist file defining multiple commands. Commands follow the same syntax as initializeCommandList:
//
// key = command name, followed by an array of 4 components:
// 1st component: name of digital library (can be "MAIN" if command is defined inside program)
// 2nd component: name of function to be called
// 3rd component: chain sent to getopt (for arguments in autocomplete)
// 4th component: takes a file/directory as argument
//
// Example:
// <key>rlogin</key>
// <array>
// <string>libnetwork_ios.dylib</string>
// <string>rlogin_main</string>
// <string>468EKLNS:X:acde:fFk:l:n:rs:uxy</string>
// <string>no</string>
// </array>
NSError* addCommandList(NSString* fileLocation) {
if (commandList == nil) initializeCommandList();
NSError* error;
NSURL *locationURL = [NSURL fileURLWithPath:fileLocation isDirectory:NO];
if ([locationURL checkResourceIsReachableAndReturnError:&error] == NO) {
fprintf(stderr, "Resource dictionary %s not found", fileLocation.UTF8String);
return error;
}
NSData* dataLoadedFromFile = [NSData dataWithContentsOfFile:fileLocation options:0 error:&error];
if (!dataLoadedFromFile) return error;
NSDictionary* newCommandList = [NSPropertyListSerialization propertyListWithData:dataLoadedFromFile options:NSPropertyListImmutable format:NULL error:&error];
if (!newCommandList) return error;
// merge the two dictionaries:
NSMutableDictionary *mutableDict = [commandList mutableCopy];
[mutableDict addEntriesFromDictionary:newCommandList];
commandList = [mutableDict copy];
return NULL;
}
NSString* commandsAsString() {
if (commandList == nil) initializeCommandList();
NSError * err;
NSData * jsonData = [NSJSONSerialization dataWithJSONObject:commandList.allKeys options:0 error:&err];
NSString * myString = [[NSString alloc] initWithData:jsonData encoding:NSUTF8StringEncoding];
return myString;
}
NSArray* commandsAsArray() {
if (commandList == nil) initializeCommandList();
return commandList.allKeys;
}
// for output file names, arguments: returns a pointer to
// immediately after the end of the argument, or NULL.
// Method:
// - if argument begins with ", go to next unescaped "
// - if argument begins with ', go to next unescaped '
// - otherwise, move to next unescaped space
//
// Must be combined with another function to remove backslash.
// Aux function:
static void* nextUnescapedCharacter(const char* str, const char c) {
char* nextOccurence = strchr(str, c);
while (nextOccurence != NULL) {
if ((nextOccurence > str + 1) && (*(nextOccurence - 1) == '\\')) {
// There is a backlash before the character.
int numBackslash = 0;
char* countBack = nextOccurence - 1;
while ((countBack > str) && (*countBack == '\\')) { numBackslash++; countBack--; }
if (numBackslash % 2 == 0) return nextOccurence; // even number of backslash
} else return nextOccurence;
nextOccurence = strchr(nextOccurence + 1, c);
}
return nextOccurence;
}
static char* getLastCharacterOfArgument(const char* argument) {
// Problem: perl -e 'install([ from_to => {@ARGV}, verbose => '\''0'\'', uninstall_shadows => '\''0'\'', dir_mode => '\''755'\'' ]);'
// Should become: "perl", "-e", "install([ from_to => {@ARGV}, verbose => '0', uninstall_shadows => '0', dir_mode => '755' ]);"
// If there is no space after the end quote, keep concatenating the argument.
char recordSeparator = 0x1e;
if (strlen(argument) == 0) return NULL; // be safe
if (argument[0] == '"') {
char* endquote = nextUnescapedCharacter(argument + 1, '"');
if (endquote != NULL) {
// Is there a space after the endquote?
if (strlen(endquote) <= 1) { return endquote + 1; } // last character
if (endquote[1] == ' ') { return endquote + 1; } // space after endquote, we're good
if (strncmp(endquote, "\"\\\"\"", 3) == 0 ) {
// Perl (for example) wrote here: "\"" and if the substitution works we get " inside the argument
// We rewrite the argument here (it gets shorter):
char* write = endquote;
for (char* read = endquote + 3; *read != 0; read++, write++) {
*write = *read;
}
write[0] = 0;
return getLastCharacterOfArgument(endquote + 1);
}
return endquote + 1;
}
return NULL;
} else if (argument[0] == '\'') {
char* endquote = nextUnescapedCharacter(argument + 1, '\'');
if (endquote != NULL) {
// Is there a space after the endquote?
if (strlen(endquote) <= 1) { return endquote + 1; } // last character
if (endquote[1] == ' ') { return endquote + 1; } // space after endquote, we're good
if (strncmp(endquote, "'\\''", 3) == 0 ) {
// Perl (for example) wrote here: '\'' and if the substitution works we get ' inside the argument
// We rewrite the argument here (it gets shorter):
// /!\ There could be other cases, e.g. '"', but why would they do that?
char* write = endquote;
for (char* read = endquote + 3; *read != 0; read++, write++) {
*write = *read;
}
write[0] = 0;
return getLastCharacterOfArgument(endquote);
}
return endquote + 1;
}
return NULL;
} else if (argument[0] == recordSeparator) {
char* endquote = strchr(argument + 1, recordSeparator);
return endquote + 1;
}
// TODO: the last character of the argument could also be '<' or '>' (vim does that, with no space after file name)
else return nextUnescapedCharacter(argument + 1, ' ');
}
// remove quotes at the beginning of argument if there's a balancing one at the end
static char* unquoteArgument(char* argument) {
if (argument[0] == '"') {
if (argument[strlen(argument) - 1] == '"') {
argument[strlen(argument) - 1] = 0x0;
return argument + 1;
}
}
if (argument[0] == '\'') {
if (argument[strlen(argument) - 1] == '\'') {
argument[strlen(argument) - 1] = 0x0;
return argument + 1;
}
}
char recordSeparator = 0x1e;
if (argument[0] == recordSeparator) {
if (argument[strlen(argument) - 1] == recordSeparator) {
argument[strlen(argument) - 1] = 0x0;
return argument + 1;
}
}
// no quotes at the beginning: replace all escaped characters:
// '\x' -> x
char* nextOccurence = strchr(argument, '\\');
while ((nextOccurence != NULL) && (strlen(nextOccurence) > 0)) {
memmove(nextOccurence, nextOccurence + 1, strlen(nextOccurence + 1) + 1);
// strcpy(nextOccurence, nextOccurence + 1);
nextOccurence = strchr(nextOccurence + 1, '\\');
}
return argument;
}
static int isRealCommand(const char* fileName) {
// File exists, is executable, not a directory.
// We check whether: a) its size is > 0 and b) it is not a Mach-O binary
int returnValue = false;
struct stat sb;
if (stat(fileName, &sb) == 0) {
// We can have an empty file with the same name in the path, to fool which():
if (sb.st_size == 0) {
return false;
}
// Not an empty file, so let's check its magic number:
int fd = open(fileName, O_RDONLY);
if (fd > 0) {
char res[4];
ssize_t retval = read(fd, &res, 4);
if (retval == 4) {
// MH_MAGIC_64 = 0xfeedfacf
if ((res[0] != '\xcf') || (res[1] != '\xfa') || (res[2] != '\xed') || (res[3] != '\xfe')) {
// it's not a Mach-O binary:
returnValue = true;
}
}
close (fd);
}
}
return returnValue;
}
static bool isBackgroundCommand(char* command) {
if (backgroundCommandList == nil) {
return false;
}
if (command == NULL) {
return false;
}
NSString *commandAsString = [NSString stringWithUTF8String: command];
for (NSString* commandInList in backgroundCommandList) {
if ([commandInList isEqualToString: commandAsString]) {
// NSLog(@"%s is a backgroundCommand\n", command);
return true;
}
if ([commandInList hasSuffix:@"*"]) {
NSString* shortCommand = commandInList;
shortCommand = [shortCommand substringToIndex:[shortCommand length] - 1];
while ([shortCommand hasSuffix:@" "]) {
shortCommand = [shortCommand substringToIndex:[shortCommand length] - 1];
}
if ([commandAsString hasPrefix: shortCommand]) {
// NSLog(@"%s is a backgroundCommand\n", command);
return true;
}
}
}
return false;
}
NSString* beforeScriptCommandName(NSString* scriptName) {
// scans the PATH for a binary that has the name script and non-null size,
// checks whether it has wasm signature, if so insert "wasm" before script name.
BOOL isDir = false;
NSFileManager *fileManager = [[NSFileManager alloc] init];
directoriesInPath = [fullCommandPath componentsSeparatedByString:@":"];
for (NSString* path in directoriesInPath) {
// If we don't have access to the path component, there's no point in continuing:
if (![fileManager fileExistsAtPath:path isDirectory:&isDir]) continue;
if (!isDir) continue; // same in the (unlikely) event the path component is not a directory
NSString* locationName;
// search for 2 possibilities: name and name.wasm
locationName = [path stringByAppendingPathComponent:scriptName];
bool fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
if (!fileFound) {
locationName = [[path stringByAppendingPathComponent:scriptName] stringByAppendingString:@".wasm"];
fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
}
if (!fileFound) continue;
// isExecutableFileAtPath replies "NO" even if file has x-bit set.
// if (![fileManager isExecutableFileAtPath:cmdname]) continue;
struct stat sb;
// Files inside the Application Bundle will always have "x" removed. Don't check.
if (!([path containsString: [[NSBundle mainBundle] resourcePath]]) // Not inside the App Bundle
&& !((stat(locationName.UTF8String, &sb) == 0))) // file exists, is not a directory
continue;
// At this point the file exists, is a file.
if (!isRealCommand(locationName.UTF8String)) // if it's one of our fake commands, search is over.
return NULL;
NSData *data = [NSData dataWithContentsOfFile:locationName]; // You have the data. Conversion to String probably failed.
NSString *fileContent = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
if ((fileContent == nil) && (data.length > 0)) {
// Conversion to string failed with UTF8. Try with Ascii as a backup:
fileContent = [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
}
NSString* signature;
// Detect WebAssembly file signature: '\0asm' (begins with 0, so not a string)
if ((data.length >0) && (((char*)data.bytes)[0] == 0)) {
// fileContent = [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
NSRange signatureRange = NSMakeRange(1, 3);
signature = [fileContent substringWithRange:signatureRange];
}
if ([signature isEqualToString:@"asm"]) {
return [@"wasm " stringByAppendingString:locationName];
}
}
// We didn't find anything, no change to scriptName
return NULL;
}
int ios_system(const char* inputCmd) {
NSLog(@"command= %s pid= %d\n", inputCmd, ios_currentPid());
char* command;
// The names of the files for stdin, stdout, stderr
char* inputFileName = 0;
char* outputFileName = 0;
char* errorFileName = 0;
// Where the symbols "<", ">" or "2>" were.
// to be replaced by 0x0 later.
char* outputFileMarker = 0;
char* inputFileMarker = 0;
char* errorFileMarker = 0;
char* scriptName = 0; // interpreted commands
bool sharedErrorOutput = false;
NSFileManager *fileManager = [[NSFileManager alloc] init];
// NSLog(@"ios_system, stdout %d \n", thread_stdout == NULL ? 0 : fileno(thread_stdout));
// NSLog(@"ios_system, stderr %d \n", thread_stderr == NULL ? 0 : fileno(thread_stderr));
if (currentSession == NULL) {
currentSession = malloc(sizeof(sessionParameters));
initSessionParameters(currentSession);
}
currentSession->global_errno = 0;
NSLog(@"Starting command: %s currentSession->isMainThread: %d", inputCmd, currentSession->isMainThread);
// Don't start if the command is NULL:
if (inputCmd == NULL) {
ios_storeThreadId(0);
return 0;
}
// initialize:
if (thread_stdin == 0) thread_stdin = currentSession->stdin;
if (thread_stdout == 0) thread_stdout = currentSession->stdout;
if (thread_stderr == 0) thread_stderr = currentSession->stderr;
if (thread_context == 0) thread_context = currentSession->context;
char* cmd = strdup(inputCmd);
char* maxPointer = cmd + strlen(cmd);
char* originalCommand = cmd;
// fprintf(thread_stderr, "Command sent: %s \n", cmd); fflush(stderr);
if (cmd[0] == '"') {
// Command was enclosed in quotes (almost always with Vim)
// It can also be the executable enclose in quotes
char* endCmd = strstr(cmd + 1, "\""); // find closing quote
if (endCmd) {
cmd = cmd + 1; // remove starting quote
endCmd[0] = ' ';
assert(endCmd < maxPointer);
}
// assert(cmd + strlen(cmd) < maxPointer);
}
if (cmd[0] == '(') {
// Standard vim encoding: command between parentheses
command = cmd + 1;
char* endCmd = strstrquoted(command, ")"); // remove closing parenthesis
if (endCmd) {
endCmd[0] = ' ';
assert(endCmd < maxPointer);
// inputFileMarker = endCmd + 1;
}
} else command = cmd;
// fprintf(thread_stderr, "Command sent: %s \n", command);
// Environment variables before alias expansion:
char* commandForParsing = strdup(command);
char* commandForParsingFree = commandForParsing;
char* firstSpace = strstrquoted(commandForParsing, " ");
while (firstSpace != NULL) {
*firstSpace = 0;
char* equalSign = strchr(commandForParsing, '=');
if (equalSign == NULL) break;
*equalSign = 0;
ios_setenv(commandForParsing, equalSign+1, 1);
command += (firstSpace - commandForParsing) + 1;
commandForParsing = firstSpace + 1;
firstSpace = strstrquoted(commandForParsing, " ");
}
free(commandForParsingFree);
// alias expansion *before* input, output and error redirection.
if ((command[0] != '\\') && (aliasDictionary != nil)) {
// \command = cancel aliasing, get the original command
char* commandForParsing = strdup(command);
char* firstSpace = strstrquoted(commandForParsing, " ");
if (firstSpace != NULL) { *firstSpace = 0; }
NSString* commandAsString = [NSString stringWithCString:commandForParsing encoding:NSUTF8StringEncoding];
NSArray<NSString*>* aliasedCommand = aliasDictionary[commandAsString];
if (aliasedCommand != nil) {
NSLog(@"%s %s %s", aliasedCommand[0].UTF8String, aliasedCommand[1].UTF8String, aliasedCommand[2].UTF8String);
char* newCommand = NULL;
if (aliasedCommand[2].length == 0) {
// all the alias, then all the arguments:
if (firstSpace == NULL) {
newCommand = strdup(aliasedCommand[0].UTF8String);
} else {
unsigned long newCommandLength = aliasedCommand[0].length + 2 + strlen(firstSpace+1);
// + 2: 1 for space, 1 for NULL termination
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s", aliasedCommand[0].UTF8String, firstSpace+1);
}
} else if ([aliasedCommand[2] isEqualToString: @"afterLast"]) {
unsigned long newCommandLength = aliasedCommand[0].length + 2 + aliasedCommand[1].length;
// + 2: 1 for space, 1 for NULL termination
if (firstSpace == NULL) { // no arguments
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s", aliasedCommand[0].UTF8String, aliasedCommand[1].UTF8String);
} else {
newCommandLength += strlen(firstSpace+1) + 1; // 1 more space
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s %s", aliasedCommand[0].UTF8String, firstSpace+1, aliasedCommand[1].UTF8String);
}
} else if ([aliasedCommand[2] isEqualToString: @"afterFirst"]) {
unsigned long newCommandLength = aliasedCommand[0].length + 2 + aliasedCommand[1].length;
// + 2: 1 for space, 1 for NULL termination
if (firstSpace == NULL) { // no arguments
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s", aliasedCommand[0].UTF8String, aliasedCommand[1].UTF8String);
} else {
char* arguments = firstSpace + 1;
char* secondSpace = strstrquoted(arguments, " ");
if (secondSpace == NULL) {
// only 1 argument, nothing after that:
newCommandLength += strlen(arguments) + 1; // 1 more space
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s %s", aliasedCommand[0].UTF8String, arguments, aliasedCommand[1].UTF8String);
} else {
*secondSpace = 0;
char* otherArguments = secondSpace + 1;
newCommandLength += strlen(arguments) + strlen(otherArguments) + 2; // 2 more spaces
newCommand = malloc(newCommandLength * sizeof(char));
sprintf(newCommand, "%s %s %s %s", aliasedCommand[0].UTF8String, arguments, aliasedCommand[1].UTF8String, otherArguments);
}
}
}
if (newCommand != NULL) {
free(originalCommand);
// After alias expansion, the new command replaces the old one:
originalCommand = newCommand;
cmd = newCommand;
command = newCommand;
}
}
free(commandForParsing);
}
// Maybe we aliased to an interactive command (vim, ssh, less, more, man, scp, sftp)
// We need to tell the command line editor:
if (isInteractive(command)) {
ios_startInteractive();
}
// NSLog(@"command after alias expansion= %s\n", command);
// Search for input, output and error redirection
// They can be in any order, although the usual are:
// command < input > output 2> error, command < input > output 2>&1 or command < input >& output
// The last two are equivalent. Vim prefers the second.
// Search for input file "< " and output file " >"
if (!inputFileMarker) inputFileMarker = command;
outputFileMarker = inputFileMarker;
functionParameters *params = (functionParameters*) malloc(sizeof(functionParameters));
// If child_streams have been defined (in dup2 or popen), the new thread takes them.
params->stdin = child_stdin;
params->stdout = child_stdout;
params->stderr = child_stderr;
params->session = currentSession;
params->backgroundCommand = isBackgroundCommand(command);
params->numInterpreter = 0;
params->context = thread_context;
child_stdin = child_stdout = child_stderr = NULL;
params->argc = 0; params->argv = 0; params->argv_ref = 0;
params->function = NULL; params->isPipeOut = false; params->isPipeErr = false;
// Only scan for input / output if there is no argument marker
char recordSeparator = 0x1e;
char* recordSeparatorPosition = strchr(inputFileMarker, recordSeparator);
if (recordSeparatorPosition == NULL) {
// scan until first "<" (input file)
inputFileMarker = strstrquoted(inputFileMarker, "<");
// scan until first non-space character:
if (inputFileMarker) {
if ((strlen(inputFileMarker) > 1) && (inputFileMarker[1] == '=')) {
// >= (used by pip install, e.g. "setuptools<=56"
// This is very specific. pip needs it, other commands act differently.
char* doubleDashMarker = strstrquoted(command, " -- ");
// Is there a double dash before the ">="? If not keep going.
if (!doubleDashMarker || (doubleDashMarker > inputFileMarker)) {
inputFileName = inputFileMarker + 1; // skip past '<'
}
} else {
inputFileName = inputFileMarker + 1; // skip past '<'
}
if (inputFileName) {
// skip past all spaces
while ((inputFileName[0] == ' ') && strlen(inputFileName) > 0) inputFileName++;
}
}
// is there a pipe ("|", "&|" or "|&")
// We assume here a logical command order: < before pipe, pipe before >.
// TODO: check what happens for unlogical commands. Refuse them, but gently.
// TODO: implement tee, because that has been removed
char* pipeMarker = strstrquoted(outputFileMarker,"&|");
if (!pipeMarker) pipeMarker = strstrquoted(outputFileMarker,"|&"); // both seem to work
if (pipeMarker) {
bool pushMainThread = currentSession->isMainThread;
currentSession->isMainThread = false;
if (params->stdout != 0) thread_stdout = params->stdout;
if (params->stderr != 0) thread_stderr = params->stderr;
// if popen fails, don't start the command
params->stdout = ios_popen(pipeMarker+2, "w");
params->stderr = params->stdout;
currentSession->isMainThread = pushMainThread;
pipeMarker[0] = 0x0;
sharedErrorOutput = true;
if (params->stdout == NULL) { // pipe open failed, return before we start a command
NSLog(@"Failed launching pipe for %s\n", pipeMarker+2);
ios_storeThreadId(0);
free(params);
free(originalCommand); // releases cmd, which was a strdup of inputCommand
return currentSession->global_errno;
}
} else {
pipeMarker = strstrquoted(outputFileMarker,"|");
if (pipeMarker) {
bool pushMainThread = currentSession->isMainThread;
currentSession->isMainThread = false;
if (params->stdout != 0) thread_stdout = params->stdout;
if (params->stderr != 0) thread_stderr = params->stderr; // ?????
// if popen fails, don't start the command
params->stdout = ios_popen(pipeMarker+1, "w");
currentSession->isMainThread = pushMainThread;
pipeMarker[0] = 0x0;
if (params->stdout == NULL) { // pipe open failed, return before we start a command
NSLog(@"Failed launching pipe for %s\n", pipeMarker+1);
ios_storeThreadId(0);
free(params);
free(originalCommand); // releases cmd, which was a strdup of inputCommand
return currentSession->global_errno;
}
}
}
// We have removed the pipe part. Still need to parse the rest of the command
// Must scan in strstr by reverse order of inclusion. So "2>&1" before "2>" before ">"
errorFileMarker = strstrquoted(outputFileMarker,"&>"); // both stderr/stdout sent to same file
// output file name will be after "&>"
if (errorFileMarker) { outputFileName = errorFileMarker + 2; outputFileMarker = errorFileMarker; }
if (!errorFileMarker) {
// TODO: 2>&1 before > means redirect stderr to (current) stdout, then redirects stdout
// ...except with a pipe.
// Currently, we don't check for that.
errorFileMarker = strstrquoted(outputFileMarker,"2>&1"); // both stderr/stdout sent to same file
if (errorFileMarker) {
outputFileName = NULL;
if (params->stdout) params->stderr = params->stdout;
outputFileMarker = strstrquoted(outputFileMarker, ">");
if (outputFileMarker - errorFileMarker == 1) // the first '>' was the one from '2>&1'
outputFileMarker = strstrquoted(outputFileMarker + 2, ">"); // is there one after that?
if (outputFileMarker)
outputFileName = outputFileMarker + 1; // skip past '>'
}
}
if (errorFileMarker) { sharedErrorOutput = true; }
else {
// specific name for error file?
errorFileMarker = strstrquoted(outputFileMarker,"2>");
if (errorFileMarker) {
errorFileName = errorFileMarker + 2; // skip past "2>"
// skip past all spaces:
while ((errorFileName[0] == ' ') && strlen(errorFileName) > 0) errorFileName++;
}
}
// scan until first ">"
bool appendToFileName = false;
if (!sharedErrorOutput) {
// output and append.
outputFileMarker = strstrquoted(outputFileMarker, ">");
if (outputFileMarker) {
if ((strlen(outputFileMarker) > 1) && (outputFileMarker[1] == '>')) { // >>
outputFileName = outputFileMarker + 2; // skip past ">>"
appendToFileName = true;
} else if ((strlen(outputFileMarker) > 1) && (outputFileMarker[1] == '=')) {
// >= (used by pip install, e.g. "setuptools>=56,!=61.0.0"
// This is very specific. pip needs it, other commands act differently.
char* doubleDashMarker = strstrquoted(command, " -- ");
// Is there a double dash before the ">="? If not keep going.
if (!doubleDashMarker || (doubleDashMarker > outputFileMarker)) {
outputFileName = outputFileMarker + 1; // skip past '>'
} // Otherwise do nothing
} else {
outputFileName = outputFileMarker + 1; // skip past '>'
}
}
} else {
if (outputFileName == NULL)
outputFileMarker = NULL;
}
if (outputFileName) {
while ((outputFileName[0] == ' ') && strlen(outputFileName) > 0) outputFileName++;
}
if (errorFileName && (outputFileName == errorFileName)) {
// we got the same ">" twice, pick the next one ("2>" was before ">")
outputFileMarker = errorFileName;
outputFileMarker = strstrquoted(outputFileMarker, ">");
if (outputFileMarker) {
if ((strlen(outputFileMarker) > 1) && (outputFileMarker[1] == '>')) { // >>
outputFileName = outputFileMarker + 2; // skip past ">>"
appendToFileName = true;
} else {
outputFileName = outputFileMarker + 1; // skip past '>'
}
while ((outputFileName[0] == ' ') && strlen(outputFileName) > 0) outputFileName++;
} else outputFileName = NULL; // Only "2>", but no ">". It happens.
}
if (outputFileName) {
char* endFile = getLastCharacterOfArgument(outputFileName);
if (endFile) endFile[0] = 0x00; // end output file name at first space
assert(endFile <= maxPointer);
}
if (inputFileName) {
char* endFile = getLastCharacterOfArgument(inputFileName);
if (endFile) endFile[0] = 0x00; // end input file name at first space
assert(endFile <= maxPointer);
}
if (errorFileName) {
char* endFile = getLastCharacterOfArgument(errorFileName);
if (endFile) endFile[0] = 0x00; // end error file name at first space
assert(endFile <= maxPointer);
}
// insert chain termination elements at the beginning of each filename.
// Must be done after the parsing.
if (inputFileMarker) inputFileMarker[0] = 0x0;
// There was a test " && (params->stdout == NULL)" below. Why?
if (outputFileMarker) outputFileMarker[0] = 0x0; // There
if (errorFileMarker) errorFileMarker[0] = 0x0;
// strip filenames of quotes, if any:
if (outputFileName) outputFileName = unquoteArgument(outputFileName);
if (inputFileName) inputFileName = unquoteArgument(inputFileName);
if (errorFileName) errorFileName = unquoteArgument(errorFileName);
//
FILE* newStream;
if (inputFileName) {
newStream = fopen(ios_expandFilename(inputFileName), "r");
if (newStream) params->stdin = newStream;
}
if (params->stdin == NULL) params->stdin = thread_stdin;
if (outputFileName) {
if (appendToFileName) {
newStream = fopen(ios_expandFilename(outputFileName), "a"); // append
} else {
newStream = fopen(ios_expandFilename(outputFileName), "w");
}
// NSLog(@"Opened %s as output file: %x", outputFileName, newStream);
if (newStream) {
if (params->stdout != NULL) {
if (fileno(params->stdout) != fileno(currentSession->stdout)) fclose(params->stdout);
}
params->stdout = newStream;
}
}
if (params->stdout == NULL) params->stdout = thread_stdout;
if (sharedErrorOutput && (params->stderr != params->stdout)) {
if (params->stderr != NULL) {
if (fileno(params->stderr) != fileno(currentSession->stderr)) fclose(params->stderr);
}
params->stderr = params->stdout;
}
else if (errorFileName) {
newStream = NULL;
newStream = fopen(ios_expandFilename(errorFileName), "w");
if (newStream) {
if (params->stderr != NULL) {
if (fileno(params->stderr) != fileno(currentSession->stderr)) fclose(params->stderr);
}
params->stderr = newStream;
}
}
if (params->stderr == NULL) params->stderr = thread_stderr;
} // recordSeparator != NULL
if (params->stdin == NULL) params->stdin = thread_stdin;
if (params->stdout == NULL) params->stdout = thread_stdout;
if (params->stderr == NULL) params->stderr = thread_stderr;
int argc = 0;
size_t numSpaces = 0;
// the number of arguments is *at most* the number of spaces plus one
char* str = command;
while(*str) if (*str++ == ' ') ++numSpaces;
char** argv = (char **)malloc(sizeof(char*) * (numSpaces + 2));
bool* dontExpand = malloc(sizeof(bool) * (numSpaces + 2));
// n spaces = n+1 arguments, plus null at the end
str = command;
while (*str) {
argv[argc] = str;
dontExpand[argc] = false;
argc += 1;
if ((argc == 2) && (strcmp(argv[0], "export") == 0)) break; // don't try to unquote the argument of export.
char* end = getLastCharacterOfArgument(str);
bool mustBreak = (end == NULL) || (strlen(end) == 0);
if (!mustBreak) end[0] = 0x0;
if ((str[0] == '\'') || (str[0] == '"') || (str[0] == recordSeparator)) {
dontExpand[argc-1] = true; // don't expand arguments in quotes
}
argv[argc-1] = unquoteArgument(argv[argc-1]);
if (mustBreak) break;
str = end + 1;
assert(argc < numSpaces + 2);
while (str && (str[0] == ' ')) str++; // skip multiple spaces
}
argv[argc] = NULL;
if (argc != 0) {
// So far, all arguments are pointers into originalCommand.
// We need to change them (environment variables expansion, ~ expansion, etc)
// Duplicate everything so we can realloc:
char** argv_copy = (char **)malloc(sizeof(char*) * (argc + 1));
for (int i = 0; i < argc; i++) argv_copy[i] = strdup(argv[i]);
argv_copy[argc] = NULL;
free(argv);
argv = argv_copy;
// We have the arguments. Parse them for environment variables, ~, etc.
for (int i = 1; i < argc; i++) if (!dontExpand[i]) { argv[i] = parseArgument(argv[i], argv[0]); }
// wildcard expansion (*, ?, []...) Has to be after $ and ~ expansion, results in larger arguments
for (int i = 1; i < argc; i++) if (!dontExpand[i]) {
if (strstrquoted (argv[i],"*") || strstrquoted (argv[i],"?") || strstrquoted (argv[i],"[")) {
glob_t gt;
if (glob(argv[i], 0, NULL, &gt) == 0) {
argc += gt.gl_matchc - 1;
argv = (char **)realloc(argv, sizeof(char*) * (argc + 1));
dontExpand = (bool *)realloc(dontExpand, sizeof(bool) * (argc + 1));
// Move everything after i by gt.gl_matchc - 1 steps up:
for (int j = argc; j - gt.gl_matchc + 1 > i; j--) {
argv[j] = argv[j - gt.gl_matchc + 1];
dontExpand[j] = dontExpand[j - gt.gl_matchc + 1];
}
for (int j = 0; j < gt.gl_matchc; j++) {
argv[i + j] = strdup(gt.gl_pathv[j]);
}
i += gt.gl_matchc - 1;
globfree(&gt);
} else {
// If there is no match, leave parameter as is, continue with command.
// Not exactly Unix behaviour, but more convenient on Phones.
// fprintf(params->stderr, "%s: %s: No match\n", argv[0], argv[i]);
// fflush(params->stderr);
globfree(&gt);
}
}
}
free(dontExpand);
// Now call the actual command:
// - is argv[0] a command that refers to a file? (either absolute path, or in $PATH)
// if so, does it exist, does it have +x bit set, does it have #! python or #! lua on the first line?
// if yes to all, call the relevant interpreter. Works for hg, for example.
if (argv[0][0] == '\\') {
// Just remove the \ at the beginning
// There can be several versions of a command (e.g. ls as precompiled and ls written in Python)
// The executable file has precedence, unless the user has specified they want the original
// version, by prefixing it with \. So "\ls" == always "our" ls. "ls" == maybe ~/Library/bin/ls
// (if it exists).
// It also cancels aliases.
size_t len_with_terminator = strlen(argv[0] + 1) + 1;
memmove(argv[0], argv[0] + 1, len_with_terminator);
} else {
NSString* commandName = [NSString stringWithCString:argv[0] encoding:NSUTF8StringEncoding];
// strcpy(currentSession->commandName, argv[0]);
// store into heap:
if ((currentSession->numCommand <= 0) && (strlen(currentSession->commandName[0]) == 0)) {
strcpy(currentSession->commandName[0], argv[0]);
currentSession->numCommand = 1;
} else {
if (currentSession->numCommand >= currentSession->numCommandsAllocated) {
int oldCommandsAllocated = currentSession->numCommandsAllocated;
currentSession->numCommandsAllocated += 10;
currentSession->commandName = realloc(currentSession->commandName, sizeof(char*) * currentSession->numCommandsAllocated);
for (int i = oldCommandsAllocated; i < currentSession->numCommandsAllocated; i++) {
currentSession->commandName[i] = malloc(sizeof(char) * NAME_MAX);
}
}
strcpy(currentSession->commandName[currentSession->numCommand], argv[0]);
currentSession->numCommand += 1;
}
BOOL isDir = false;
bool cmdIsAFile = false;
bool cmdIsReal = false;
bool cmdIsAPath = false;
if ([commandName hasPrefix:@"~/"]) {
NSString* replacement_string = [NSString stringWithCString:(getenv("HOME")) encoding:NSUTF8StringEncoding];
NSString* test_string = @"~";
commandName = [commandName stringByReplacingOccurrencesOfString:test_string withString:replacement_string options:NULL range:NSMakeRange(0, 1)];
}
if ([fileManager fileExistsAtPath:commandName isDirectory:&isDir] && (!isDir)) {
// File exists, is a file.
struct stat sb;
if (stat(commandName.UTF8String, &sb) == 0) {
// File exists, is executable, not a directory.
cmdIsAFile = true;
// We can have an empty file with the same name in the path, to fool which():
// We can also have a Mach-O binary with the same name in the path (in simulator, mostly)
cmdIsReal = isRealCommand(commandName.UTF8String);
}
}
// if commandName contains "/", then it's a path, and we don't search for it in PATH.
cmdIsAPath = ([commandName rangeOfString:@"/"].location != NSNotFound);
if (!cmdIsAPath || cmdIsAFile) {
// We go through the path, because that command may be a file in the path
NSString* checkingPath = [NSString stringWithCString:getenv("PATH") encoding:NSUTF8StringEncoding];
if (! [fullCommandPath isEqualToString:checkingPath]) {
fullCommandPath = checkingPath;
directoriesInPath = [fullCommandPath componentsSeparatedByString:@":"];
}
for (NSString* path in directoriesInPath) {
// If we don't have access to the path component, there's no point in continuing:
if (![fileManager fileExistsAtPath:path isDirectory:&isDir]) continue;
if (!isDir) continue; // same in the (unlikely) event the path component is not a directory
NSString* locationName;
if (!cmdIsAFile) {
// search for 4 possibilities: name, name.bc, name.ll and name.wasm
locationName = [path stringByAppendingPathComponent:commandName];
bool fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
if (!fileFound) {
locationName = [[path stringByAppendingPathComponent:commandName] stringByAppendingString:@".bc"];
fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
}
if (!fileFound) {
locationName = [[path stringByAppendingPathComponent:commandName] stringByAppendingString:@".ll"];
fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
}
if (!fileFound) {
locationName = [[path stringByAppendingPathComponent:commandName] stringByAppendingString:@".wasm"];
fileFound = [fileManager fileExistsAtPath:locationName isDirectory:&isDir];
fileFound = fileFound && !isDir;
}
if (!fileFound) continue;
// isExecutableFileAtPath replies "NO" even if file has x-bit set.
// if (![fileManager isExecutableFileAtPath:cmdname]) continue;
struct stat sb;
// Files inside the Application Bundle will always have "x" removed. Don't check.
if (!([path containsString: [[NSBundle mainBundle] resourcePath]]) // Not inside the App Bundle
&& !((stat(locationName.UTF8String, &sb) == 0))) // file exists, is not a directory
continue;
} else
// if (cmdIsAFile) we are now ready to execute this file:
locationName = commandName;
if (([locationName hasSuffix:@".bc"]) || ([locationName hasSuffix:@".ll"])) {
// CLANG bitcode. insert lli in front of argument list:
argc += 1;
argv = (char **)realloc(argv, sizeof(char*) * argc);
// Move everything one step up
for (int i = argc; i >= 1; i--) { argv[i] = argv[i-1]; }
argv[1] = realloc(argv[1], locationName.length + 1);
strcpy(argv[1], locationName.UTF8String);
argv[0] = strdup("lli"); // this argument is new
break;
} else if ([locationName hasSuffix:@".wasm"]) {
// insert wasm in front of argument list:
argc += 1;
argv = (char **)realloc(argv, sizeof(char*) * (argc + 1));
// Move everything one step up
for (int i = argc-1; i >= 1; i--) { argv[i] = argv[i-1]; }
argv[1] = realloc(argv[1], locationName.length + 1);
strcpy(argv[1], locationName.UTF8String);
argv[0] = strdup("wasm"); // this argument is new
break;
} else {
if (isRealCommand(locationName.UTF8String)) {
cmdIsReal = true;
NSData *data = [NSData dataWithContentsOfFile:locationName]; // You have the data. Conversion to String probably failed.
NSString *fileContent = [[NSString alloc]initWithData:data encoding:NSUTF8StringEncoding];
if ((fileContent == nil) && (data.length > 0)) {
// Conversion to string failed with UTF8. Try with Ascii as a backup:
fileContent = [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
}
NSString* firstLine;
if (fileContent != nil) {
NSRange firstLineRange = [fileContent rangeOfString:@"\n"];
if (firstLineRange.length > 0) {
firstLineRange.length = firstLineRange.location;
} else {
firstLineRange.length = fileContent.length;
}
firstLineRange.location = 0;
firstLine = [fileContent substringWithRange:firstLineRange];
}
if ([firstLine hasPrefix:@"#!"]) {
// 1) get script language name
// The last word of the line is the command. This covers all of the cases encountered:
// "#! /usr/bin/python", "#! /usr/local/bin/python" and "#! /usr/bin/myStrangePath/python" are all OK.
// We also accept "#! /usr/bin/env python" because it is used.
// Special case: scripts that begin with "#! /bin/sh" will be executed with dash
// And we want to accept "#! bc -l" too, so we can have multiple arguments.
// Take alphanumericCharacterSet and invert it.
firstLine = [firstLine substringFromIndex:2]; // remove "#!" at the beginning
firstLine = [firstLine stringByTrimmingCharactersInSet: [NSCharacterSet whitespaceCharacterSet]]; // remove any extra space
NSArray<NSString*> *firstLineComponents = [firstLine componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
NSMutableArray<NSString*> *components = [firstLineComponents mutableCopy];
if ([components.firstObject hasSuffix:@"env"]) {
// /usr/bin/env <command>
[components removeObjectAtIndex:0];
}
// Extract stript name by removing the path:
NSString* scriptNameString = components[0];
NSCharacterSet* separators = [NSCharacterSet characterSetWithCharactersInString:@"/"];
NSArray<NSString*> *scriptComponents = [scriptNameString componentsSeparatedByCharactersInSet:separators];
scriptNameString = scriptComponents.lastObject;
if ([scriptNameString isEqualToString:@"sh"])
scriptNameString = @"dash";
// If scriptNameString is a file that exists in PATH and has webAssembly signature, then insert "wasm script". Other cases?
components[0] = scriptNameString;
NSString* beforeCommand = beforeScriptCommandName(scriptNameString);
if (beforeCommand != NULL) {
[components removeObjectAtIndex:0];
NSArray<NSString*> *newComponents = [beforeCommand componentsSeparatedByCharactersInSet:[NSCharacterSet whitespaceCharacterSet]];
[components insertObjects:newComponents atIndexes:[NSIndexSet indexSetWithIndexesInRange:NSMakeRange(0, newComponents.count)]];
}
unsigned long numComponents = components.count;
if (numComponents > 0) {
// 2) insert all arguments at beginning of argument list:
argc += numComponents;
argv = (char **)realloc(argv, sizeof(char*) * (argc + 1));
// Move everything numComponents step up
for (int i = argc; i >= numComponents; i--) { argv[i] = argv[i-numComponents]; }
// Change the location of the file (from "command" to "/actual/full/path/command"):
// This pointer existed before
argv[numComponents] = realloc(argv[numComponents], locationName.length + 1);
strcpy(argv[numComponents], locationName.UTF8String);
// Copy all arguments without change (except the first):
for (int i = 0; i < numComponents; i++) {
argv[i] = strdup(components[i].UTF8String); // creates new pointers
}
break;
}
} else {
// Detect WebAssembly file signature: '\0asm' (begins with 0, so not a string)
if ((data.length >0) && (((char*)data.bytes)[0] == 0)) {
// fileContent = [[NSString alloc]initWithData:data encoding:NSASCIIStringEncoding];
NSRange signatureRange = NSMakeRange(1, 3);
firstLine = [fileContent substringWithRange:signatureRange];
}
if ([firstLine isEqualToString:@"asm"]) {
// WebAssembly file, identified by signature:
// Same code as above, but single command:
argc += 1;
argv = (char **)realloc(argv, sizeof(char*) * (argc + 1));
// Move everything numComponents step up
for (int i = argc; i >= 1; i--) { argv[i] = argv[i-1]; }
// Change the location of the file (from "command" to "/actual/full/path/command"):
// This pointer existed before
argv[1] = realloc(argv[1], locationName.length + 1);
strcpy(argv[1], locationName.UTF8String);
// Copy all arguments without change (except the first):
argv[0] = strdup("wasm"); // creates new pointer
break;
}
}
} else {
cmdIsReal = false;
}
}
if (cmdIsAFile) break; // else keep going through the path elements.
}
}
if (!cmdIsReal || (cmdIsAPath && !cmdIsAFile)) {
// argv[0] is a file that doesn't exist, or has size 0. Probably one of our commands.
// Replace with its name:
char* newName = basename(argv[0]);
argv[0] = realloc(argv[0], strlen(newName) + 1);
strcpy(argv[0], newName);
}
}
// NSLog(@"After command parsing, stdout %d stderr %d \n", fileno(params->stdout), fileno(params->stderr));
// fprintf(thread_stderr, "Command after parsing: ");
// for (int i = 0; i < argc; i++)
// fprintf(thread_stderr, "[%s] ", argv[i]);
// We've reached this point: either the command is a file, from a script we support,
// and we have inserted the name of the script at the beginning, or it is a builtin command
int (*function)(int ac, char** av) = NULL;
if (commandList == nil) initializeCommandList();
NSString* commandName = [NSString stringWithCString:argv[0] encoding:NSUTF8StringEncoding];
if ([commandName isEqualToString:@"sh"]) {
// if it's sh -c commands (or sh -c command1 || command2), we continue using our own sh_main
// (for continuity: keep our known bugs, rather than break things).
// otherwise we use dash_main:
bool cflag = false;
// Technically, "-c" should be the first argument of sh, so no need to scan through all arguments
// But we're being extra careful.
for (int i = 1; i < argc; i++) {
if (argv[i][0] == '-') {
if (strlen(argv[i]) == 1) continue;
if (strchr(argv[i], 'c') != NULL) {
cflag = true;
break;
}
} else break;
}
if (!cflag) { // We need to know it's dash, not sh.
commandName = @"dash";
argv[0] = realloc(argv[0], 5);
strcpy(argv[0], "dash");
}
}
//
NSArray* commandStructure = [commandList objectForKey: commandName];
void* handle = NULL;
if (commandStructure != nil) {
NSString* libraryName = commandStructure[0];
NSString* functionName = commandStructure[1];
// Python, Perl and TeX can have multiple commands calling themselves:
// hasPrefix covers python, python3, python3.9.
if ([commandName hasPrefix: @"python"]) {
// Ability to start multiple python3 scripts (required for Jupyter notebooks):
// start by increasing the number of the interpreter, until we're out.
int numInterpreter = 0;
if ((currentPythonInterpreter < numPythonInterpreters) && (!PythonIsRunning[currentPythonInterpreter])) {
numInterpreter = currentPythonInterpreter;
currentPythonInterpreter++;
} else {
NSDate *start = [NSDate date];
NSDate *now = [NSDate date];
NSTimeInterval timeInterval = [now timeIntervalSinceDate:start];
while (timeInterval < 1) { // keep trying for 1 second
while (numInterpreter < numPythonInterpreters) {
if (PythonIsRunning[numInterpreter] == false) break;
numInterpreter++;
}
if (numInterpreter < numPythonInterpreters) break;
numInterpreter = 0;
now = [NSDate date];
timeInterval = [now timeIntervalSinceDate:start];
}
if (numInterpreter >= numPythonInterpreters) {
if (showPythonInterpreterAlert) {
// Only show this alert once per session:
display_alert(@"Too many Python scripts", @"There are too many Python interpreters running at the same time. Try closing some of them.");
NSLog(@"%@", @"Too many python scripts running simultaneously. Try closing some notebooks.\n");
showPythonInterpreterAlert = false;
currentSession->global_errno = ENOENT;
}
function = &too_many_scripts;
functionName = @"notAValidCommand";
argv[0][0] = 'x'; // prevent reinitialization in cleanup_function
} else {
currentPythonInterpreter = numInterpreter;
}
}
if ((numInterpreter == 0) && (strlen(argv[0]) > 7)) {
// python3.x creates issues, so we truncate to 'python3'
argv[0][7] = 0;
}
if ((numInterpreter >= 0) && (numInterpreter < numPythonInterpreters)) {
params->numInterpreter = numInterpreter;
PythonIsRunning[numInterpreter] = true;
if (numInterpreter > 0) {
if ([commandName isEqualToString: @"python"]) {
// Add space for an extra letter at the end of "python" (+1 for "A", +1 for '\0')
argv[0] = realloc(argv[0], strlen(argv[0]) + 2);
}
char suffix[2];
suffix[0] = 'A' + (numInterpreter - 1);
suffix[1] = 0;
argv[0][6] = suffix[0];
argv[0][7] = 0;
commandName = [@"python" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
NSLog(@"Python new commandName: %s", commandName.UTF8String);
libraryName = [[commandName stringByAppendingString: @".framework/"] stringByAppendingString: commandName];
NSLog(@"Python new libraryName: %s", libraryName.UTF8String);
}
}
} else if ([commandName hasPrefix: @"perl"]) {
// Ability to start multiple perl scripts (required for cpan):
// start by increasing the number of the interpreter, until we're out.
int numInterpreter = 0;
if (currentPerlInterpreter < numPerlInterpreters) {
numInterpreter = currentPerlInterpreter;
currentPerlInterpreter++;
} else {
while (numInterpreter < numPerlInterpreters) {
if (PerlIsRunning[numInterpreter] == false) break;
numInterpreter++;
}
if (numInterpreter >= numPerlInterpreters) {
display_alert(@"Too many Perl scripts", @"There are too many Perl interpreters running at the same time. Try closing some of them.");
NSLog(@"%@", @"Too many perl scripts running simultaneously.\n");
function = &too_many_scripts;
functionName = @"notAValidCommand";
currentSession->global_errno = ENOENT;
argv[0][0] = 'x'; // prevent reinitialization in cleanup_function
}
}
if ((numInterpreter >= 0) && (numInterpreter < numPerlInterpreters)) {
params->numInterpreter = numInterpreter;
PerlIsRunning[numInterpreter] = true;
NSLog(@"Starting a Perl interpreter: %d", params->numInterpreter);
if (numInterpreter > 0) {
char suffix[2];
suffix[0] = 'A' + (numInterpreter - 1);
suffix[1] = 0;
commandName = [@"perl" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
libraryName = [libraryName stringByReplacingOccurrencesOfString:@"perl" withString:commandName];
}
}
} else if ([TeXcommands containsObject: commandName]) {
// It's a TeX command. Ability to start multiple TeX commands (required for Tikz)
// start by increasing the number of the interpreter, until we're out.
int numInterpreter = 0;
if (currentTeXInterpreter < numTeXInterpreters) {
numInterpreter = currentTeXInterpreter;
currentTeXInterpreter++;
} else {
while (numInterpreter < numTeXInterpreters) {
if (TeXIsRunning[numInterpreter] == false) break;
numInterpreter++;
}
if (numInterpreter >= numTeXInterpreters) {
display_alert(@"Too many TeX scripts", @"There are too many TeX interpreters running at the same time. Try closing some of them.");
NSLog(@"%@", @"Too many TeX scripts running simultaneously.\n");
function = &too_many_scripts;
functionName = @"notAValidCommand";
currentSession->global_errno = ENOENT;
argv[0][0] = 'x'; // prevent reinitialization in cleanup_function
}
}
if ((numInterpreter >= 0) && (numInterpreter < numTeXInterpreters)) {
params->numInterpreter = numInterpreter;
TeXIsRunning[numInterpreter] = true;
if (numInterpreter > 0) {
// There are multiple TeX commands, and they can start each other.
char suffix[2];
suffix[0] = 'A' + (numInterpreter - 1);
suffix[1] = 0;
// libraryName can be pdftex, luatex or luahbtex:
if ([libraryName hasPrefix: @"pdftex"]) {
NSString* newName = [@"pdftex" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
libraryName = [libraryName stringByReplacingOccurrencesOfString:@"pdftex" withString:newName];
} else if ([libraryName hasPrefix: @"luatex"]) {
NSString* newName = [@"luatex" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
libraryName = [libraryName stringByReplacingOccurrencesOfString:@"luatex" withString:newName];
} else if ([libraryName hasPrefix: @"luahbtex"]) {
NSString* newName = [@"luahbtex" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
libraryName = [libraryName stringByReplacingOccurrencesOfString:@"luahbtex" withString:newName];
}
}
}
} else if ([commandName hasPrefix: @"dash"]) {
// Ability to start multiple dash commands:
// start by increasing the number of the interpreter, until we're out.
int numInterpreter = 0;
if (currentDashCommand < numDashCommands) {
numInterpreter = currentDashCommand;
currentDashCommand++;
} else {
NSDate *start = [NSDate date];
NSDate *now = [NSDate date];
NSTimeInterval timeInterval = [now timeIntervalSinceDate:start];
while (timeInterval < 1) { // keep trying for 1 second
while (numInterpreter < numDashCommands) {
if (dashIsRunning[numInterpreter] == false) break;
numInterpreter++;
}
if (numInterpreter < numDashCommands) break;
numInterpreter = 0;
now = [NSDate date];
timeInterval = [now timeIntervalSinceDate:start];
}
if (numInterpreter >= numDashCommands) {
display_alert(@"Too many dash scripts", @"There are too many dash scripts running at the same time. Try closing some of them.");
NSLog(@"%@", @"Too many dash scripts running simultaneously.\n");
functionName = @"notAValidCommand";
currentSession->global_errno = ENOENT;
argv[0][0] = 'x'; // prevent reinitialization in cleanup_function
}
}
if ((numInterpreter >= 0) && (numInterpreter < numDashCommands)) {
params->numInterpreter = numInterpreter;
dashIsRunning[numInterpreter] = true;
NSLog(@"Starting a dash shell: %d", params->numInterpreter);
if (numInterpreter > 0) {
char suffix[2];
suffix[0] = 'A' + (numInterpreter - 1);
suffix[1] = 0;
commandName = [@"dash" stringByAppendingString: [NSString stringWithCString: suffix encoding:NSUTF8StringEncoding]];
libraryName = [libraryName stringByReplacingOccurrencesOfString:@"dash" withString:commandName];
}
}
}
if ([libraryName isEqualToString: @"SELF"]) handle = RTLD_SELF; // commands defined in ios_system.framework
else if ([libraryName isEqualToString: @"MAIN"]) handle = RTLD_MAIN_ONLY; // commands defined in main program
else handle = dlopen(libraryName.UTF8String, RTLD_LAZY | RTLD_GLOBAL); // commands defined in dynamic library
if (handle == NULL) {
char* errorLoading = strdup(dlerror());
fprintf(thread_stderr, "Failed loading %s from %s, cause = %s\n", commandName.UTF8String, libraryName.UTF8String, errorLoading);
NSLog(@"Failed loading %s from %s, cause = %s\n", commandName.UTF8String, libraryName.UTF8String, errorLoading);
NSString* fileLocation = [[NSBundle mainBundle] pathForResource:libraryName ofType:nil];
free(errorLoading);
} else {
function = dlsym(handle, functionName.UTF8String);
NSLog(@"Loading %s from %s", functionName.UTF8String, libraryName.UTF8String);
if (function == NULL) {
char* errorLoading = strdup(dlerror());
fprintf(thread_stderr, "Failed loading %s from %s, cause = %s\n", functionName.UTF8String, libraryName.UTF8String, errorLoading);
NSLog(@"Failed loading %s from %s, cause = %s\n", commandName.UTF8String, libraryName.UTF8String, errorLoading);
free(errorLoading);
}
}
}
if (function == NULL) {
function = &command_not_found;
currentSession->global_errno = ENOENT;
// function = dlsym(RTLD_SELF, "command_not_found");
}
if (function) {
// We run the function in a thread because there are several
// points where we can exit from a shell function.
// Commands call pthread_exit instead of exit
// thread is attached, could also be un-attached
params->argc = argc;
params->argv = argv;
params->function = function;
params->dlHandle = handle;
params->isPipeIn = (params->stdin != thread_stdin);
params->isPipeOut = (params->stdout != thread_stdout);
if (params->stdout != NULL)
NSLog(@"params->stdout: %d thread_stdout: %d \n", fileno(params->stdout), fileno(thread_stdout));
if (params->stdin != NULL)
NSLog(@"params->stdin: %d thread_stdin: %d \n", fileno(params->stdin), fileno(thread_stdin));
params->isPipeErr = (params->stderr != thread_stderr) && (params->stderr != params->stdout);
params->storeRootThread = false;
// params->session = currentSession;
// Before starting, do we have enough file descriptors available?
int numFileDescriptorsOpen = 0;
for (int fd = 0; fd < limitFilesOpen.rlim_cur; fd++) {
errno = 0;
int flags = fcntl(fd, F_GETFD, 0);
if (flags == -1 && errno) {
continue;
}
++numFileDescriptorsOpen ;
}
// fprintf(stderr, "Num file descriptor = %d\n", numFileDescriptorsOpen);
// We assume 128 file descriptors will be enough for a single command.
if (numFileDescriptorsOpen + 128 > limitFilesOpen.rlim_cur) {
limitFilesOpen.rlim_cur += 1024;
int res = setrlimit(RLIMIT_NOFILE, &limitFilesOpen);
// Check the result:
getrlimit(RLIMIT_NOFILE, &limitFilesOpen);
if (res == 0) NSLog(@"[Info] Increased file descriptor limit to = %llu\n", limitFilesOpen.rlim_cur);
else NSLog(@"[Warning] Failed to increased file descriptor limit to = %llu\n", limitFilesOpen.rlim_cur);
}
NSLog(@"Starting command: %s, currentSession->isMainThread: %d", commandName.UTF8String, currentSession->isMainThread);
if ([commandName isEqualToString:@"wasm"])
startedPreparingWebAssemblyCommand();
if (currentSession->isMainThread) {
params->storeRootThread = true;
// I'm still not sure why this is needed specially for dash and no other commands:
// Needed for pipes
if ([commandName isEqualToString: @"dash"]) {
params->storeRootThread = false;
}
bool commandOperatesOnFiles = ([commandStructure[3] isEqualToString:@"file"] ||
[commandStructure[3] isEqualToString:@"directory"] ||
params->isPipeOut || params->isPipeErr);
NSString* currentPath = [fileManager currentDirectoryPath];
commandOperatesOnFiles &= (currentPath != nil);
if (commandOperatesOnFiles) {
// Send a signal to the system that we're going to change the current directory:
NSURL* currentURL = [NSURL fileURLWithPath:currentPath];
NSFileCoordinator *fileCoordinator = [[NSFileCoordinator alloc] initWithFilePresenter:nil];
[fileCoordinator coordinateWritingItemAtURL:currentURL options:0 error:NULL byAccessor:^(NSURL *currentURL) {
currentSession->isMainThread = false;
volatile pthread_t _tid = NULL;
pthread_create(&_tid, NULL, run_function, params);
while (_tid == NULL) { }
// ios_storeThreadId(_tid);
if (currentSession->mainThreadId == NULL) currentSession->mainThreadId = _tid;
// Wait for this process to finish:
if (joinMainThread) {
pthread_join(_tid, NULL);
// If there are auxiliary process, also wait for them:
if (currentSession->lastThreadId > 0) pthread_join(currentSession->lastThreadId, NULL);
currentSession->lastThreadId = 0;
currentSession->current_command_root_thread = 0;
} else {
pthread_detach(_tid); // a thread must be either joined or detached
}
currentSession->isMainThread = true;
}];
} else {
currentSession->isMainThread = false;
volatile pthread_t _tid = NULL;
pthread_create(&_tid, NULL, run_function, params);
while (_tid == NULL) { }
// ios_storeThreadId(_tid);
if (currentSession->mainThreadId == NULL) currentSession->mainThreadId = _tid;
// Wait for this process to finish:
if (joinMainThread) {
pthread_join(_tid, NULL);
// If there are auxiliary process, also wait for them:
if (currentSession->lastThreadId > 0) pthread_join(currentSession->lastThreadId, NULL);
currentSession->lastThreadId = 0;
currentSession->current_command_root_thread = 0;
} else {
pthread_detach(_tid); // a thread must be either joined or detached
}
currentSession->isMainThread = true;
}
} else {
NSLog(@"Starting command %s, global_errno= %d\n", command, currentSession->global_errno);
// Don't send signal if not in main thread. Also, don't join threads.
volatile pthread_t _tid_local = NULL;
pthread_create(&_tid_local, NULL, run_function, params);
// The last command on the command line (with multiple pipes) will be created first
while (_tid_local == NULL) { }; // Wait until thread has actually started
// fprintf(stderr, "Started thread = %x\n", _tid_local);
if (currentSession->lastThreadId == 0) currentSession->lastThreadId = _tid_local; // will be joined later
else pthread_detach(_tid_local); // a thread must be either joined or detached.
}
} else {
fprintf(params->stderr, "%s: command not found\n", argv[0]);
NSLog(@"%s: command not found\n", argv[0]);
free(argv);
// If command output was redirected to a pipe, we still need to close it.
// (to warn the other command that it can stop waiting)
// We still need this step because there can be multiple pipes.
if (params->stdout != currentSession->stdout) {
fclose(params->stdout);
}
if ((params->stderr != currentSession->stderr) && (params->stderr != params->stdout)) {
fclose(params->stderr);
}
if ((handle != NULL) && (handle != RTLD_SELF)
&& (handle != RTLD_MAIN_ONLY)
&& (handle != RTLD_DEFAULT) && (handle != RTLD_NEXT))
dlclose(handle);
free(params); // This was malloc'ed in ios_system
ios_storeThreadId(0);
currentSession->global_errno = 127;
// TODO: this should also raise an exception, for python scripts
} // if (function)
} else { // argc != 0
ios_storeThreadId(0);
free(argv); // argv is otherwise freed in cleanup_function
free(dontExpand);
free(params);
}
NSLog(@"returning from ios_system, global_errno= %d\n", currentSession->global_errno);
free(originalCommand); // releases cmd, which was a strdup of inputCommand
fflush(thread_stdin);
fflush(thread_stdout);
fflush(thread_stderr);
return currentSession->global_errno;
}
NSArray<NSString *> * pathNormalizeArray(NSArray<NSString *> * parts, BOOL allowAboveRoot) {
NSMutableArray<NSString *> * res = [[NSMutableArray alloc] init];
for (NSString * p in parts) {
// ignore empty parts
if (p.length == 0 || [p isEqualToString:@"."] || [p isEqualToString:@"/"]) {
continue;
}
if ([p isEqualToString: @".."]) {
if (res.count && ![@".." isEqualToString: [res lastObject]]) {
[res removeLastObject];
} else if (allowAboveRoot) {
[res addObject: p];
}
} else {
[res addObject: p];
}
}
return res;
}
NSString * pathNormalize(NSString *path) {
BOOL isAbsolute = [path hasPrefix:@"/"];
BOOL trailingSlash = [path hasSuffix:@"/"];
NSString * result = [pathNormalizeArray([path pathComponents], !isAbsolute) componentsJoinedByString: @"/"];
if (!result.length && !isAbsolute) {
result = @".";
}
if (result.length && trailingSlash) {
result = [result stringByAppendingString:@"/"];
}
return [(isAbsolute ? @"/" : @"") stringByAppendingString:result];
}
NSString * pathJoin(NSString * segmentA, NSString * segmentB) {
NSMutableString *path = [[NSMutableString alloc] init];
NSString * a = segmentA ?: @"";
NSString * b = segmentB ?: @"";
if ([b hasPrefix:@"/"]) {
return pathNormalize(b);
}
if (a.length) {
[path appendString: a];
if (b.length) {
[path appendString:@"/"];
[path appendString:b];
}
} else if (b.length) {
[path appendString: b];
}
return pathNormalize(path);
}
//
char* ios_getPythonLibraryName(void) {
// Ability to start multiple python3 scripts, expanded for commands that start python3 as a dynamic library.
// (mostly vim, right now)
// start by increasing the number of the interpreter, until we're out.
int numInterpreter = 0;
if ((currentPythonInterpreter < numPythonInterpreters) && (!PythonIsRunning[currentPythonInterpreter])) {
numInterpreter = currentPythonInterpreter;
currentPythonInterpreter++;
} else {
while (numInterpreter < numPythonInterpreters) {
if (PythonIsRunning[numInterpreter] == false) break;
numInterpreter++;
}
if (numInterpreter >= numPythonInterpreters) {
// NSLog(@"ios_getPythonLibraryName: returning NULL\n");
return NULL;
} else {
currentPythonInterpreter = numInterpreter;
}
}
char* libraryName = NULL;
if ((numInterpreter >= 0) && (numInterpreter < numPythonInterpreters)) {
PythonIsRunning[numInterpreter] = true;
if (numInterpreter > 0) {
libraryName = strdup("pythonA");
libraryName[6] = 'A' + (numInterpreter - 1);
} else {
libraryName = strdup("python3_ios");
}
NSLog(@"ios_getPythonLibraryName: returning %s\n", libraryName);
return libraryName;
}
NSLog(@"ios_getPythonLibraryName: returning NULL\n");
return NULL;
}
void ios_releasePythonLibraryName(char* name) {
NSLog(@"ios_releasePythonLibraryName: releasing %s\n", name);
char libNumber = name[6];
if (libNumber == '3') PythonIsRunning[0] = false;
else {
libNumber -= 'A' - 1;
if ((libNumber > 0) && (libNumber < MaxPythonInterpreters))
PythonIsRunning[libNumber] = false;
}
free(name);
}