macの最近のブログ記事

で。

CFStream APIについて触れたかったのはこっち。CFStreamはネットワーク部分はいわゆるTCP/IPソケットと同じなので、connectとacceptによって通信路が確立される。

前のエントリで触れた内容ではクライアント側の実装、すなわちconnect側だったわけだが、もちろんサーバソケット(accept側)の実装もできる。ちょっと手間はかかるけれどもこんな感じ。

static void
_server_socket_callback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
    
    NSLog(@"accepted. (s = %p, handle = %d)", s, handle);
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocket(kCFAllocatorDefault, handle, &readStream, &writeStream);
    
    if (_setup_read_stream(readStream) != 0) {
        CFRelease(readStream);
        readStream = NULL;
    }
    if (_setup_write_stream(writeStream) != 0) {
        CFRelease(writeStream);
        writeStream = NULL;
    }
    
    NSLog(@"readStream = %p, writeStream = %p", readStream, writeStream);
}

static void
_do_server()
{
    CFSocketContext context = { 0, NULL, NULL, NULL, NULL };
    CFSocketRef socket = CFSocketCreate(kCFAllocatorDefault, PF_INET, SOCK_STREAM, 0, kCFSocketAcceptCallBack, _server_socket_callback, &context);

    int yes = 1;
    setsockopt(CFSocketGetNative(socket), SOL_SOCKET, SO_REUSEADDR, &yes, sizeof(yes));
    
    struct sockaddr_in sin;
    memset(&sin, 0, sizeof(sin));
    sin.sin_family = AF_INET;
    sin.sin_addr.s_addr = INADDR_ANY;
    sin.sin_port = htons(8888);
    sin.sin_len = sizeof(struct sockaddr_in);
    
    CFDataRef data = CFDataCreate(kCFAllocatorDefault, (UInt8*)&sin, sizeof(sin));
    CFSocketSetAddress(socket, data);
    CFRelease(data);
    
    CFRunLoopSourceRef source = CFSocketCreateRunLoopSource(kCFAllocatorDefault, socket, 0);
    CFRunLoopAddSource(CFRunLoopGetCurrent(), source, kCFRunLoopCommonModes);
    CFRelease(source);
    
    NSLog(@"socket = %p", socket);
}
  1. CFSocketCreate()でCFSocketオブジェクトを生成し、
  2. CFSocketSetAddress()でバインディングアドレスを指定、
  3. CFSocketCreateRunLoopSource()およびCFRunLoopAddSource()でRun LoopにCFSocketオブジェクトを登録、
  4. 最初のCFSocketCreate()で登録したコールバック(_server_socket_callback)にacceptされたソケットディスクリプタ(=CFSocketNativeHandle)が渡り、
  5. 4.のソケットディスクリプタからCFStreamCreatePairWithSocket()より、CFReadStream/CFWriteStreamを生成。

これ以降は前エントリの扱いと同じ。

んで。

このパターンで生成されたCFReadStream/CFWriteStreamは、CFReadStreamClose()/CFWriteStreamClose()してもTCP接続は閉じないという現象を確認してしまいました。これそういうもん?と色々調べたところ、CFReadStream/CFWriteStreamオブジェクトのプロパティkCFStreamPropertyShouldCloseNativeSocketをTRUE(kCFBooleanTrue)にするとちゃんと閉じてくれるようになった。

