Mounting Volumes Asynchronously in Swift

3.3k Views Asked by At

I am trying to port a method to mount volumes asynchronous, from Objective C to Swift. but i run into some trouble with some lines of code. I hope somebody can give me an answer.

The problems are : kNAUIOptionKey, kNAUIOptionNoUI and kNetFSUseGuestKey: unresolved identifiers. I can't find an equivalent in Swift.

kCFBooleanTrue : CFBoolean is not convertible to UnsafePointer.

Code between /* and */ is hard to convert to Swift and that's where some help would be welcome.

import Foundation
import Cocoa
import NetFS

func mountVolumeAsync(#username:String, #password:String, #ipadres:String, #proto:String, #mountpoint:String) -> Bool{
    var theURLPath: NSURL
    if username.lowercaseString == "guest" {
        println("Volume will be mounted as Guest user.....")
        let thestring = "\(proto)://\(ipadres)/\(mountpoint)"
        let theURLPath = NSURL(string:thestring)
        if theURLPath == nil {
            println("Path to file is invalid.")
            return false
        }
    } else {
        let thestring = "\(proto)://\(username):\(password)@\(ipadres)/\(mountpoint)"
        let theURLPath = NSURL( string:thestring)
        if theURLPath == nil {
            println("Path to file is invalid.")
            return false
        }
    }

    var mount_options = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, nil, nil);

    if (mount_options != nil) {
        CFDictionarySetValue(mount_options, kNAUIOptionKey, kNAUIOptionNoUI);
        if username.lowercaseString == "guest" {
        CFDictionarySetValue( mount_options, kNetFSUseGuestKey, kCFBooleanTrue);
        }
    }

    var status = false
    /* ---- Objective C Code to port to Swift Code ------
    AsyncRequestID requestID = NULL;
    dispatch_queue_t queue =dispatch_get_main_queue();

    NetFSMountURLAsync(
        CFBridgingRetain(theURLPath),
        NULL,
        (__bridge CFStringRef) username,
        (__bridge CFStringRef) passwd,
        mount_options,
        NULL,
        &requestID,
        queue,
        ^(int status, AsyncRequestID requestID, CFArrayRef mountpoints) {
            NSLog(@"mounted: %d - %@", status, (__bridge NSArray *) mountpoints);
    });

    if (!status) {
        NSString *msg = [NSString stringWithFormat:@"[%@]  is not mounted! Check your params",mntpt];
        return NO;
    }
    */
    return status

}

I am still trying to solve this problem, but unfortunately I do not succeed. Nobody answered this question and I am still looking for an answer. I changed the code to the following and it compiles, but does not run. While debugging i could find the following error :

Printing description of open_options:

(CFMutableDictionary!) open_options = 1 key/value pair {
  [0] =
<Execution was interrupted, reason: EXC_BAD_ACCESS (code=1, address=0x4f4955414e80).
The process has been returned to the state before expression evaluation.>

I found this info in the NetFS framework:

  • The following dictionary keys for open_options are supported:
  • kNetFSUseGuestKey: Login as a guest user.
  • kNetFSAllowLoopbackKey Allow a loopback mount.
  • kNAUIOptionKey = UIOption Suppress authentication dialog UI. *

  • The following dictionary keys for mount_options are supported:

  • kNetFSMountFlagsKey = MNT_DONTBROWSE No browsable data here (see ).
  • kNetFSMountFlagsKey = MNT_RDONLY A read-only mount (see ).
  • kNetFSAllowSubMountsKey = true Allow a mount from a dir beneath the share point.
  • kNetFSSoftMountKey = true Mount with "soft" failure semantics.
  • kNetFSMountAtMountDirKey = true Mount on the specified mountpath instead of below it.
  • Note that if kNetFSSoftMountKey isn't set, then it's set to TRUE.

I hope somebody can help.

func mountVolumeAsync(#username:String, #password:String, #ipadres:String, #proto:String, #mountpoint:String) -> Bool{
var theURLPath: NSURL? = nil

if username.lowercaseString == "guest" {
    // "Volume will be mounted as Guest user....."
    let thestring = StringWithFormat("%@://%@/%@",proto,ipadres,mountpoint)
    let theURLPath = NSURL(string:thestring)
    if theURLPath == nil {
        println("Path to file is invalid.")
        return false
    }
} else {
    let thestring = StringWithFormat("%@://%@:%@@%@/%@",proto,username,password,ipadres,mountpoint)
    let theURLPath = NSURL(string:thestring)
    if theURLPath == nil {
        println("Path to file is invalid.")
        return false
    }
}

var mount_options = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, nil, nil);
var open_options = CFDictionaryCreateMutable( kCFAllocatorDefault, 0, nil, nil);

if open_options != nil {
    CFDictionarySetValue(open_options,"kNAUIOptionKey","NoUI")
}

if (mount_options != nil) {
    //CFDictionarySetValue(mount_options,kNAUIOptionKey"UIOption")
    if username.lowercaseString == "guest" {
        let kNetFSUseGuestKey = "Guest"
        //CFDictionarySetValue( mount_options, kNetFSUseGuestKey, kCFBooleanTrue);
        CFDictionarySetValue(open_options,kNetFSUseGuestKey, "kCFBooleanTrue")
    }
}

var status = false
var requestID: AsyncRequestID = nil
let queue = dispatch_get_main_queue()

NetFSMountURLAsync(
    theURLPath,
    nil,
    username as NSString,
    password as NSString,
    mount_options,
    open_options,
    &requestID,
    queue)
        {(stat:Int32,  requestID:AsyncRequestID,  mountpoints:CFArray!) -> Void in
            println("mounted: \(stat) - \(mountpoints)")
        }

if status == false {
    let msg = "[\(mountpoint)  is not mounted! Check your params"
    println(msg)
    return false
}
}
2

