2

I'm programming an iOS app for jailbroken devices running iOS 12 or newer in Swift. It is a package manager and in order to install packages I need to run a command which is dpkg -i [PACKAGE_ID] control.

In order to achieve that I made the following function:

func task(launchPath: String, arguments: String) {
    let task = CommandLine()
    task.launchPath = launchPath
    task.arguments = arguments

    let pipe = Pipe()
    task.standardOutput = pipe

    task.launch()
    task.waitUntilExit()

    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = String(data: data, encoding: String.Encoding.utf8)

    return output

}

When I want to run the command, I call my function like this:

task(launchPath: "/usr/libexec/Thunderbolt/supersling", arguments: "/usr/bin/dpkg -i" + packageID + "control")

But it keeps giving me the following error:

'CommandLine' cannot be constructed because it has no accessible initializers

I've been searching on the internet for this error, and I've read that you cannot run CommandLine() in iOS, but I know it is possible to run commands.

I've been searching on the internet on how to do this and found something for Objective-C:

NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath:@"/usr/libexec/Thunderbolt/supersling"];
    [task setArguments:@[@"/usr/bin/dpkg", @"-I", packageID, @"control"]];

    NSPipe *pipe = [NSPipe pipe];
    [task setStandardOutput:pipe];

    [task launch];
    [task waitUntilExit];

    NSFileHandle *read = [pipe fileHandleForReading];
    NSData *dataRead = [read readDataToEndOfFile];
    NSString *stringRead = [[NSString alloc] initWithData:dataRead encoding:NSUTF8StringEncoding];

This does the exact same thing I do but in Objective-C.

Why does Xcode allow this Objective-C code to be run but the Swift one doesn't? Is there any other way to run commands for Swift?

Thanks in advance.

EDIT: I've discovered that the Objective-C code imports it's own NSTask.h header file:

//#import <Foundation/Foundation-Structs.h>
@class NSURL, NSArray, NSDictionary;

@interface NSTask : NSObject

@property (copy) NSURL * executableURL;
@property (copy) NSArray * arguments;
@property (copy) NSDictionary * environment;
@property (copy) NSURL * currentDirectoryURL;
@property (retain) id standardInput;
@property (retain) id standardOutput;
@property (retain) id standardError;
@property (readonly) int processIdentifier;
@property (getter=isRunning,readonly) BOOL running;
@property (readonly) int terminationStatus;
@property (readonly) long long terminationReason;
@property (copy) id terminationHandler;
@property (assign) long long qualityOfService;
+(id)currentTaskDictionary;
+(id)launchedTaskWithDictionary:(id)arg1 ;
+(id)launchedTaskWithLaunchPath:(id)arg1 arguments:(id)arg2 ;
+(id)launchedTaskWithExecutableURL:(id)arg1 arguments:(id)arg2 error:(out id*)arg3 terminationHandler:(/*^block*/id)arg4 ;
+(id)allocWithZone:(NSZone*)arg1 ;
-(void)waitUntilExit;
-(NSURL *)executableURL;
-(id)currentDirectoryPath;
-(void)setArguments:(NSArray *)arg1 ;
-(void)setCurrentDirectoryPath:(id)arg1 ;
-(id)launchPath;
-(void)setLaunchPath:(id)arg1 ;
-(int)terminationStatus;
-(long long)terminationReason;
-(void)launch;
-(BOOL)launchAndReturnError:(id*)arg1 ;
-(void)setCurrentDirectoryURL:(NSURL *)arg1 ;
-(NSURL *)currentDirectoryURL;
-(void)setExecutableURL:(NSURL *)arg1 ;
-(void)interrupt;
-(long long)suspendCount;
-(void)setStandardInput:(id)arg1 ;
-(void)setStandardOutput:(id)arg1 ;
-(void)setStandardError:(id)arg1 ;
-(id)standardInput;
-(id)standardOutput;
-(id)standardError;
-(id)init;
-(NSDictionary *)environment;
-(BOOL)isRunning;
-(BOOL)suspend;
-(BOOL)resume;
-(void)setEnvironment:(NSDictionary *)arg1 ;
-(void)setQualityOfService:(long long)arg1 ;
-(void)setTerminationHandler:(id)arg1 ;
-(int)processIdentifier;
-(id)terminationHandler;
-(long long)qualityOfService;
-(void)terminate;
-(NSArray *)arguments;
@end

Can I use this with Swift? If possible, how could I do that?

amodrono
  • 1,900
  • 4
  • 24
  • 45
  • The Swift equivalent of `NSTask` is `Process`, not `CommandLine` – Martin R Jun 16 '19 at 11:10
  • @MartinR I thought it was renamed to `CommandLine()` in Swift 4, but when I replace `CommandLine()` to `Process()`, it tells me `Use of unresolved identifier Process(), did you mean Progress()?`. – amodrono Jun 16 '19 at 11:12
  • The problem is that iOS does not allow to start separate processes, so that the class is not available in the SDK. I do not know how to work around that on jailbroken devices. – Martin R Jun 16 '19 at 11:15

2 Answers2

3

Please read the edit number 3

Ok, so basically I figured out by myself how to do this, so I'm going to post this answer in order to help people who might have the same problem.

Note this is only available for jailbroken devices

This works on both non-jailbroken and jailbroken iOS devices

