Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Issue with callback functions #120

Closed
yveszoundi opened this issue Jul 27, 2023 · 3 comments
Closed

Issue with callback functions #120

yveszoundi opened this issue Jul 27, 2023 · 3 comments

Comments

@yveszoundi
Copy link

I'm trying to execute a command and capture its output via NSTask, as a way to run external commands within an app bundle (sandbox issues and PATH visibility).

The code below executes successfully, but the command output is printed directly to stdout, instead of being captured. That means that my callback (set_readability_handler) is never registered or invoked.

I suspect that it could be a method signature issue or something fundamental that I'm missing. Any help would be appreciated.

extern crate objc;
extern crate objc_id;
extern crate objc_foundation;

use std::error::Error;
use objc::runtime::{Sel, BOOL, Object};
use objc::{msg_send, sel, class, sel_impl};
use objc_foundation::{NSString, INSString, NSArray, INSArray};

fn main() -> Result<(), Box<dyn Error>> {
    unsafe {
        let cls = class!(NSPipe);
        let pipe: *mut Object = msg_send![cls, alloc];        
        let cls = class!(NSTask);
        let tk: *mut Object = msg_send![cls, alloc];

        let strings = vec![
            NSString::from_str("Hello"),
            NSString::from_str("World"),
        ];

        let args = NSArray::from_vec(strings);

        // See https://gist.github.com/PaulChana/74c7f009e23492fb14bd92e0d82d5268
        extern "C" fn set_readability_handler(_: &Object, _sel: Sel, _: &Object, _: BOOL) {
            println!("Output has been captured.....Maybe not....");
        }          
        
        let s = NSString::from_str("/bin/echo");
        let _:() = msg_send![tk, setLaunchPath: s];
        let _:() = msg_send![tk, setArguments:  args];
        let _:() = msg_send![tk, setStandardOutput: pipe];
        let _:() = msg_send![tk, setStandardError:  pipe];

        let read_handle: *mut Object = msg_send![pipe, fileHandleForReading];

        let _:() = msg_send![read_handle, setReadabilityHandler: set_readability_handler];        
        let _:() = msg_send![read_handle, waitForDataInbackgroundAndNotify];        
        let _:() = msg_send![tk, launch];
        let _:() = msg_send![tk, waitUntilExit];
        let _:() = msg_send![read_handle, closeFile];
        let _:() = msg_send![tk, release];
        let _:() = msg_send![read_handle, release];
        let _:() = msg_send![pipe, release];
    }

    Ok(())
}
@madsmtm
Copy link

madsmtm commented Jul 28, 2023

Probably several reasons:

  1. You have only allocated, and not initialized the NSPipe and NSTask.
  2. The argument passing to many of your message sends are wrong! (Try enabling the verify_message feature, then you should encounter a few errors).
  3. You are trying to use a function as the readabilityHandler, where Objective-C expects a block.
  4. waitForDataInbackgroundAndNotify can only be called on threads that have an active event loop, as the docs say. Also, it's misspelled, it should be waitForDataInBackgroundAndNotify.

@madsmtm
Copy link

madsmtm commented Jul 28, 2023

Although not entirely production ready, and somewhat a shameless self-plug, I can recommend icrate for this sort of stuff. Although you still have to be a bit vigilant (as you always do when writing Objective-C), you can usually avoid a lot of mistakes with that:

use icrate::Foundation::{NSPipe, NSTask, NSString, NSArray};
use block2::ConcreteBlock;
use objc2::ClassType;

