Schlaubischlump / LocationSimulator

MacOS application to spoof / fake / mock your iOS / iPadOS or iPhoneSimulator device location. WatchOS and TvOS are partially supported.
https://schlaubischlump.github.io/LocationSimulator/
GNU General Public License v3.0
2.39k stars 184 forks source link

[Feature] Simulator support #79

Closed gocal closed 3 years ago

gocal commented 3 years ago

It would be really handy to add the simulator support.

Simulators can be picked from the side list ("Simulator" section)

Schlaubischlump commented 3 years ago

That sounds like a great idea! I'll keep it in mind when I start rewriting the backend code to spoof the location. To my knowledge there is no API to change the location inside the simulator. But it seems like xcrun can control the simulator using simctl. I would prefer not calling command line tools from within swift, but if there is no other way I'll have to do it that way.

It looks like someone already wrote a command line tool for that purpose which internally uses xcrun: https://github.com/MobileNativeFoundation/set-simulator-location/blob/master/sources/simulators.swift

Schlaubischlump commented 3 years ago

I played a little bit with the CoreSimulator framework. Detecting devices and changing the location of a simulator device is possible without simctl. It feels a little bit hacky, since reading the pids for all Simulator.app instances is bad coding style. If anybody knows how to do this without the pid stuff, let me know. I don't know when I have the time to integrate the code into LocationSimulator. If anybody sees this and feels up to the task do the following:

  1. Rename Device to IOSDevice
  2. Create a protocol Device
  3. Create a struct SimulatorDevice
  4. Both IOSDevice and SimulatorDevice should implement the Device protocol
  5. Move this code down below into a helper file, format it and expose it to swift.
  6. Implement SimulatorDevice based on this helper code.
  7. Change the sidebar code to add an additional "Simulator" section.
  8. It might not work, because reading pids of other processes might not be possible from within the sandbox without a special capability
#import <Foundation/Foundation.h>
#import <sys/proc_info.h>
#import <libproc.h>
#include <dlfcn.h>

// This might not be accurate
typedef NS_ENUM(NSUInteger, SimBootState) {
    SimBootStatusOffline = 1,
    SimBootStatusBooting = 2,
    SimBootStatusBooted = 3,
    SimBootStatusShutdown = 4
};

@interface SimDevice : NSObject {}
@property(copy, nonatomic) NSUUID *UDID;
@property(readonly, nonatomic) NSString *name;
- (mach_port_t)lookup:(NSString *)portName error:(NSError **)error;
@end

@interface SimDeviceSet : NSObject {}
- (NSUInteger)registerNotificationHandler:(void (^_Nonnull)(NSDictionary *))handler;
@property(readonly, nonatomic) NSArray *availableDevices;
@end

@interface SimServiceContext : NSObject {}
+ (id)serviceContextForDeveloperDir:(id)arg1 error:(NSError **)arg2;
- (SimDeviceSet *)defaultDeviceSetWithError:(NSError **)arg1;
@end

@interface SimulatorBridge : NSObject {}
- (void)setLocationWithLatitude:(double)latitude andLongitude:(double)longitude;
@end

void* load_bundle(NSString * _Nonnull path) {
    if (![[NSFileManager defaultManager] fileExistsAtPath:path]) {
        NSLog(@"WARNING: Bundle is not present at path: %@", path);
        return nil;
    }
    void* fw = dlopen(path.UTF8String, RTLD_NOW | RTLD_GLOBAL);
    if (!fw) {
        NSLog(@"ERROR: %s", dlerror());
        abort();
    }
    return fw;
}

/**
 - Parameter simulatorPath: The path to the Simulator.app
 - Return: All possible simulator bridge port names for each Simulator.app instance.
 */
