From b97462993b00ecc5cd04051b4fc2abd164fefb04 Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Sun, 28 Feb 2021 18:07:04 +0200 Subject: Single app instance; IPC mechanism Only one instance of Lagrange is allowed to run per a runtime directory. Otherwise the instances will overwrite each others' files. Added a check at startup to see if an instance is already running. If so, options can be used to communicate with it. By default, a new tab is opened in the running instance. IssueID #37 IssueID #164 IssueID #174 IssueID #178 --- CMakeLists.txt | 2 + src/app.c | 166 +++++++++++++++++++++++++++++++++++++++++++------- src/ipc.c | 186 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++ src/ipc.h | 17 +++++- 4 files changed, 349 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 6067f9cd..3ee6ea00 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -100,6 +100,8 @@ set (SOURCES src/gopher.h src/history.c src/history.h + src/ipc.c + src/ipc.h src/lookup.c src/lookup.h src/media.c diff --git a/src/app.c b/src/app.c index b76c4aaa..8a02ec3f 100644 --- a/src/app.c +++ b/src/app.c @@ -30,6 +30,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "gmdocument.h" #include "gmutil.h" #include "history.h" +#include "ipc.h" #include "ui/certimportwidget.h" #include "ui/color.h" #include "ui/command.h" @@ -50,12 +51,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include #include #include -#include -#include -#include -#include -#include -#include +#include #include #include @@ -422,8 +418,116 @@ static uint32_t postAutoReloadCommand_App_(uint32_t interval, void *param) { return interval; } +static void terminate_App_(int rc) { + SDL_Quit(); + deinit_Foundation(); + exit(rc); +} + +static void communicateWithRunningInstance_App_(iApp *d, iProcessId instance, + const iStringList *openCmds) { + iString *cmds = new_String(); + const iProcessId pid = currentId_Process(); + iConstForEach(CommandLine, i, &d->args) { + if (i.argType == value_CommandLineArgType) { + continue; + } + if (equal_CommandLineConstIterator(&i, "go-home")) { + appendCStr_String(cmds, "navigate.home\n"); + } + else if (equal_CommandLineConstIterator(&i, "new-tab")) { + iCommandLineArg *arg = argument_CommandLineConstIterator(&i); + if (!isEmpty_StringList(&arg->values)) { + appendFormat_String(cmds, "open newtab:1 url:%s\n", + cstr_String(constAt_StringList(&arg->values, 0))); + } + else { + appendCStr_String(cmds, "tabs.new\n"); + } + iRelease(arg); + } + else if (equal_CommandLineConstIterator(&i, "close-tab")) { + appendCStr_String(cmds, "tabs.close\n"); + } + else if (equal_CommandLineConstIterator(&i, "list-tab-urls;L")) { + appendFormat_String(cmds, "ipc.list.urls pid:%d\n", pid); + } + } + if (!isEmpty_StringList(openCmds)) { + append_String(cmds, collect_String(joinCStr_StringList(openCmds, "\n"))); + } + if (isEmpty_String(cmds)) { + /* By default open a new tab. */ + appendCStr_String(cmds, "tabs.new\n"); + } + if (!isEmpty_String(cmds)) { + iString *result = communicate_Ipc(cmds); + if (result) { + puts(cstr_String(result)); + } + delete_String(result); + } + iUnused(instance); +// else { +// printf("Lagrange already running (PID %d)\n", instance); +// } + terminate_App_(0); +} + static void init_App_(iApp *d, int argc, char **argv) { init_CommandLine(&d->args, argc, argv); + /* Configure the valid command line options. */ { + defineValues_CommandLine(&d->args, "close-tab", 0); + defineValues_CommandLine(&d->args, "echo;E", 0); + defineValues_CommandLine(&d->args, "go-home", 0); + defineValues_CommandLine(&d->args, "help", 0); + defineValues_CommandLine(&d->args, "list-tab-urls;L", 0); + defineValuesN_CommandLine(&d->args, "new-tab", 0, 1); + defineValues_CommandLine(&d->args, "sw", 0); + defineValues_CommandLine(&d->args, "version;V", 0); + } + iStringList *openCmds = new_StringList(); + /* Handle command line options. */ { + if (contains_CommandLine(&d->args, "help")) { + printf("Usage: lagrange [options] [URLs] [paths]\n"); + terminate_App_(0); + } + if (contains_CommandLine(&d->args, "version;V")) { + printf("Lagrange version " LAGRANGE_APP_VERSION "\n"); + terminate_App_(0); + } + /* Check for URLs. */ + iBool newTab = iFalse; + iConstForEach(CommandLine, i, &d->args) { + const iRangecc arg = i.entry; + if (i.argType == value_CommandLineArgType) { + /* URLs and file paths accepted. */ + const iBool isKnownScheme = + startsWithCase_Rangecc(arg, "gemini:") || startsWithCase_Rangecc(arg, "gopher:") || + startsWithCase_Rangecc(arg, "finger:") || startsWithCase_Rangecc(arg, "file:") || + startsWithCase_Rangecc(arg, "data:") || startsWithCase_Rangecc(arg, "about:"); + if (isKnownScheme || fileExistsCStr_FileInfo(cstr_Rangecc(arg))) { + pushBack_StringList( + openCmds, + collectNewFormat_String("open newtab:%d url:%s", + newTab, + isKnownScheme + ? cstr_Rangecc(arg) + : cstrCollect_String(makeFileUrl_String( + collectNewRange_String(arg))))); + newTab = iTrue; + } + else { + fprintf(stderr, "Invalid URL/file: %s\n", cstr_Rangecc(arg)); + terminate_App_(1); + } + } + else if (!isDefined_CommandLine(&d->args, collectNewRange_String(i.entry))) { + fprintf(stderr, "Unknown option: %s\n", cstr_Rangecc(arg)); + terminate_App_(1); + } + } + } /* Where was the app started from? We ask SDL first because the command line alone is not a reliable source of this information, particularly when it comes to different operating systems. */ { @@ -437,6 +541,17 @@ static void init_App_(iApp *d, int argc, char **argv) { } SDL_free(exec); } + init_Ipc(dataDir_App_()); + /* Only one instance is allowed to run at a time; the runtime files (bookmarks, etc.) + are not shareable. */ { + const iProcessId instance = check_Ipc(); + if (instance) { + communicateWithRunningInstance_App_(d, instance, openCmds); + terminate_App_(0); + } + listen_Ipc(); /* We'll respond to commands from other instances. */ + } + printf("Lagrange: A Beautiful Gemini Client\n"); const iBool isFirstRun = !fileExistsCStr_FileInfo(cleanedPath_CStr(concatPath_CStr(dataDir_App_(), "prefs.cfg"))); d->isFinishedLaunching = iFalse; @@ -445,7 +560,7 @@ static void init_App_(iApp *d, int argc, char **argv) { init_SortedArray(&d->tickers, sizeof(iTicker), cmp_Ticker_); d->lastTickerTime = SDL_GetTicks(); d->elapsedSinceLastTicker = 0; - d->commandEcho = checkArgument_CommandLine(&d->args, "echo") != NULL; + d->commandEcho = checkArgument_CommandLine(&d->args, "echo;E") != NULL; d->forceSoftwareRender = checkArgument_CommandLine(&d->args, "sw") != NULL; d->initialWindowRect = init_Rect(-1, -1, 900, 560); #if defined (iPlatformMsys) @@ -529,21 +644,10 @@ static void init_App_(iApp *d, int argc, char **argv) { } } /* URLs from the command line. */ { - iBool newTab = iFalse; - for (size_t i = 1; i < size_StringList(args_CommandLine(&d->args)); i++) { - const iString *arg = constAt_StringList(args_CommandLine(&d->args), i); - const iBool isKnownScheme = - startsWithCase_String(arg, "gemini:") || startsWithCase_String(arg, "gopher:") || - startsWithCase_String(arg, "file:") || startsWithCase_String(arg, "data:") || - startsWithCase_String(arg, "about:"); - if (isKnownScheme || fileExists_FileInfo(arg)) { - postCommandf_App("open newtab:%d url:%s", - newTab, - isKnownScheme ? cstr_String(arg) - : cstrCollect_String(makeFileUrl_String(arg))); - newTab = iTrue; - } + iConstForEach(StringList, i, openCmds) { + postCommandString_App(i.value); } + iRelease(openCmds); } } @@ -567,6 +671,7 @@ static void deinit_App(iApp *d) { deinit_CommandLine(&d->args); iRelease(d->launchCommands); delete_String(d->execPath); + deinit_Ipc(); iRecycle(); } @@ -954,6 +1059,9 @@ void postRefresh_App(void) { void postCommand_App(const char *command) { iApp *d = &app_; iAssert(command); + if (strlen(command) == 0) { + return; + } SDL_Event ev; if (*command == '!') { /* Global command; this is global context so just ignore. */ @@ -1784,6 +1892,22 @@ iBool handleCommand_App(const char *cmd) { } return iFalse; } + else if (equal_Command(cmd, "ipc.list.urls")) { + iProcessId pid = argLabel_Command(cmd, "pid"); + if (pid) { + iString *urls = collectNew_String(); + iConstForEach(ObjectList, i, iClob(listDocuments_App())) { + append_String(urls, url_DocumentWidget(i.object)); + appendCStr_String(urls, "\n"); + } + write_Ipc(pid, urls, response_IpcWrite); + } + return iTrue; + } + else if (equal_Command(cmd, "ipc.signal")) { + signal_Ipc(arg_Command(cmd)); + return iTrue; + } else { return iFalse; } diff --git a/src/ipc.c b/src/ipc.c index 81d4e35b..8fdc3bd5 100644 --- a/src/ipc.c +++ b/src/ipc.c @@ -21,4 +21,190 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ipc.h" +#include "app.h" +#include +#include +#include +#include +#include +#include + +#include + +iDeclareType(Ipc) + +struct Impl_Ipc { + iString dir; + iBool isListening; +}; + +static iIpc ipc_; + +static const char *lockFilePath_(const iIpc *d) { + return concatPath_CStr(cstr_String(&d->dir), ".pid"); +} + +static const char *inputFilePath_(const iIpc *d, int pid) { + return concatPath_CStr(cstr_String(&d->dir), + format_CStr(".run.%d.cfg", pid ? pid : currentId_Process())); +} + +void init_Ipc(const char *runDir) { + iIpc *d = &ipc_; + initCStr_String(&d->dir, runDir); + d->isListening = iFalse; + signal(SIGUSR1, SIG_IGN); +} + +void deinit_Ipc(void) { + iIpc *d = &ipc_; + signal(SIGUSR1, SIG_IGN); + if (d->isListening) { + remove(lockFilePath_(d)); + } + deinit_String(&d->dir); +} + +static void handleUserSignal_(int sig) { + iIpc *d = &ipc_; + iAssert(sig == SIGUSR1); + iUnused(sig); + const char *path = inputFilePath_(d, 0); + iFile *f = newCStr_File(path); + if (open_File(f, readOnly_FileMode)) { + iString *cmds = new_String(); + initBlock_String(cmds, collect_Block(readAll_File(f))); + iRangecc line = iNullRange; + while (nextSplit_Rangecc(range_String(cmds), "\n", &line)) { + postCommand_App(cstr_Rangecc(line)); + } + delete_String(cmds); + } + iRelease(f); + remove(path); +} + +void listen_Ipc(void) { + iIpc *d = &ipc_; + signal(SIGUSR1, handleUserSignal_); + iFile *f = newCStr_File(lockFilePath_(d)); + if (open_File(f, writeOnly_FileMode)) { + printf_Stream(stream_File(f), "%u", currentId_Process()); + d->isListening = iTrue; + } + iRelease(f); +} + +iProcessId check_Ipc(void) { + const iIpc *d = &ipc_; + iProcessId pid = 0; + iFile *f = newCStr_File(lockFilePath_(d)); + if (open_File(f, readOnly_FileMode)) { + const iBlock *running = collect_Block(readAll_File(f)); + close_File(f); + pid = atoi(constData_Block(running)); + if (!exists_Process(pid)) { + pid = 0; + remove(cstr_String(path_File(f))); /* Stale. */ + } + } + iRelease(f); + return pid; +} + +/*----------------------------------------------------------------------------------------------*/ + +iDeclareType(IpcResponse) + +struct Impl_IpcResponse { + iString *output; + iBool success; + iMutex mtx; + iCondition finished; +}; + +static void init_IpcResponse(iIpcResponse *d) { + d->output = new_String(); + d->success = iFalse; + init_Mutex(&d->mtx); + init_Condition(&d->finished); +} + +static void deinit_IpcResponse(iIpcResponse *d) { + deinit_Condition(&d->finished); + deinit_Mutex(&d->mtx); + delete_String(d->output); +} + +iDefineTypeConstruction(IpcResponse) + +static iIpcResponse *response_; + +static void handleSignal_IpcResponse_(int sig) { + iUnused(sig); + iAssert(response_); + iIpcResponse *d = response_; + lock_Mutex(&d->mtx); + iFile *f = newCStr_File(inputFilePath_(&ipc_, 0)); + if (open_File(f, text_FileMode | readOnly_FileMode)) { + iBlock *input = readAll_File(f); + close_File(f); + remove(cstr_String(path_File(f))); + setBlock_String(d->output, input); + d->success = iTrue; + delete_Block(input); + } + iRelease(f); + signal_Condition(&d->finished); + unlock_Mutex(&d->mtx); +} + +iBool write_Ipc(iProcessId pid, const iString *input, enum iIpcWrite type) { + iBool ok = iFalse; + iFile *f = newCStr_File(inputFilePath_(&ipc_, pid)); + if (open_File(f, text_FileMode | append_FileMode)) { + write_File(f, utf8_String(input)); + if (type == command_IpcWrite) { + printf_Stream(stream_File(f), "\nipc.signal arg:%d\n", currentId_Process()); + } + close_File(f); + ok = iTrue; + } + iRelease(f); + return ok; +} + +iString *communicate_Ipc(const iString *command) { + const iProcessId dst = check_Ipc(); + if (dst) { + if (write_Ipc(dst, command, command_IpcWrite)) { + response_ = new_IpcResponse(); + signal(SIGUSR1, handleSignal_IpcResponse_); + lock_Mutex(&response_->mtx); + if (kill(dst, SIGUSR1) == 0) { + iTime until; + initTimeout_Time(&until, 1.0); + waitTimeout_Condition(&response_->finished, &response_->mtx, &until); + } + unlock_Mutex(&response_->mtx); + if (!response_->success) { + delete_IpcResponse(response_); + response_ = NULL; + } + } + } + signal(SIGUSR1, SIG_IGN); + if (response_) { + iString *result = copy_String(response_->output); + trimEnd_String(result); + delete_IpcResponse(response_); + response_ = NULL; + return result; + } + return NULL; +} + +void signal_Ipc(iProcessId pid) { + kill(pid, SIGUSR1); +} diff --git a/src/ipc.h b/src/ipc.h index 22dce2a8..8127bf86 100644 --- a/src/ipc.h +++ b/src/ipc.h @@ -22,5 +22,20 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #pragma once -#include +#include +#include +void init_Ipc (const char *runDir); +void deinit_Ipc (void); + +iProcessId check_Ipc (void); +void listen_Ipc (void); +iString * communicate_Ipc (const iString *command); +void signal_Ipc (iProcessId pid); + +enum iIpcWrite { + command_IpcWrite, + response_IpcWrite, +}; + +iBool write_Ipc (iProcessId pid, const iString *input, enum iIpcWrite type); -- cgit v1.2.3