Search code examples
pasteboard

Is it possible to access the Mac OS X pasteboard when logged in via SSH?


We have the following snippet.

OSStatus createErr = PasteboardCreate(kPasteboardClipboard, &m_pboard);
if (createErr != noErr) {
    LOG((CLOG_DEBUG "failed to create clipboard reference: error %i" createErr));
}

This compiles fine, however, it fails to run when called from SSH. This is because there is no pasteboard available in the SSH terminal. However, the idea here is to share clipboards between computers.

When run from desktop terminal, this works just fine. But when run from SSH, PasteboardCreate returns -4960 (aka, coreFoundationUnknownErr). I assume that the only way around this issue is to run the application from within the same environment as the pasteboard, but is this possible?


Solution

  • Accessing the pasteboard directly looks to be a no-go. First, launchd won't register the processes1 with the pasteboard server's mach port. You'd first need find a way to get the pasteboard server's mach port (mach_port_names?). Also, direct communication between user sessions is prohibited2, and other communication is limited. I'm not sure if your program will have the rights to connect to the pasteboard server.

    Here's a first shot at an illustrative example on using Apple events to get & set the clipboard as a string. Error handling is minimal to nonexistent (I'm not certain how I feel about require_noerr). If you're going to get/set clipboard data multiple times during a run, you can save the Apple events and, when copying to the clipboard, use AECreateDesc & AEPutParamDesc or (maybe) AEBuildParameters. AEVTBuilder might be of use.

    NSString* paste() {
        NSString *content;
    
        AppleEvent paste, reply = { typeNull, 0L };
        AEBuildError buildError = { typeNull, 0L };
        AEDesc clipDesc = { typeNull, 0L };
    
        OSErr err;
    
        err = AEBuildAppleEvent(kAEJons, kAEGetClipboard, 
                                typeApplicationBundleID, "com.apple.finder", strlen("com.apple.finder"), 
                                kAutoGenerateReturnID, kAnyTransactionID,
                                &paste, &buildError,
                                ""
            );
        require_noerr(err, paste_end);
        err = AESendMessage(&paste, &reply, kAEWaitReply, kAEDefaultTimeout);
        err = AEGetParamDesc(&reply, keyDirectObject, typeUTF8Text, &clipDesc);
        require_noerr(err, pastErr_getReply);
    
        Size dataSize = AEGetDescDataSize(&clipDesc);
        char* clipData = malloc(dataSize);
        if (clipData) {
            err = AEGetDescData(&clipDesc, clipData, dataSize);
            if (noErr == err) {
                content = [NSString stringWithCString:clipData encoding:NSUTF8StringEncoding];
            } else {}
            free(clipData);
        }
    
        AEDisposeDesc(&clipDesc);
    pastErr_getReply:
        AEDisposeDesc(&reply);
    pasteErr_sending:
        AEDisposeDesc(&paste);
    paste_end:
        return content;
    }
    
    OSStatus copy(NSString* clip) {
        AppleEvent copy, reply = { typeNull, 0L };
        AEBuildError buildError = { typeNull, 0L };
    
        OSErr err = AEBuildAppleEvent(kAEJons, kAESetClipboard, 
                                      typeApplicationBundleID, "com.apple.finder", strlen("com.apple.finder"), 
                                      kAutoGenerateReturnID, kAnyTransactionID,
                                      &copy, &buildError,
                                      "'----':utf8(@)",
                                      AEPARAMSTR([clip UTF8String])
                                      /*
                                        "'----':obj {form: enum(prop), want: type(@), seld: type(@), from: null()}"
                                        "data:utf8(@)",
                                        AEPARAM(typeUTF8Text),
                                        AEPARAM(pClipboard),
                                        AEPARAMSTR([clip UTF8String])
                                      */
            );
        if (aeBuildSyntaxNoErr != buildError.fError) {
            return err;
        }
        AESendMessage(&copy, &reply, kAENoReply, kAEDefaultTimeout);
        AEDisposeDesc(&reply);
        AEDisposeDesc(&copy);
        return noErr;
    }
    

    I'm leaving the Core Foundation approach above, but you'll probably want to use NSAppleEventDescriptor to extract the clipboard contents from the Apple Event reply.

        err = AESendMessage(&paste, &reply, kAEWaitReply, kAEDefaultTimeout);
    require_noerr(err, pasteErr_sending);
        // nsReply takes ownership of reply
        NSAppleEventDescriptor *nsReply = [[NSAppleEventDescriptor alloc] initWithAEDescNoCopy:&reply];
        content = [[nsReply descriptorAtIndex:1] stringValue];
        [nsReply release];
    
    pasteErr_sending:
        AEDisposeDesc(&paste);
    paste_end:
        return content;
    }
    

    An NSAppleEventDescriptor is also easier to examine in a debugger than an AEDesc. To examine replies, you can also to set the AEDebugReceives environment variable when using osascript or Script Editor.app:

    AEDebugReceives=1 osascript -e 'tell application "Finder" to get the clipboard'
    

    References:

    1. "Configuring User Sessions"
    2. "Communicating Across Login Sessions"
    3. Mach Kernel Interface, especially:
    4. CFMessagePort Reference (mach port wrapper):
    5. Apple Events Programming Guide
    6. Apple Event Manager Reference
    7. AEBuild*, AEPrint* and Friends
    8. AEBuildAppleEvent on CocoaDev
    9. Mac OS X Debugging Magic (for AEDebugSends and other AEDebug* environment variables)