NSArray<NSString *>* getSimulatorPortNames(NSString *simulatorPath) {
    NSMutableArray<NSString *> *portNames = [[NSMutableArray alloc] init];
    // Get all pids for every process
    size_t numberOfProcesses = proc_listpids(PROC_ALL_PIDS, 0, NULL, 0);
    pid_t pids[numberOfProcesses];
    bzero(pids, sizeof(pids));
    proc_listpids(PROC_ALL_PIDS, 0, pids, sizeof(pids));
    for (int i = 0;   i < numberOfProcesses;  ++i) {
        if (pids[i] == 0) { continue;  }
        char pathBuffer[PROC_PIDPATHINFO_MAXSIZE];
        bzero(pathBuffer, PROC_PIDPATHINFO_MAXSIZE);
        proc_pidpath(pids[i], pathBuffer, sizeof(pathBuffer));
        // If the pid belongs to the Simulator.app
        if (strlen(pathBuffer) > 0 && strcmp(simulatorPath.UTF8String, pathBuffer) == 0) {
            NSString *name = [NSString stringWithFormat:@"com.apple.iphonesimulator.bridge.%d", pids[i]];
            [portNames addObject:name];
        }
    }
    return portNames;
}

SimulatorBridge *bridgeForSimDevice(SimDevice *device, NSString* portName) {
    NSError *error = nil;
    mach_port_t bridgePort = [device lookup:portName error:&error];
    if (error == nil && bridgePort != 0) {
        NSPort *bridgeMachPort = [NSMachPort portWithMachPort:bridgePort];
        NSConnection *bridgeConnection = [NSConnection connectionWithReceivePort:nil sendPort: bridgeMachPort];
        NSDistantObject *bridgeDistantObject = [bridgeConnection rootProxy];
        if ([bridgeDistantObject respondsToSelector:@selector(setLocationScenarioWithPath:)]) {
            return (SimulatorBridge *) bridgeDistantObject;
        } else {
            NSLog(@"Distant Object for port: '%@' is not a SimulatorBridge", portName);
        }
    } else {
        NSLog(@"Could not get port for name: '%@'", portName);
    }
    return nil;
}

int main(int argc, const char * argv[]) {
    @autoreleasepool {

        NSString *simulatorPath = @"/Applications/Xcode.app/Contents/Developer/Applications/Simulator.app/Contents/MacOS/Simulator";

        load_bundle(@"/Library/Developer/PrivateFrameworks/CoreSimulator.framework/CoreSimulator");

        NSString *path = @"/Library/Developer/CommandLineTools";
        SimServiceContext* context = [NSClassFromString(@"SimServiceContext") serviceContextForDeveloperDir:path error:NULL];
        SimDeviceSet *deviceSet = [context defaultDeviceSetWithError:nil];

        // Example of how to change the locaiton for all currently available devices.
        NSArray<NSString *>* simualtorPorts = getSimulatorPortNames(simulatorPath);
        for (SimDevice *simDevice in deviceSet.availableDevices) {
            for (NSString *portName in simualtorPorts) {
                SimulatorBridge *bridge = bridgeForSimDevice(simDevice, portName);
                if (!bridge) break;
                [bridge setLocationWithLatitude:49.828386 andLongitude:6.742060];
                NSLog(@"Change location");
            }
        }

        // Listen for new devices
        [deviceSet registerNotificationHandler:^(NSDictionary* info) {
            NSString *notification_name = info[@"notification"];
            if ([notification_name isEqualToString: @"device_state"]) {
                SimDevice *device = info[@"device"];
                SimBootState new_state = [info[@"new_state"] unsignedIntValue];
                if (new_state == SimBootStatusBooted) {
                    NSLog(@"Added: %@", device);
                } else if (new_state == SimBootStatusShutdown) {
                    NSLog(@"Removed: %@", device);
                }

            }
        }];

        while (TRUE);
    }
    return 0;
}
Schlaubischlump commented 3 years ago

I made a little progress and implemented a simple Objective-C wrapper around this stuff. But there are still a couple of problems:

  1. The device is detected to early, before the bridge connection is available. If I use the previous method I commented out, before I edited the post, it should work... but it is ugly
  2. There seems to be no way to reset the location spoofing
  3. This breaks the sandbox support
Schlaubischlump commented 3 years ago

I implemented a first version with simulator support. Resetting the location is not possible. Sandbox is still intact, I found a workaround (add com.apple.CoreSimulator to the AppGroup). I changed the detection algorithm, so that the simulator device is detected, when the boot process is finished.

Could you test the new version ? Just download the nightly build.