static void
_server_socket_callback(CFSocketRef s, CFSocketCallBackType type, CFDataRef address, const void* data, void* info)
{
    switch (type) {
        case kCFSocketAcceptCallBack: {
            CFSocketNativeHandle handle = *(CFSocketNativeHandle*)data;
            
            NSLog(@"accepted. (s = %p, handle=%d)", s, handle);
            
            CFReadStreamRef readStream = NULL;
            CFWriteStreamRef writeStream = NULL;
            
            CFStreamCreatePairWithSocket(kCFAllocatorDefault, handle, &readStream, &writeStream);
            
            CFReadStreamSetProperty(readStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            CFWriteStreamSetProperty(writeStream, kCFStreamPropertyShouldCloseNativeSocket, kCFBooleanTrue);
            
            if (_setup_read_stream(readStream) != 0) {
                CFRelease(readStream);
                readStream = NULL;
            }
            if (_setup_write_stream(writeStream) != 0) {
                CFRelease(writeStream);
                writeStream = NULL;
            }

            NSLog(@"readStream = %p, writeStream = %p", readStream, writeStream);
        }
            break;
    }
}

さて前のエントリでCFHostをうっかりtoll-freeだと勘違いして書いてしまったせいで、引き続きのこのエントリが内容がらりと変えることになってしまいましてね、と前置きをしつつの第2回であります。

さて無事クエリを解決できたCFHostを用いていよいよ通信の確立ですが。CFStream APIを使えばそのまま非同期接続が可能なのでそれで充分です。さっそく行ってみる。ざっくり。

static int
_setup_read_stream(CFReadStreamRef readStream)
{
    CFStreamClientContext context = { 0, NULL, NULL, NULL, NULL };
    if (!CFReadStreamSetClient(readStream,
                               kCFStreamEventOpenCompleted |
                               kCFStreamEventHasBytesAvailable |
                               kCFStreamEventErrorOccurred |
                               kCFStreamEventEndEncountered,
                               _read_stream_callback, &context)) {
        CFStreamError err = CFReadStreamGetError(readStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        return -1;
    }
    CFReadStreamScheduleWithRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    
    if (!CFReadStreamOpen(readStream)) {
        // error.
        CFStreamError err = CFReadStreamGetError(readStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        
        CFReadStreamSetClient(readStream, kCFStreamEventNone, NULL, NULL);
        CFReadStreamUnscheduleFromRunLoop(readStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        return -1;
    }
    
    return 0;
}

static int
_setup_write_stream(CFWriteStreamRef writeStream)
{
    CFStreamClientContext context = { 0, NULL, NULL, NULL, NULL };
    
    if (!CFWriteStreamSetClient(writeStream,
                                kCFStreamEventOpenCompleted |
                                kCFStreamEventCanAcceptBytes |
                                kCFStreamEventErrorOccurred |
                                kCFStreamEventEndEncountered,
                                _write_stream_callback, &context)) {
        CFStreamError err = CFWriteStreamGetError(writeStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        return -1;
    }
    CFWriteStreamScheduleWithRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
    
    if (!CFWriteStreamOpen(writeStream)) {
        // error.
        CFStreamError err = CFWriteStreamGetError(writeStream);
        NSLog(@"err = %d/%ld", err.error, err.domain);
        
        CFWriteStreamSetClient(writeStream, kCFStreamEventNone, NULL, NULL);
        CFWriteStreamUnscheduleFromRunLoop(writeStream, CFRunLoopGetCurrent(), kCFRunLoopCommonModes);
        return -1;
    }
    
    return 0;
}

static void
_do_connect(CFHostRef host)
{
    NSLog(@"host = %@", host);
    
    CFReadStreamRef readStream = NULL;
    CFWriteStreamRef writeStream = NULL;
    
    CFStreamCreatePairWithSocketToCFHost(kCFAllocatorDefault, host, 80, &readStream, &writeStream);
    
    NSLog(@"readStream = %@", readStream);
    NSLog(@"writeStream = %@", writeStream);
    
    if (_setup_read_stream(readStream) != 0) {
        CFRelease(readStream);
        readStream = NULL;
    }
    if (_setup_write_stream(writeStream) != 0) {
        CFRelease(writeStream);
        writeStream = NULL;
    }

    NSLog(@"connect start.");
}

_do_connect()で使うCFStreamCreatePairWithSocketToCFHost()からCFReadStream/CFWriteStreamオブジェクトを取り出してこれをCFRunLoopにバインドするまでで一連の処理になります。read/writeともに、使用するAPIの流れは以下の通り。

  1. CFXxxStreamSetClient()
  2. CFXxxStreamScheduleWithRunLoop()
  3. CFXxxStreamOpen()

最初のCFXxxStreamSetClient()でコールバック関数を指定した後、Run Loopに登録。その後にCFXxxStreamOpen()でCFStreamを開けばコールバック関数に処理が渡り始める。

static void
_read_stream_callback(CFReadStreamRef readStream, CFStreamEventType eventType, void* info)
{
    NSLog(@"readStream = %p, eventType = %ld", readStream, eventType);
    
    switch (eventType) {
        case kCFStreamEventOpenCompleted:
            ...            
        case kCFStreamEventHasBytesAvailable:
            ...
        case kCFStreamEventEndEncountered:
            ...
        case kCFStreamEventErrorOccurred:
            ...
    }
}

static void
_write_stream_callback(CFWriteStreamRef writeStream, CFStreamEventType eventType, void* info)
{
    NSLog(@"writeStream = %p, eventType = %ld", writeStream, eventType);
    
    switch (eventType) {
        case kCFStreamEventOpenCompleted:
            ...
        case kCFStreamEventCanAcceptBytes:
            ...
        case kCFStreamEventEndEncountered:
            ...
        case kCFStreamEventErrorOccurred:
            ...
    }
}

一方で、ここまでコールバックを設定するのにCFHostClientContextおよびCFStreamClientContextという構造体があったけれども、これがコールバック関数の引数に渡ってくることになる。

struct CFHostClientContext {
  CFIndex			 version;
  void *			  info;
  CFAllocatorRetainCallBack  retain;
  CFAllocatorReleaseCallBack  release;
  CFAllocatorCopyDescriptionCallBack  copyDescription;
};
typedef struct CFHostClientContext	  CFHostClientContext;

typedef CALLBACK_API_C( void , CFHostClientCallBack )(CFHostRef theHost, CFHostInfoType typeInfo, const CFStreamError *error, void *info);

typedef struct {
    CFIndex version;
    void *info;
    void *(*retain)(void *info);
    void (*release)(void *info);
    CFStringRef (*copyDescription)(void *info);
} CFStreamClientContext;

typedef void (*CFReadStreamClientCallBack)(CFReadStreamRef stream, CFStreamEventType type, void *clientCallBackInfo);
typedef void (*CFWriteStreamClientCallBack)(CFWriteStreamRef stream, CFStreamEventType type, void *clientCallBackInfo);

CFHostClientContextおよびCFStreamClientContextのinfoフィールドで指定したアドレスが、CFHostClientCallBackの第4引数info、CFReadStreamClientCallBack/CFWriteStreamClientCallBackの第3引数clientCallBackInfoに渡ってくる。

このCFHostClientContext/CFStreamClientContextでは、infoフィールドのアドレスをCoreFoundationオブジェクトが標準でそうであるように、参照カウントの仕組みを前提としており、infoを引数にするretain/release関数を指定することができる。それぞれretain/releaseフィールドはこの用途で使われる。また、copyDescriptionフィールドは、デバッグ用途などでオブジェクトの内容を文字列化する際に使うことができる。CocoaでNSLogに書式化文字列を使って"%@"にオブジェクトを直接ぶち込み、オブジェクトの内容をコンソールに簡易に出力することができるアレのこと。

ただ、retain/release/copyDescriptionフィールドはあまり重要性が高くなく、Cocoaとの連携で考える限りにおいて参照カウントの管理についてもObjective-C側でのみ面倒を見ていれば大抵は用が足りると思われ、あんまりそこは気にしなくてよさそうな感触である。

ちょっと自分で必要があって調べたので、ここにまとめておこうと思う。Core Foundationネタである。

大抵のことはCocoa(というかFoundationとAppKit)レイヤーで済んでしまうので、割とこう厄介になることは少ないもんなんだけど、いざ細かいところが気になり出して手を加えようとしはじめると粒度が荒くて...なんてことが割とある。以下はそのときに調べて使ったものを少しずつ小分けにまとめてみたもの。備忘録的に行ってみよう。

***

CFHostで非同期に名前解決を行えることを知ったのでそのやり方をまとめてみた。CFHostは普通の同期解決も非同期解決もできるんだが、メインループとの親和的な統合を歌っているだけあり、非同期解決の方でこそその旨味が発揮されるというものかもしれない。っていうか同期解決なら普通にNSHost(Cocoa)でもできるし。

非同期で名前解決することで得られるメリットは「アプリの挙動が安定すること」だ。MacOSX/iOSいずれであっても、ユーザの操作にはできるだけダイレクトに応答するようにした方が、アプリが安定して動いている感じがしてよろこばれるし、また何かしらのブロッキングな処理で一見アプリの動作が止まっているように見えても、「名前解決をしているのか」「データを読もうとしているのか」がわかる、あるいは本当に異常で「フリーズして」しまっているのかを見分けられるというのはアプリケーションに対する安心感として帰ってくる。また、もし誤操作でブロッキングさせた、もしくはブロックしている間に気が変わって他のことをしたくなったなら、簡単にキャンセルして元に戻せるようにもしたい。快適なインターネットライフ(何)のために、アプリケーションの挙動はできるだけユーザに安心と信頼を与えられるようでありたいものだ。

ネットワークアプリケーションを作るときに、ブロッキングが発生するタイミングは大きく三つある。

  • 名前解決
  • TCP接続の確立(connect(),accept())
  • データの送受信

これらの状況で何も考えずに処理を書くと簡単にブロックが発生してしまう。ブロックが発生するというのは即ちGUI操作が止まってウィンドウは動かせずボタンは押せず、レインボーカーソルのお出ましである。「サーバとの通信を行います」とモーダルダイアログで告知してたまに通信を行うだけの必要最低限のネットワーク機能しか無いようなやつならまだしも、メールソフトやブラウザが頻繁に固まるようなアプリケーションばかりでは、正直快適とは言いがたい。

Cocoaではこの「データの送受信」については既に非同期処理が導入されている(NSStreamsとか)。このため大抵のケースはこれで間に合うが、前の二つ「名前解決」「connect()/accept()」については同期のみ、というか手の入れようが無い。まあDNSはローカル上でもキャッシュ機構が働いているし、NSURLなどではそこも含めて隠蔽しているはずなので(未確認だけど)、こまけえこたぁ気に(ryってことなのかもしれない。

しかしNSURLにはそぐわないがヘビーにTCP/IPをつつきたい用途なんかもやっぱりあるわけで、そうするとあとはBSDソケットレイヤーでO_NONBLOCKを...とか言いたくなるところだが、Core FoundationにはCFHostというAPIがあって、こいつで非同期クエリが飛ばせるらしいと知った(しかもNSHostのtoll-free)←勘違いでした(_ _)。

http://developer.apple.com/library/ios/#documentation/CoreFoundation/Reference/CFHostRef/Reference/reference.html
CFHost Reference

おおまかな流れとしては以下の通り。

  1. CFHostCreateWithName()でインスタンス生成
  2. CFHostSetClient()でコールバックを設定
  3. CFHostScheduleWithRunLoop()でメインループに接続
  4. CFHostStartInfoResolution()でクエリ送信
  5. 2.で設定したコールバックでクエリ応答を受信
static void
_host_client_callback(CFHostRef host, CFHostInfoType typeInfo, const CFStreamError *err, void* info)
{
    Boolean resolved = FALSE;
    
    CFArrayRef addr = CFHostGetAddressing(host, &resolved);
    
    NSLog(@"ASYNC: resolved = %d, err = %d/%ld", resolved, err->error, err->domain);
    NSLog(@"ASYNC: result = %@", addr);    
}

static void
_do_query(CFStringRef s)
{
    Boolean ret, resolved;
    CFStreamError err;
    
    CFHostRef host = CFHostCreateWithName(NULL, s);
    CFHostClientContext context = { 0, NULL, CFRetain, CFRelease, NULL };
    
    CFHostSetClient(host, _host_client_callback, &context);
    
    CFHostScheduleWithRunLoop(host, CFRunLoopGetMain(), kCFRunLoopDefaultMode);
    ret = CFHostStartInfoResolution(host, kCFHostAddresses, &err);
    {
        CFArrayRef addr;
        
        NSLog(@"SYNC: ret = %d", ret);
        
        addr = CFHostGetAddressing(host, &resolved);
        
        NSLog(@"SYNC: resolved = %d, err = %d/%ld", resolved, err.error, err.domain);
        NSLog(@"SYNC: result = %@", addr);    
    }
}

CFHostStartInfoResolution()に至る前にCFHostSetClient()を行ってコールバックを設定していなければ、非同期解決にはならずにそのままCFHostStartInfoResolution()でブロックがかかる。

そして_do_query()の引数にCFStringRefが与えられているがこれもNSStringのtoll-free bridgeがかかっているので、呼び出す時はNSStringオブジェクトにキャストして渡してやればよい。

    NSString* s = @"www.google.com";
    _do_query((CFStringRef)s);

そして実行結果は下記のようになる。

2011-05-13 12:17:54.757 CFHostTester[65231:903] SYNC: ret = 1
2011-05-13 12:17:54.760 CFHostTester[65231:903] SYNC: resolved = 0, err = 0/0
2011-05-13 12:17:54.761 CFHostTester[65231:903] SYNC: result = (null)
2011-05-13 12:17:54.860 CFHostTester[65231:903] ASYNC: resolved = 1, err = 0/0
2011-05-13 12:17:54.861 CFHostTester[65231:903] ASYNC: result = (
    <10020000 40e9b769 00000000 00000000>,
    <10020000 40e9b76a 00000000 00000000>,
    <10020000 40e9b793 00000000 00000000>,
    <10020000 40e9b763 00000000 00000000>,
    <10020000 40e9b767 00000000 00000000>,
    <10020000 40e9b768 00000000 00000000>
)

ログの出力に「SYNC:」と「ASYNC:」とそれぞれ付く箇所の出力内容に注意。SYNC:の付く箇所はCFHostStartInfoResolution()を呼んだ直後に取ったデータだが、ここには決して名前解決したデータが乗ってくることはない。何度も同じ名前解決を行って確実にDNSキャッシュに乗ったと思われる状況でも同じだったので、非同期設定を行ったCFHostではコールバックが行われるまではDNS情報はCFHostオブジェクトには渡って来ない、と見るべきなよう。もちろん、非同期設定を行わないつまりCFHostSetClient()およびCFHostScheduleWithRunLoop()を呼ばなければ、普通に「SYNC:」の行で結果は返る。

NSStreamのメソッドでさらに足りないなーと思っていたのがCFStreamCreatePairWithSocketっていうAPI。...いやもっとぶっちゃけて言えばbind()してlisten()してaccept()するサーバソケットをNSStreamで使いたいなー、という。

で、結論から言ってしまうとこれでNSInputStream/NSOutputStreamが取れるのはいいんだが、closeするときがめんどくさい。お行儀悪くてもいいって言うなら止めはしないが。めんどくさいというのは、inputもoutputも共々closeしてもソケットそのものをcloseしないとpeerにFINを送ってくれないようなのです。

まあCocoaで組むサーバアプリなんてそうそうないんで普通は気にしないもんだけど(というかだからこそNSStreamでは手薄なんだが)、だからといって全く使わないこともないわけでねえ。DCC(IRC)とか、FTPとかね。

というわけでいろいろ試してみたけど、closeするまでちゃんとソケットのディスクリプタそのものをちゃんと管理しておけってところのようだ。まあせっかくなのでCFSocketまで使え、っていうソリューションもあるけどどっちにしろ多少の追加実装が必要そうなことには変わりなさげ。

v6/v4両アドレスを併用しているホストで、v4アドレスでのみlisten()しているサーバプロセスに対してMacから接続しようとする際、しくじるというパターンがあった。具体的に言うとtiarraがv6アドレスでlisten()してくれなかったんですがね。

Intermezzoの中で+[NSHost getStreamsToHost:port:inputStream:outputStream:]を使っていたのが、これがうまくなかったらしい。v6アドレスでconnect()しに行って蹴られてそこでくじけて終了してた。telnetだとv6失敗→v4の順でフォールバックしてくれるのに、Intermezzoの手製コードだとそれが起こらなかったの。

実はこの+[NSHost getStreamsToHost:port:inputStream:outputStream:]メソッドはiOS SDKでは使えないため、以下のような置き換え用のコードが存在している。これに切り替えて動作させたらv6→v4の順にconnect()してくれて問題を回避できた。1番目の引数にホスト名を表す文字列(NSString)を要求するので、-[NSHost name]を使って渡すようにした。

http://developer.apple.com/library/ios/#qa/qa2009/qa1652.html
Technical Q&A QA1652: Using NSStreams For A TCP Connection Without NSHost

というわけで、NSHostで名前解決を先にしてしまうとv6→v4フォールバックが起こらなかったよ、という結果報告でござった。

http://developer.apple.com/iphone/program/sdk.html
Get Ready for iPhone OS 3.0 - iPhone Developer Program

まだあんまり中身見通してないのでアレですけども。とりあえず個人的に欲しい機能がまだ来ないっぽいのが(´・ω・`)です。はい。

あとはかつて使ってたiPod Touch(16GB)を手放さずにとっとけばよかったな、とも。

NSTextAttachmentを使ってNSAttributedStringに画像をはめ込むことが出来るのは知ってたんだけど、最近ふと思いついて初めてちゃんと使ってみた。

	NSFileWrapper* filewrapper = [[[NSFileWrapper alloc] initWithPath:@"..."] autorelease];	
	NSTextAttachment* attachment = [[[NSTextAttachment alloc] initWithFileWrapper:filewrapper] autorelease];

	id text = [NSAttributedString attributedStringWithAttachment:attachment];

こんな風に。だけど、適当に拾ってきた画像だと縦横サイズが不定なので、やたらどでかいファイル持ってこようものなら、際限なく広がってしまってちょっと始末が悪い。最大サイズを指定してそれ以上になったらリサイズするようにしてみたくなった。

pict01.png

つまりこれが、

pict02.png

こうなっちゃったりすると、いやよね奥さんそう思いませんおほほ?という。

でこれが、あれやこれやと試してみてなかなかうまく行かなかった。NSTextAttachmentCellがNSTextAttachmentによって自動的に生成されるに任せていると、元のファイルのサイズでそのまま出てしまう。-[NSTextAttachment attachmentCell]で得られるのはid<NSTextAttachmentCell>で、内部で生成されたNSImageインスタンスを参照するメソッド等は用意されていない。むりくりNSTextAttachmentCell*型にキャストするわけにもいかないので、どうしようかという話になった。

まあ、思いつけばな〜んだの部類すけど、縮小したNSImageを自前で生成してそれを同じく自前で作ったNSTextAttachmentCellに渡してNSTextAttachmentにつなげる、という手はずに乗せたらうまくいったとよ。


NSSize
_size_proportionally(NSSize origSize, NSSize maxSize)
{
	CGFloat zoom = (origSize.width > origSize.height ? maxSize.width / origSize.width : maxSize.height / origSize.height);

	return NSMakeSize(origSize.width * zoom, origSize.height * zoom);
}

- (IBAction)doAction:(id)sender
{
	NSURL *url = [NSURL URLWithString:@"http://icanhascheezburger.files.wordpress.com/2008/11/funny-pictures-always-hold-hands-with-your-kitten.jpg"];
	NSImage *image = [[[NSImage alloc] initWithContentsOfURL:url] autorelease];

	[image setSize:_size_proportionally([image size], NSMakeSize(200, 200))];
    NSTextAttachment* attachment = [[[NSTextAttachment alloc] init] autorelease];
	NSTextAttachmentCell* attachmentCell = [[[NSTextAttachmentCell alloc] initImageCell:image] autorelease];
	
	[attachment setAttachmentCell:attachmentCell];
	
	id text = [NSMutableAttributedString attributedStringWithAttachment:attachment];
	[text addAttribute:NSLinkAttributeName value:url range:NSMakeRange(0, [text length])];
	
	[[textView textStorage] appendAttributedString:text];
}

pict03.png

こうなった。よーしよし。

手元のテキストファイルでUTF-8のがあって、適当なスクリプトやアプリケーションに渡してテキスト処理を行っていたのがどうもうまくいかないことがあったので、よく見てみたら先頭にBOMがついていたときの話。

nkfのマニュアルを見たところ、-wと-w8とでBOMの有無を区別してくれるので、こいつに通せばよしなに変換してくれるかと思ったけども、どうやら入力ではBOMを判別してはくれないらしい。変化はなかった。ちなみにバージョンは2.0.7。

ここで変換によるBOM取りをあきらめてエディタで削除する方針に変更。何もしないとvimはBOM付きでもちゃんと認識して表示を隠してくれるので、隠さず見せるようにバイナリモードで読む必要があり。

$ vim -b hogehoge.txt

これでok。

<feff>hoge
このようにして見せてくれるので、<feff>を消せばよい。

欲しいのは欲しいんだが

| コメント(0) | トラックバック(0)

http://www.apple.com/jp/news/2008/oct/15macbook.html
新しいMacBookファミリー、ノートブックのデザインを再定義

筐体がアルミでありながらLEDディスプレイの周りが黒ブチになったのは自分としてはものすごく嬉しい。ディスプレイの周辺を黒色で締めてほしいってのはずっと思ってたし、これは大変よろこばしい。うちのTVもこれがあって今のをつかってるようなもの。

http://www.toshiba.co.jp/product/tv/ctv/25zb22.htm
東芝4:3フラットテレビ 25ZB22/25ZS12(生産終了製品)

こっち↑はまだまだ現役っすよ。

ま、Nehalem(っていうかCore i7)が出ればすぐにマイチェンするんだし、そんときでいっかなーと思いはするものの、そそられる物欲がいいかんじです。

とはいえ現状の環境(MacPro+MacBookAir)で不満が無いので、しばらくは眺めてることにします。

MacBookを手放そうかと

| コメント(0) | トラックバック(0)

まあ、こちらを奮発したのにはわけがあって、この半年自宅作業になった影響でノートPCをほとんど使わなくなってしまったことに気がついた、というのもある。Mac機はすでにMac Proがあるし、Win機を1台用意する方がはるかに利用度が高いっつーか。

とはいえまったく使うシチュエイションがなくなったかっていうとそうでもなく、適度に悩ましいんだなこれが。どうしましょうね。

このアーカイブについて

このページには、過去に書かれたブログ記事のうちmacカテゴリに属しているものが含まれています。

前のカテゴリはlinuxです。

次のカテゴリはmemoです。

最近のコンテンツはインデックスページで見られます。過去に書かれたものはアーカイブのページで見られます。

ウェブページ

OpenID対応しています OpenIDについて
Powered by Movable Type 5.04