The best way to achieve this I was able to find is to use a custom Objective-C header file which created the object NSTask and everything it needs.

Then, in order to use this code with Swift, you'll need to create a Bridging-Header, in which you'll need to import the NSTask.h for it to be exposed to Swift and being able to use it in your Swift code.

Once done this, just use the following function in your code whenever you want to run a task:

func task(launchPath: String, arguments: String...) -> NSString {
    let task = NSTask.init()
    task?.setLaunchPath(launchPath)
    task?.arguments = arguments

    // Create a Pipe and make the task
    // put all the output there
    let pipe = Pipe()
    task?.standardOutput = pipe

    // Launch the task
    task?.launch()
    task?.waitUntilExit()

    // Get the data
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)

    return output!
}

And call it like this:

task(launchPath: "/usr/bin/dpkg", arguments: "-i", packageID, "control")

This will also return the value, so you can even display it by doing:

print(task(launchPath: "/usr/bin/echo", arguments: "Hello, World!"))

Which will print:

~> Hello, World!

Hope this solves the problem.

EDIT 1: I found out that as you are using a custom NSTask object, you are allowed to run the task, even on non-jailbroken devices. Tested on both iPad Mini 2 (iOS 12.1 ~> Jailbroken) and iPhone Xr (iOS 12.2 ~> not jailbroken).

NOTE: Even though this also works on non-jailbroken devices, your App will be rejected on the AppStore, as @ClausJørgensen said:

You're using private APIs, so it'll be rejected on the App Store. Also, Xcode 11 has some new functionality that will trigger a build failure when using certain private APIs.

I'd only recommend the usage of this for apps that won't be uploaded to the App Store, otherwise, try to achieve what you want without using commands, there sure will be any other way to do that.

EDIT 2: For this to work and avoid throwing an NSInternalInconsistencyException, you'll need to set the launchPath to the executable's full path instead of just the directory containing it.

You'll also need to set all the command arguments separated by commas.

Working method (24th March 2020) (ONLY FOR JAILBROKEN DEVICES)

There's an easier method, which is roughly recommended instead of the one above, for running CLI commands on jailbroken iOS devices.

The reason why you can't use NSTask on iOS devices is that your app is sandboxed, and there's no way to avoid that on stock iOS. That's where jailbreaking takes place: using theos' New Instance Creator (NIC) and creating a new instance with the type application, you can easily create unsandboxed applications.

Note that this process is for making jailbreak packages that are installed with the Debian packager (dpkg. If you've used Debian you'll probably be familiar with that). Packages are compiled to .deb files.

Once done that, you can easily use NSTask on your app without any problem, like this:

func task(launchPath: String, arguments: String...) -> NSString {
    let task = NSTask.init()
    task?.setLaunchPath(launchPath)
    task?.arguments = arguments

    // Create a Pipe and make the task
    // put all the output there
    let pipe = Pipe()
    task?.standardOutput = pipe

    // Launch the task
    task?.launch()
    task?.waitUntilExit()

    // Get the data
    let data = pipe.fileHandleForReading.readDataToEndOfFile()
    let output = NSString(data: data, encoding: String.Encoding.utf8.rawValue)

    return output!
}

And call it like this:

task(launchPath: "/usr/bin/dpkg", arguments: "-i", packageID, "control")

When you finished your app, just compile it using make:

make do #just compile the package
make package #compile the package and save it on the 'packages' folder
make package install #if theos and ssh are configured properly, compile the package and automatically install it on your iPhone.
amodrono
  • 1,900
  • 4
  • 24
  • 45
  • You should really clarify loud and clear that this is only available on jailbroken devices; otherwise a bunch of newbies will come about and not understand why they can't do it. – Claus Jørgensen Jun 16 '19 at 13:56
  • @ClausJørgensen well, I actually discovered this does work even on non-jailbroken devices – amodrono Jun 17 '19 at 08:19
  • Yes, but you're using private APIs. So it'll be rejected on the App Store. Also, Xcode 11 have some new functionality that will trigger a build failure when using certain private APIs. – Claus Jørgensen Jun 17 '19 at 08:50
  • @ClausJørgensen Ok, I didn't know that Apple Store rejected apps using private APIs, but anyways, as I'm releasing it for jailbroken iOS devices, so it won't be much of a problem for me. – amodrono Jun 17 '19 at 09:16
  • Thanks for the answer. One question: what is a "packageID"? – ExeRhythm Jun 14 '20 at 13:15
-1

help! when i test in my iphone7(ios13.1 ~> not jailbroken),i had drag the command file to xcode project, and get the file path like this:

let commandPath = Bundle.main.path(forResource: "command", ofType: nil)! 

when i run in iphone like this:

task(launchPath: commandPath, arguments:"")

i got:

'NSInvalidArgumentException', reason: 'launch path not accessible'

how can i get the right path for my command file?

even_cheng
  • 11
  • 1
  • 3
    This should be deleted, because it's a question, posted as an answer. – Trenton McKinney Nov 06 '19 at 07:18
  • Hi! Sorry for the late response. By this date (25th March 2020), you can easily run CLI arguments in plain swift without needing to resort to workarounds by compiling your program using theos (https://github.com/theos/theos) as an unsandboxed application. – amodrono Mar 25 '20 at 17:59