diff --git a/launchers/macosx/README.md b/launchers/macosx/README.md index 327d58654f..e663fcd50f 100644 --- a/launchers/macosx/README.md +++ b/launchers/macosx/README.md @@ -18,6 +18,7 @@ This is some Swift code which makes the application use events to signal the dif | router_pid | the pid number as string | Triggered when we know the pid of the router subprocess | | router_version | the version string | Triggered when we have successfully extracted current I2P version | | extract_errored | the error message | Triggered if the process didn't exit correctly | +| router_already_running | an error message | Triggered if any processes containing i2p.jar in name/arguments already exists upon router launch | ## Misc @@ -35,3 +36,12 @@ An example build command: `xcodebuild -target I2PLauncher -sdk /Applications/Xcode.app/Contents/Developer/Platforms/MacOSX.platform/Developer/SDKs/MacOSX.sdk` +## Objective-C / Swift Links + +* https://nshipster.com/at-compiler-directives/ +* https://developer.apple.com/documentation/swift/cocoa_design_patterns/handling_cocoa_errors_in_swift +* https://content.pivotal.io/blog/rails-to-ios-what-the-are-these-symbols-in-my-code +* https://mackuba.eu/2008/10/05/learn-objective-c-in-30-minutes/ +* https://en.wikipedia.org/wiki/Objective-C +* http://cocoadevcentral.com/d/learn_objectivec/ + diff --git a/launchers/macosx/RouterTask.h b/launchers/macosx/RouterTask.h index 7ef5118279..9f215d31ee 100644 --- a/launchers/macosx/RouterTask.h +++ b/launchers/macosx/RouterTask.h @@ -2,6 +2,10 @@ #include #include +#include +#include +#include +#include #include #import @@ -10,6 +14,8 @@ #include #include + + const std::vector defaultStartupFlags { @"-Xmx512M", @"-Xms128m", @@ -52,7 +58,162 @@ const std::vector defaultFlagsForExtractorJob { @end +@interface IIProcessInfo : NSObject { + +@private + int numberOfProcesses; + NSMutableArray *processList; +} +- (id) init; +- (int)numberOfProcesses; +- (void)obtainFreshProcessList; +- (BOOL)findProcessWithStringInNameOrArguments:(NSString *)procNameToSearch; +@end +#ifdef __cplusplus +// Inspired by the "ps" util. +inline std::string getArgvOfPid(int pid) { + int mib[3], argmax, nargs, c = 0; + size_t size; + char *procargs, *sp, *np, *cp; + int show_args = 1; + mib[0] = CTL_KERN; + mib[1] = KERN_ARGMAX; + + size = sizeof(argmax); + if (sysctl(mib, 2, &argmax, &size, NULL, 0) == -1) { + return std::string("sorry"); + } + + /* Allocate space for the arguments. */ + procargs = (char *)malloc(argmax); + if (procargs == NULL) { + return std::string("sorry"); + } + /* + * Make a sysctl() call to get the raw argument space of the process. + * The layout is documented in start.s, which is part of the Csu + * project. In summary, it looks like: + * + * /---------------\ 0x00000000 + * : : + * : : + * |---------------| + * | argc | + * |---------------| + * | arg[0] | + * |---------------| + * : : + * : : + * |---------------| + * | arg[argc - 1] | + * |---------------| + * | 0 | + * |---------------| + * | env[0] | + * |---------------| + * : : + * : : + * |---------------| + * | env[n] | + * |---------------| + * | 0 | + * |---------------| <-- Beginning of data returned by sysctl() is here. + * | argc | + * |---------------| + * | exec_path | + * |:::::::::::::::| + * | | + * | String area. | + * | | + * |---------------| <-- Top of stack. + * : : + * : : + * \---------------/ 0xffffffff + */ + mib[0] = CTL_KERN; + mib[1] = KERN_PROCARGS2; + mib[2] = pid; + + + size = (size_t)argmax; + if (sysctl(mib, 3, procargs, &size, NULL, 0) == -1) { + free(procargs); + return std::string("sorry"); + } + + memcpy(&nargs, procargs, sizeof(nargs)); + cp = procargs + sizeof(nargs); + + /* Skip the saved exec_path. */ + for (; cp < &procargs[size]; cp++) { + if (*cp == '\0') { + /* End of exec_path reached. */ + break; + } + } + if (cp == &procargs[size]) { + free(procargs); + return std::string("sorry"); + } + + /* Skip trailing '\0' characters. */ + for (; cp < &procargs[size]; cp++) { + if (*cp != '\0') { + /* Beginning of first argument reached. */ + break; + } + } + if (cp == &procargs[size]) { + free(procargs); + return std::string("sorry"); + } + /* Save where the argv[0] string starts. */ + sp = cp; + + /* + * Iterate through the '\0'-terminated strings and convert '\0' to ' ' + * until a string is found that has a '=' character in it (or there are + * no more strings in procargs). There is no way to deterministically + * know where the command arguments end and the environment strings + * start, which is why the '=' character is searched for as a heuristic. + */ + for (np = NULL; c < nargs && cp < &procargs[size]; cp++) { + if (*cp == '\0') { + c++; + if (np != NULL) { + /* Convert previous '\0'. */ + *np = ' '; + } else { + /* *argv0len = cp - sp; */ + } + /* Note location of current '\0'. */ + np = cp; + + if (!show_args) { + /* + * Don't convert '\0' characters to ' '. + * However, we needed to know that the + * command name was terminated, which we + * now know. + */ + break; + } + } + } + + /* + * sp points to the beginning of the arguments/environment string, and + * np should point to the '\0' terminator for the string. + */ + if (np == NULL || np == sp) { + /* Empty or unterminated string. */ + free(procargs); + return std::string("sorry"); + } + return std::string(sp); +} +#endif diff --git a/launchers/macosx/RouterTask.mm b/launchers/macosx/RouterTask.mm index a845f1e1dd..585998bad1 100644 --- a/launchers/macosx/RouterTask.mm +++ b/launchers/macosx/RouterTask.mm @@ -8,6 +8,11 @@ #include "include/subprocess.hpp" #import "I2PLauncher-Swift.h" #include "AppDelegate.h" + +#include +#include +#include +#include #endif #include "include/PidWatcher.h" @@ -20,7 +25,6 @@ @implementation I2PRouterTask - - (void)routerStdoutData:(NSNotification *)notification { NSLog(@"%@", [[NSString alloc] initWithData:[notification.object availableData] encoding:NSUTF8StringEncoding]); @@ -31,7 +35,6 @@ { self.userRequestedRestart = NO; self.isRouterRunning = NO; - //self.input = [NSFileHandle fileHandleWithStandardInput]; self.routerTask = [NSTask new]; self.processPipe = [NSPipe new]; [self.routerTask setLaunchPath:options.binPath]; @@ -47,14 +50,11 @@ [self.routerTask setTerminationHandler:^(NSTask* task) { // Cleanup NSLog(@"termHandler triggered!"); - auto swiftRouterStatus = [[RouterProcessStatus alloc] init]; - [swiftRouterStatus setRouterStatus: false]; - [swiftRouterStatus setRouterRanByUs: false]; - [swiftRouterStatus triggerEventWithEn:@"router_stop" details:@"normal shutdown"]; + [[[RouterProcessStatus alloc] init] triggerEventWithEn:@"router_stop" details:@"normal shutdown"]; [[SBridge sharedInstance] setCurrentRouterInstance:nil]; sendUserNotification(APP_IDSTR, @"I2P Router has stopped"); }]; - return self; + return self; } - (void) requestShutdown @@ -83,9 +83,8 @@ @catch (NSException *e) { NSLog(@"Expection occurred %@", [e reason]); - auto swiftRouterStatus = [[RouterProcessStatus alloc] init]; self.isRouterRunning = NO; - [swiftRouterStatus triggerEventWithEn:@"router_exception" details:[e reason]]; + [[[RouterProcessStatus alloc] init] triggerEventWithEn:@"router_exception" details:[e reason]]; [[SBridge sharedInstance] setCurrentRouterInstance:nil]; sendUserNotification(@"An error occured, can't start the I2P Router", [e reason]); return 0; @@ -98,3 +97,142 @@ } @end + +typedef struct kinfo_proc kinfo_proc; + +@implementation IIProcessInfo +- (id) init +{ + self = [super init]; + if (self != nil) + { + numberOfProcesses = -1; // means "not initialized" + processList = NULL; + } + return self; +} + +- (int)numberOfProcesses +{ + return numberOfProcesses; +} + +- (void)setNumberOfProcesses:(int)num +{ + numberOfProcesses = num; +} + +- (int)getBSDProcessList:(kinfo_proc **)procList + withNumberOfProcesses:(size_t *)procCount +{ +#ifdef __cplusplus + int err; + kinfo_proc * result; + bool done; + static const int name[] = { CTL_KERN, KERN_PROC, KERN_PROC_ALL, 0 }; + size_t length; + + // a valid pointer procList holder should be passed + assert( procList != NULL ); + // But it should not be pre-allocated + assert( *procList == NULL ); + // a valid pointer to procCount should be passed + assert( procCount != NULL ); + + *procCount = 0; + result = NULL; + done = false; + + do + { + assert( result == NULL ); + + // Call sysctl with a NULL buffer to get proper length + length = 0; + err = sysctl((int *)name,(sizeof(name)/sizeof(*name))-1,NULL,&length,NULL,0); + if( err == -1 ) + err = errno; + + // Now, proper length is optained + if( err == 0 ) + { + result = (kinfo_proc *)malloc(length); + if( result == NULL ) + err = ENOMEM; // not allocated + } + + if( err == 0 ) + { + err = sysctl( (int *)name, (sizeof(name)/sizeof(*name))-1, result, &length, NULL, 0); + if( err == -1 ) + err = errno; + + if( err == 0 ) + done = true; + else if( err == ENOMEM ) + { + assert( result != NULL ); + free( result ); + result = NULL; + err = 0; + } + } + }while ( err == 0 && !done ); + + // Clean up and establish post condition + if( err != 0 && result != NULL ) + { + free(result); + result = NULL; + } + + *procList = result; // will return the result as procList + if( err == 0 ) + *procCount = length / sizeof( kinfo_proc ); + assert( (err == 0) == (*procList != NULL ) ); + return err; +} + +- (void)obtainFreshProcessList +{ + int i; + kinfo_proc *allProcs = 0; + size_t numProcs; + NSString *procName; + + int err = [self getBSDProcessList:&allProcs withNumberOfProcesses:&numProcs]; + if( err ) + { + numberOfProcesses = -1; + processList = NULL; + + return; + } + + // Construct an array for ( process name, pid, arguments concat'ed ) + processList = [NSMutableArray arrayWithCapacity:numProcs]; + for( i = 0; i < numProcs; i++ ) + { + int pid = (int)allProcs[i].kp_proc.p_pid; + procName = [NSString stringWithFormat:@"%s, pid %d, args: %s", allProcs[i].kp_proc.p_comm, pid, getArgvOfPid(pid).c_str()]; + [processList addObject:procName]; + } + + [self setNumberOfProcesses:(int)numProcs]; + free( allProcs ); +#endif +} + + +- (BOOL)findProcessWithStringInNameOrArguments:(NSString *)procNameToSearch +{ + BOOL seenProcessThatMatch = NO; + for (NSString* processInfoStr in processList) { + if ([processInfoStr containsString:procNameToSearch]) { + seenProcessThatMatch = YES; + break; + } + } + return seenProcessThatMatch; +} +@end diff --git a/launchers/macosx/SBridge.mm b/launchers/macosx/SBridge.mm index 6778631e78..96375065b8 100644 --- a/launchers/macosx/SBridge.mm +++ b/launchers/macosx/SBridge.mm @@ -20,6 +20,9 @@ #import #import "I2PLauncher-Swift.h" +#include "LoggerWorker.hpp" +#include "Logger.h" +#include "logger_c.h" #include "AppDelegate.h" #include "include/fn.h" @@ -28,27 +31,39 @@ std::future startupRouter(NSString* javaBin, NSArray* arguments, NSString* i2pBaseDir, RouterProcessStatus* routerStatus) { @try { - RTaskOptions* options = [RTaskOptions alloc]; - options.binPath = javaBin; - options.arguments = arguments; - options.i2pBaseDir = i2pBaseDir; - auto instance = [[I2PRouterTask alloc] initWithOptions: options]; - - [[SBridge sharedInstance] setCurrentRouterInstance:instance]; - [instance execute]; - sendUserNotification(APP_IDSTR, @"The I2P router is starting up."); - auto pid = [instance getPID]; - NSLog(@"Got pid: %d", pid); - if (routerStatus != nil) { - [routerStatus setRouterStatus: true]; - [routerStatus setRouterRanByUs: true]; - [routerStatus triggerEventWithEn:@"router_start" details:@"normal start"]; - [routerStatus triggerEventWithEn:@"router_pid" details:[NSString stringWithFormat:@"%d", pid]]; + IIProcessInfo* processInfoObj = [[IIProcessInfo alloc] init]; + [processInfoObj obtainFreshProcessList]; + auto anyRouterLookingProcs = [processInfoObj findProcessWithStringInNameOrArguments:@"i2p.jar"]; + if (anyRouterLookingProcs) { + auto errMessage = @"Seems i2p is already running - I've detected another process with i2p.jar in it's arguments."; + NSLog(@"%@", errMessage); + sendUserNotification(APP_IDSTR, errMessage); + [routerStatus triggerEventWithEn:@"router_already_running" details:@"won't start - another router is running"]; + return std::async(std::launch::async, []{ + return -1; + }); + } else { + RTaskOptions* options = [RTaskOptions alloc]; + options.binPath = javaBin; + options.arguments = arguments; + options.i2pBaseDir = i2pBaseDir; + auto instance = [[I2PRouterTask alloc] initWithOptions: options]; + + [[SBridge sharedInstance] setCurrentRouterInstance:instance]; + [instance execute]; + sendUserNotification(APP_IDSTR, @"The I2P router is starting up."); + auto pid = [instance getPID]; + NSLog(@"Got pid: %d", pid); + if (routerStatus != nil) { + // TODO: Merge events router_start and router_pid ? + [routerStatus triggerEventWithEn:@"router_start" details:@"normal start"]; + [routerStatus triggerEventWithEn:@"router_pid" details:[NSString stringWithFormat:@"%d", pid]]; + } + + return std::async(std::launch::async, [&pid]{ + return pid; + }); } - - return std::async(std::launch::async, [&pid]{ - return pid; - }); } @catch (NSException *e) { @@ -92,7 +107,7 @@ std::future startupRouter(NSString* javaBin, NSArray* arguments, const char * basePath = [i2pPath UTF8String]; auto jarList = buildClassPathForObjC(basePath); const char * classpath = jarList.c_str(); - NSLog(@"Classpath from ObjC = %s", classpath); + MLog(0, @"Classpath from ObjC = %s", classpath); return [[NSString alloc] initWithUTF8String:classpath]; } @@ -105,14 +120,17 @@ std::future startupRouter(NSString* javaBin, NSArray* arguments, auto classPathStr = buildClassPathForObjC(basePath); RouterProcessStatus* routerStatus = [[RouterProcessStatus alloc] init]; + + NSString *confDir = [NSString stringWithFormat:@"%@/Application\\ Support/i2p", NSHomeDirectory()]; + try { std::vector argList = { @"-Xmx512M", @"-Xms128m", @"-Djava.awt.headless=true", - @"-Dwrapper.logfile=/tmp/router.log", + [NSString stringWithFormat:@"-Dwrapper.logfile=%@/router.log", [NSString stringWithUTF8String:getDefaultLogDir().c_str()]], @"-Dwrapper.logfile.loglevel=DEBUG", - @"-Dwrapper.java.pidfile=/tmp/routerjvm.pid", + [NSString stringWithFormat:@"-Dwrapper.java.pidfile=%@/router.pid", confDir], @"-Dwrapper.console.loglevel=DEBUG" }; diff --git a/launchers/macosx/main.mm b/launchers/macosx/main.mm index 2d1f274252..f7eba9c349 100644 --- a/launchers/macosx/main.mm +++ b/launchers/macosx/main.mm @@ -32,12 +32,16 @@ #include "include/subprocess.hpp" #include "include/strutil.hpp" -using namespace subprocess; +#include "Logger.h" +#include "LoggerWorker.hpp" +using namespace subprocess; #endif #define debug(format, ...) CFShow([NSString stringWithFormat:format, ## __VA_ARGS__]); + + @interface AppDelegate () @end @@ -68,6 +72,7 @@ using namespace subprocess; std::string basePath(homeDir); basePath.append("/Library/I2P"); + auto jarResPath = [launcherBundle pathForResource:@"launcher" ofType:@"jar"]; NSLog(@"Trying to load launcher.jar from url = %@", jarResPath); self.metaInfo.jarFile = jarResPath; @@ -196,6 +201,7 @@ using namespace subprocess; NSBundle *launcherBundle = [NSBundle mainBundle]; + // Helper object to hold statefull path information self.metaInfo = [[ExtractMetaInfo alloc] init]; self.metaInfo.i2pBase = [NSString stringWithUTF8String:i2pBaseDir.c_str()]; @@ -288,12 +294,25 @@ using namespace subprocess; } @end - +#ifdef __cplusplus +namespace { + const std::string logDirectory = getDefaultLogDir(); +} +#endif int main(int argc, const char **argv) { NSApplication *app = [NSApplication sharedApplication]; +#ifdef __cplusplus + mkdir(logDirectory.c_str(), S_IRUSR | S_IWUSR | S_IXUSR); + + SharedLogWorker logger("I2PLauncher", logDirectory); + MeehLog::initializeLogging(&logger); + + MLOG(INFO) << "Application is starting up"; +#endif + AppDelegate *appDelegate = [[AppDelegate alloc] initWithArgc:argc argv:argv]; app.delegate = appDelegate; auto mainBundle = [NSBundle mainBundle]; @@ -304,8 +323,11 @@ int main(int argc, const char **argv) [NSApp terminate:nil]; } +#pragma GCC diagnostic push +#pragma GCC diagnostic ignored "-Wdeprecated-declarations" [NSBundle loadNibNamed:@"I2Launcher" owner:NSApp]; - +#pragma GCC diagnostic pop + [NSApp run]; return 0; }