Gestão Dev & TIARTIGO

Usando kqueue no Cocoa

Eu sou a favor da filosofia de manter os detalhes fora do código, e muitas vezes eu uso arquivos de configuração para organizar esses detalhes. No Cocoa e no Cocoa Touch, isso geralmente significa mantê-los em um plist, mas poderia facilmente ser em JSON, XML, ou YAML.

Eu recebi recentemente uma lista de mudanças que deveriam ser feitas em um projeto. Eram todas pequenas correções que não envolviam muito trabalho, e eram detalhes que eu especifiquei em um arquivo de configuração. Ao fazer essas correções, eu iria mudar um dos valores, recompilar o app, e ver que a mudança que eu havia feito tinha o efeito desejado. Mas como esse arquivo de configuração estava sendo lido no momento da execução, eu percebi que eu poderia recarregar o arquivo de configuração e pular a recompilação.

Usando stat

Meu primeiro palpite sobre como resolver esse problema foi chamar stat em um timer para checar a última vez em que foi modificado e recarregar o arquivo caso necessário. Aqui está um pequeno exemplo para demonstrar:

#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

int main(int argc,char** argv)
{
struct stat buffer;
int status;
int fildes = open("test.plist",O_RDONLY);
time_t startTime = 0;
if(fildes <= 0)
{
printf("No such file...\n");
return -1;
}

for(;;)
{
status = fstat(fildes,&buffer);
if(startTime != buffer.st_atime)
{
startTime = buffer.st_atime;
printf("Reload Config File\n");
}
usleep(100*1000); //Sleep for 0.1 seconds
}

close(fildes);
return 0;
}

Se fôssemos usar isso em uma aplicação cocoa, eu iria envolvê-lo em um método objetcive-c mas isso demonstra a técnica.

Essa parecia uma maneira ineficiente de fazer isso, uma vez que na maior parte do tempo não houve mudança. Isso teria que estar rodando em uma thread diferente ou como parte de um timer ligado ao loop de execução. Acredito que exista uma maneira melhor de fazer isso.

Usando kqueue

O kqueue é uma chamada do sistema BSD que permite que você receba notificações de eventos. Eles não são limitados a notificações de arquivo de sistema, mas é assim que vamos utilizá-las. Queremos receber uma notificação toda vez que uma test.plist for modificada. Esse método parece ser muito mais eficiente do que checando o status do arquivo de sistema por mudanças.

#include <sys/event.h>
#include <sys/time.h>
#include <fcntl.h>
#include <stdio.h>
#include <stdlib.h>

int main(int argc,char** argv)
{
int fildes;
int kq;
int status;
struct kevent change;
struct kevent event;

kq = kqueue();
fildes = open("test.plist", O_RDONLY);
if(fildes <= 0)
{
printf("No such file...\n");
return -1;
}

EV_SET(&change, fildes, EVFILT_VNODE,
EV_ADD | EV_ENABLE | EV_ONESHOT,
NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB,
0, 0);

for(;;)
{
status = kevent(kq, &change, 1, &event, 1, NULL);

if(status > 0)
{
printf("Reload config file\n");
}
}

close(kq);
close(fildes);
return 0;
}

Usando kqueue no Cocoa

Agora que temos o protótipo da linha de comando básica do que queremos, é hora de transformá-lo em algo que podemos usar dentro do nosso app cocoa. Eu criei um wrapper simples em volta do kqueue chamado FileSystemWatch com dois métodos. O primeiro é chamado watchFileAtPath:target:action:. Você simplesmente passa o caminho para o arquivo e o método para ser chamado quando existe uma mudança naquele arquivo.

#import <Foundation/Foundation.h>

@interface FileSystemWatch : NSObject

- (void)watchFileAtPath:(NSString*)path target:(id)target action:(SEL)action;
- (void)stopWatching;

@end
#import "FileSystemWatch.h"

#include <sys/event.h>
#include <sys/time.h>
#include <fcntl.h>


@interface FileSystemWatch ()
@property (nonatomic,retain) NSString* path;
@property (nonatomic,assign) id target;
@property (nonatomic,assign) SEL action;
@property (nonatomic,assign) int fildes;
@property (nonatomic,assign) int kq;
@property (nonatomic,retain) NSThread* watchThread;

@end

@implementation FileSystemWatch
@synthesize path = _path;
@synthesize target = _target;
@synthesize action = _action;
@synthesize fildes = _fildes;
@synthesize kq = _kq;
@synthesize watchThread = _watchThread;

- (void)dealloc
{
[_path release], _path = nil;
[_watchThread release], _watchThread = nil;
[super dealloc];
}

- (void)watchFileAtPath:(NSString*)path target:(id)target action:(SEL)action
{
self.path = path;
self.target = target;
self.action = action;

self.fildes = open([self.path UTF8String], O_RDONLY);
if(self.fildes <= 0)
{
return;
}
self.kq = kqueue();

self.watchThread = [[[NSThread alloc] initWithTarget:self selector:@selector(watchInBackground) object:nil] autorelease];
[self.watchThread start];
}

- (void)stopWatching
{
close(self.kq);
close(self.fildes);
[self.watchThread cancel];
}

- (void)watchInBackground
{
int status;
struct kevent change;
struct kevent event;

EV_SET(&change, self.fildes, EVFILT_VNODE,
EV_ADD | EV_ENABLE | EV_ONESHOT,
NOTE_DELETE | NOTE_EXTEND | NOTE_WRITE | NOTE_ATTRIB,
0, 0);

while(status > 0)
{
status = kevent(self.kq, &change, 1, &event, 1, NULL);

if(status > 0)
{
if([self.target respondsToSelector:self.action])
[self.target performSelectorOnMainThread:self.action withObject:self waitUntilDone:YES];
}
}

close(self.kq);
close(self.fildes);
}

@end

Aqui está um exemplo de como você usaria essa classe:

FileSystemWatch* watcher = [[FileSystemWatch alloc] init];
[watcher watchFileAtPath:@"path/to/config.plist" target:self action:@selector(fileChanged:)];

...

- (void)fileChanged:(FileSystemWatch*)watcher
{
NSLog(@"File changed, time to reload");
}

Depois de usá-la, você precisa chamar stopWatching antes de lançá-la.

[watcher stopWatching];
[watcher release];

Na prática

Eu tenho usado esse método há um certo tempo em aplicações OS X e iOS. E mesmo não sendo de nenhum uso real no iOS, uma vez que você não consegue modificar os arquivos de configuração enquanto eles estão sendo executados, é muito útil de ter no simulador.

Uma das dificuldades de usar uma aplicação Cocoa Touch é que o caminho para o arquivo que você quer modificar na verdade está no seu diretório de origem, não no diretório do pacote. Então você tem que passá-lo em um caminho absoluto, em vez de em um caminho relativo. Isso se torna um problema quando um projeto está sendo usado por diversas máquinas pessoais, que mantêm o projeto em diferentes locais. Eu ainda estou trabalhando em uma solução para esse problema.

Discussão no Reddit.


Atualização

Mostraram para mim que usar o GDC pode ser uma maneira melhor de esperar por notificações de arquivo de sistema. Eu escrevi um post que discute isso aqui.

?

Texto original disponível em http://www.davidhamrick.com/2011/10/09/kqueue-in-cocoa.html

Matérias especiais e reportagens conduzidas internamente pela Redação iMasters. Acompanhe no Twitter @imasters e no Instagram/Threads @portalimasters

Ver perfil