fn main() -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        let pipe = NSPipe::pipe();
        let tk = NSTask::init(NSTask::alloc());

        let s = NSString::from_str("/bin/echo");
        tk.setLaunchPath(Some(&s));

        let strings = vec![
            NSString::from_str("Hello"),
            NSString::from_str("World"),
        ];
        tk.setArguments(Some(&NSArray::from_vec(strings)));

        tk.setStandardOutput(Some(&pipe));
        tk.setStandardError(Some(&pipe));

        let read_handle = pipe.fileHandleForReading();

        let handler = ConcreteBlock::new(|_file_handle| {
            println!("Output has been captured...");
        });
        let handler = handler.copy();
        read_handle.setReadabilityHandler(Some(&handler));

        read_handle.waitForDataInBackgroundAndNotify();
        tk.launch();
        tk.waitUntilExit();
        read_handle.closeFile();
    }

    Ok(())
}

@yveszoundi
Copy link
Author

Thanks @madsmtm, your code helped a lot.

I now have a better feel for the macos-specific code that I could test in my application.

Below is what I came up with for running a command with NSTask and parsing its output.

Cargo manifest file

[package]
name = "testing"
version = "0.1.0"
edition = "2021"

[dependencies]
icrate = { version = "0.0.3", features= [ "block", "Foundation", "Foundation_NSFileHandle", "Foundation_NSArray", "Foundation_NSTask", "Foundation_NSString", "Foundation_NSURL", "Foundation_NSData", "Foundation_NSPipe" ]}
block2 = "0.2.0"
objc2 = "0.4.0"

Main code

use icrate::Foundation::{NSPipe, NSTask, NSString, NSArray, NSData, NSDictionary, NSURL};
use block2::ConcreteBlock;
use objc2::ClassType;
use objc2::msg_send;
use objc2::runtime::Object;

use objc2::rc::autoreleasepool;

fn nsdata_to_string(ll: usize, data: *mut Object) -> Option<String> {
    unsafe {
        let alloc_nsstring  = NSString::alloc();
        let nsdata = &* (data as *const NSData);
        let id_nsstring = NSString::initWithData_encoding(alloc_nsstring, nsdata, 4);

        if let Some(nsstring) = id_nsstring {            
            let mut ret = String::with_capacity(ll);

            autoreleasepool(|pool| {
                let v = nsstring.as_str(pool);
                ret.push_str(v);
            });

            Some(ret)
        } else {
            None
        }
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    unsafe {
        let pipe = NSPipe::pipe();
        let tk = NSTask::init(NSTask::alloc());
        let pstring = "/bin/echo".to_string();
        let pstr = pstring.as_str();
        let s = NSString::from_str(pstr);
        let pp = NSURL::alloc();
        let p =  NSURL::initFileURLWithPath(pp, &s);        

        let strings = vec![
            NSString::from_str("Hello"),
            NSString::from_str("World"),
        ];

        let k = NSString::from_str("NSUnbufferedIO");
        let v = NSString::from_str("YES");
        let dict = NSDictionary::from_keys_and_objects(&[&*k], vec![v]);
        
        tk.setExecutableURL(Some(&p));
        tk.setArguments(Some(&NSArray::from_vec(strings)));
        tk.setEnvironment(Some(&dict));
        tk.setStandardOutput(Some(&pipe));
        tk.setStandardError(Some(&pipe));

        let read_handle = pipe.fileHandleForReading();

        let handler = ConcreteBlock::new(|handle| {
            let data: *mut Object = msg_send![handle, availableData];
            let ll: usize = msg_send![data, length];

            if ll != 0 {
                if let Some(output) = nsdata_to_string(ll, data) {
                    println!("Output: {}", output);
                }
            }
        });
        
        let handler = handler.copy();
        read_handle.setReadabilityHandler(Some(&handler));
        read_handle.waitForDataInBackgroundAndNotify();

        if let Err(ex) = tk.launchAndReturnError() {
            return Err(ex.to_string().into());
        }
        
        tk.waitUntilExit();

        if let Err(ex) = read_handle.closeAndReturnError() {
            return Err(ex.to_string().into());
        }

        if tk.terminationStatus() != 0 {
            return Err("Task failed!".into());
        }
    }

    Ok(())
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

2 participants