There are 2 best solutions below

1
zneak On BEST ANSWER

Your Swift code crashes because passing a Swift string to a function that accepts an UnsafePointer, such as CFDictionarySetValue, will pass it a pointer to the string character data encoded as UTF-8, not the string object. Additionally, the character data is valid only until the end of the function call, after which it is freed.

The symbols you are looking for do not exist in Swift because they depend on macro expansion. Swift supports #define for dumb tokens (like #define FOO 1), but not for macros like CFSTR. Any such key that you wish to use needs to be copied over to the Swift side. To find their value, use them in a C/Objective-C file and command-click them to go to their definition. You'll find that:

#define kNAUIOptionKey CFSTR("UIOption")
#define kNAUIOptionNoUI CFSTR("NoUI")
#define kNetFSUseGuestKey CFSTR("Guest")

kCFBooleanTrue is missing for the same reason (though it doesn't use CFSTR).

You have two options to bring the CFSTR strings to your Swift code. The first and simplest is just to declare the values as Swift variables:

let kNAUIOptionKey = "UIOption"
let kNAUIOptionNoUI = "NoUI"
let kNetFSUseGuestKey = "Guest"

The second option is to expose the values (with a different name to avoid clashing on the C side of things) through your bridging header:

NSString* NAUIOptionKey;
NSString* NAUIOptionNoUI;
NSString* NetFSUseGuestKey;

Then have something like that at the top level of an Objective-C file:

#include <NetFS/NetFS.h>
NSString* NAUIOptionKey = (NSString*)kNAUIOptionKey;
NSString* NAUIOptionNoNui = (NSString*)kNAUIOptionNoUI;
NSString* NetFSUseGuestKey = (NSString*)kNetFSUseGuestKey;

NSString and CFStringRef are the same thing under the hood. It is documented behavior that casting from one to the other is fine (it's called toll-free bridging).

This solution is probably the most forward-compatible because it doesn't require you to know what the defined values expand to. Apple could theoretically change the string values between SDK versions, so the Swift version could break in the future (though compiled binaries will keep working, and compiling against the same SDK will keep working for as long as Apple supports it, so it's not a big deal).

kCFBooleanTrue is the same as NSNumber(bool: true), because of toll-free bridging again.

Similarly, NSDictionary is toll-free bridged to CFDictionaryRef, so I encourage you to use that instead of the CFDictionary API, which is a pain to use from Swift because of all the void pointers. This is especially attractive in Swift, because the compiler recognizes that NSDictionary* and CFDictionaryRef are the same thing.

So that would be:

import Foundation

let kNAUIOptionKey = "UIOption"
let kNAUIOptionNoUI = "NoUI"
let kNetFSUseGuestKey = "Guest"

func exampleCFDictionary() -> CFDictionaryRef {
    var dict = NSMutableDictionary()
    dict[kNAUIOptionKey] = kNAUIOptionNoUI
    dict[kNetFSUseGuestKey] = NSNumber(bool: true)
    return dict
}

println(exampleCFDictionary())

I'm afraid that you'll be on your own for NetFS itself, but this should pretty much solve your dictionary woes. You obviously don't need a separate function to create the dictionaries, I did it because it was simpler for me than copying your code.

Finally, you are encouraged to create short, self-contained and correct examples of what you're having issues with. Not a lot of people know about NetFS, but it seems that you had issues with Swift and Core Foundation types more than NetFS. With a good SSCCE, you could have reduced your code to these three lines:

var openOptions = CFDictionaryCreateMutable(kCFAllocatorDefault, 0, nil, nil)
CFDictionarySetValue(openOptions, "kNAUIOptionKey", "NoUI")
println(openOptions)

A lot of people on here know what's wrong with this and you'd probably have had your answer within minutes rather than three months later.

3
Swift Soda On

I made some modifications and got this to work consistently with the Xcode 7 beta 5 and Swift 2.0. Async is way better than the Sync version as it does not block the thread. I did to have to bridge any Objective-C parameters. The constants worked perfectly leaving them alone and when I logged their names it they would come up to the proper values. This eliminated some of the extra work described above. But the code did give me a great starting place, so I am thankful for both entries here.

I did use CFMutableDictionary and NSMutableDictionary. The CFMutableRef did not work. This type may have changed recently in the method or was misread. Also the open and mounts options were backwards.

Some observations: only afp:// supports password change. Also the password change flag does not work

public var kNetFSChangePasswordKey: String { get }

it causes an endless loop when used as an open option. Maybe I am using it wrong. If someone can get that flag to work would be good to know how. I tried setting it in the open option dictionary and set it to true and did not have much luck without other than spinning endlessly. Maybe it's a bug in the API and no one has undercovered it.

As a workaround, when I want the user to change their password, I have a checkbox for the password change, then I have it connect via afp:// along with the password sent as blank "", this allows the user to change their password manually at login and if the user is forced to change their password by the server, it will work. Under smb3 the user does not have that option and it will skip any forced password entitlements from the server. Then next time the user logs in turning of the change password flag, it will connect via SMB3. In case you are wondering why use SMB3, because it is noticeably faster than the Apple File Protocol. Microsoft finally got something right. Go figure.

Also the server on both afp and smb connects faster with the .local extension on the URL. If you try afp:// without it, you can run into an endless loop (maybe a DNS problem). But .local is also faster when connecting via SMB. Though SMB will connect without the .local extension or with, but it always connects instantly with .local attached. I am not connecting over the internet, but we might try it when Google Fiber is at our shop.

func openOptionsDict() -> CFMutableDictionary {
    let dict = NSMutableDictionary()
    dict[kNAUIOptionKey] = kNAUIOptionAllowUI
    dict[kNetFSUseGuestKey] = false
    return dict
}

func mountOptionsDict() -> CFMutableDictionary {
    let dict = NSMutableDictionary()
    return dict
}

func asyncMountShare(serverAddress: String, shareName: String, userName: String, password: String) {
    let fm = NSFileManager.defaultManager()
    let mountPoint = "/Volumes/".stringByAppendingString(shareName)
    var isDir : ObjCBool = false
    if fm.fileExistsAtPath(mountPoint, isDirectory:&isDir) {
        if isDir {
            unmount(mountPoint, 0)
            print("unmount \(mountPoint)")
        }
    }

    let escapedAddress = serverAddress.stringByAddingPercentEncodingWithAllowedCharacters(NSCharacterSet.URLQueryAllowedCharacterSet())
    let shareAddress = NSURL(string: escapedAddress!)!

    let openOptions : CFMutableDictionary = openOptionsDict()
    let mount_options : CFMutableDictionary = mountOptionsDict()

    var requestID: AsyncRequestID = nil
    let queue = dispatch_get_main_queue()

    NetFSMountURLAsync(shareAddress, nil, userName as NSString, password as NSString, openOptions, mount_options, &requestID, queue)
        {(stat:Int32,  requestID:AsyncRequestID,  mountpoints:CFArray!) -> Void in
            print("msg: \(stat) mountpoint: \(mountpoints)")
    }
}