@peccul is peccu

(love peccu '(emacs lisp cat outdoor bicycle mac linux coffee))

macのデスクトップアプリの標準出力と標準エラー出力をコンソールで確認できるアプリを作ってみた

macのデスクトップアプリ(Mountain Lionより新しいもの)ではfprintf(stdout, ...)fprintf(stderr, ...)が捨てられているようで、これを確認する手段を調べた。

パターン1: ターミナルから起動する

〜.app というアプリケーションの実態(実行ファイル)は /path/to/〜.app/Contents/MacOS/〜 なので、 これをターミナルから実行することでターミナルにstdoutやstderrを表示することができる。

通常のコマンドと同様パイプで別のコマンドに渡したり、リダイレクトでファイルに出力することも可能。

パターン2: NSLogにパイプしてコンソールで確認する

ターミナルはちょっと、という状況だったため、実行ファイルを起動して、stdout/stderrをNSLogにパイプするmacアプリを作ってみた。

NSLogの類であればコンソール (/Applications/Utilities/Console.app)で確認できる。

「stdout、stderrを見たいアプリ(〜.app)」を作ったアプリのアイコンにドラッグ&ドロップするとそのアプリが起動して、stdout/stderrがNSLogで出力される。

実装の説明(簡易)

実装した内容はAppDelegate.mとInfo.plistの修正、Sandboxの無効化だけ。

Info.plistの修正

アプリアイコンにドラッグ&ドロップするために対応する拡張子を指定する必要がある。 今回は.appを追加。

 <dict>
        <key>CFBundleTypeName</key>
        <string>Application</string>
        <key>LSHandlerRank</key>
        <string>Default</string>
        <key>CFBundleTypeExtensions</key>
        <array>
            <string>app</string>
        </array>
    </dict>

Xcodeで見るとこんな感じ。

f:id:peccu:20171108151122p:plain

Sandboxの無効化

プロジェクトの設定のCapabilitiesからApp SandboxをOFFに変更。 これがONだとドロップしたアプリも一緒にSandbox内で起動してしまうため、普段利用している設定等が読み込めなかった。

f:id:peccu:20171108151253p:plain

AppDelegate.mの実装

  • ドラッグ&ドロップ時の処理 (application:openFile:)

    ファイルをアプリケーションアイコンにドロップするとapplication:openFile:が呼ばれる。

    ファイルパスが引数に渡されるので、/path/to/〜.app から /path/to/〜.app/Contents/MacOS/〜 を生成してNSTaskで実行する([self launchAppWithPath:apppath])。

- (BOOL)application:(NSApplication *)sender openFile:(NSString *)filename {
    // get Application path
    // cf. https://developer.apple.com/documentation/foundation/nsregularexpression
    NSError *error = NULL;
    NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"/([^/]+).app"
                                                                           options:NSRegularExpressionCaseInsensitive
                                                                             error:&error];
    NSArray *matches = [regex matchesInString:filename
                                      options:0
                                        range:NSMakeRange(0, [filename length])];
    if ([matches count] < 1) {
        return NO;
    }
    NSTextCheckingResult *match = [matches objectAtIndex:0];
    NSRange firstHalfRange = [match rangeAtIndex:1];
    NSString *appname = [filename substringWithRange:firstHalfRange];
    NSString *apppath = [NSString stringWithFormat:@"%@/Contents/MacOS/%@", filename, appname];
    NSLog(@"Launching %@ (%@)", appname, apppath);

    [self launchAppWithPath:apppath];
    return YES;
}
  • アプリの実行とstdout、stderrのパイプ (launchAppWithPath:, receivedData:, receivedErrData:)

    NSTaskで起動する実行ファイルを指定し、NSPipe (変数pとep)をタスクのstdout, stderrに接続している。
    NSFileHandleにてパイプに何か出力があった時の通知を有効にして(waitForDataInBackgroundAndNotify)
    その通知発生時の処理としてreceivedData:receivedErrData:メソッドを指定している。

- (void)launchAppWithPath:(NSString *)apppath {
    NSTask *task = [[NSTask alloc] init];
    [task setLaunchPath: apppath];

    // pipe stdout to NSLog
    // cf. https://stackoverflow.com/a/6931865/514411
    NSPipe *p = [NSPipe pipe];
    [task setStandardOutput:p];

    NSFileHandle *fh = [p fileHandleForReading];
    [fh waitForDataInBackgroundAndNotify];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedData:) name:NSFileHandleDataAvailableNotification object:fh];

    // pipe stderr to NSLog
    NSPipe *ep = [NSPipe pipe];
    [task setStandardError:ep];
    NSFileHandle *efh = [ep fileHandleForReading];
    [efh waitForDataInBackgroundAndNotify];
    [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(receivedErrData:) name:NSFileHandleDataAvailableNotification object:efh];
    [task launch];
}

receivedData:receivedErrData:はほぼ同じで、通知内容からデータを文字列として読み取り、NSLogで出力している。
receivedData:receivedErrData:の違いは出力時のprefixがstdout:stderr:か、のみです。

// pipe stdout to NSLog
// cf. https://stackoverflow.com/a/6931865/514411
- (void)receivedData:(NSNotification *)notif {
    NSFileHandle *fh = [notif object];
    NSData *data = [fh availableData];
    if (data.length > 0) { // if data is found, re-register for more data (and print)
        [fh waitForDataInBackgroundAndNotify];
        NSString *str = [[NSString alloc] initWithData:data encoding:NSUTF8StringEncoding];
        NSLog(@"stdout: %@" ,str);
    }
}

参考URL