Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Working EventLoop by fixing Framework NSString* constant NSDefaultRunLoopMode #56

Merged

Conversation

shanewholloway
Copy link
Contributor

Having implemented asynchronous event loops before, I was investigating why @JeanSebTr's code example in issue #53 was not working. Poking around with PyObjC, the value of NSDefaultRunLoopMode should have been "kCFRunLoopDefaultMode", but was coming back null. Using PyObjC's implementation to troubleshoot, I was able to create a patch for constant resolving in lib/import.js that fixes the problem. It also enables a node-friendly Cocoa application event loop to address issue #53 and #2.

@TooTallNate
Copy link
Owner

Wow this is awesome :)

@TooTallNate
Copy link
Owner

@trevorlinton Any comments? Seems to work for me.

@trevorlinton
Copy link
Collaborator

@TooTallNate
@shanewholloway

I haven't had a chance to dig into it just yet, i'll pull down the request and give it a whirl.

@trevorlinton
Copy link
Collaborator

@TooTallNate @shanewholloway

I'm fine with integrating the changes; doesn't seem to break any of the unit tests (and the core changes seem great).

I'm not really sure how this fixes #53 or #2. Shane, maybe you can provide some direction here? I haven't been able to get an event loop to run with NSApplication or use any UI elements. In other words, YES to the pull, but lets keep #53/#2 open until we can provide directions on running co-habitated NSApp loops with nodobjc.

Here's the test I tried:

var $ = require('../')
  , assert = require('assert')
  , util = require('util')
  , events = require('events')

$.import('Foundation')
$.import('Cocoa')

function EventLoop(start) {
  events.EventEmitter.call(this)

  assert.equal($.NSDefaultRunLoopMode, 'kCFRunLoopDefaultMode')

  if (start) this.start();
  return this
}
util.inherits(EventLoop, events.EventEmitter)

EventLoop.prototype.start = function() {
  this.emit('start')
  return this.schedule(true)
}
EventLoop.prototype.stop = function() {
  this._is_running = false
  this.emit('stop')
  return this
}
EventLoop.prototype._schedule = setTimeout
EventLoop.prototype.schedule = function(runRecurring) {
  if (runRecurring !== undefined)
    this._is_running = runRecurring
  var memento = this._schedule(this.eventLoop.bind(this))
  this.emit('scheduled', memento, this._is_running)
  return this
}

EventLoop.prototype.eventLoop = function(runRecurring) {
  this.eventLoopCore()
  if (this._is_running || runRecurring)
    this.schedule(runRecurring)
  return this
}
EventLoop.prototype.eventLoopCore = function(block) {
  var untilDate = block ? $.NSDate('distantFuture') : null; // or $.NSDate('distantPast') to not block
  var event, app = $.NSApplication('sharedApplication')
  var runLoopPool = $.NSAutoreleasePool('alloc')('init')

  var count = 0;
  try {
    this.emit('eventLoop-enter')
    do {
      this.emit('event-next', count)
      event = app('nextEventMatchingMask',
              $.NSAnyEventMask.toString(), // …grumble… uint64 as string …grumble…
              'untilDate', untilDate,
              'inMode', $.NSDefaultRunLoopMode,
              'dequeue', 1)
      this.emit('event-match', event, app, count)
      if (event) {
        app('sendEvent', event)
        this.emit('event-sent', event, app, count)
      }
      ++count;
    } while (event)
    this.emit('eventLoop-exit', count)
  } catch (err) {
    this.emit('error', err)
    throw err
  } finally {
    runLoopPool('drain')
  }
  return this;
}



var pool = $.NSAutoreleasePool('alloc')('init')

var Obj = $.NSObject.extend('Obj')
  , invokeCount = 0
  , eventLoopCount = 0

var evtLoop = new EventLoop()
evtLoop.on('eventLoop-exit', function(count) { eventLoopCount += count })

/*Obj.addMethod('sel:', 'v@:@', function (self, _cmd, timer) {
  assert.equal('Info', timer('userInfo').toString())
  if (++invokeCount == 5) {
    timer('invalidate');
    evtLoop.stop()
  }
}).register()*/

/*var timer = $.NSTimer('scheduledTimerWithTimeInterval', 0.1
                     ,'target', Obj('alloc')('init')
                     ,'selector', 'sel:'
                     ,'userInfo', $('Info')
                     ,'repeats', 1)*/

//process.on('exit', function () {
//  assert.equal(invokeCount, 5)
//  assert(eventLoopCount > invokeCount, eventLoopCount+' evts > '+invokeCount+' timers')
//})

evtLoop.start();

$.import('Cocoa');

var app = $.NSApplication('sharedApplication'), appcount = 0;
var AppDelegate = $.NSObject.extend('AppDelegate')
AppDelegate.addMethod('applicationDidFinishLaunching:', 'v@:@', function (self, _cmd, notif) {
    setInterval(function() {
        appcount++;
        if(appcount == 5) {
            assert.equal(appcount, 5);
            process.exit(0);
        }
        console.log("Run "+appcount); 
    }, 1000);
    console.log("Run"); 
});
AppDelegate.register();

var delegate = AppDelegate('alloc')('init');
app('setDelegate', delegate);
console.log("Begin");
app('run');
console.log("Error");
process.exit(1);

Let me know if i'm missing something.

@shanewholloway
Copy link
Contributor Author

The code is close; however, app('run') blocks the main thread, preventing Node.js & LibUV event loop magic from happening. I pulled a revised version of your code into gist demo of applicationDidFinishLaunching. Note the call to app('finishLaunching') on line 27 that kicks off the notification captured in your app delegate.

The replacement logic for app('run') is implemented in evtLoop.start() via a setTimeout tail chain – e.g. function eventLoop(){ eventLoopCore(); setTimeout(eventLoop,0) }. Your question also helped identified a re-entry bug I had in schedule(), fixed in v2 of EventLoop.js.

@shanewholloway
Copy link
Contributor Author

As noted in issue #2, I also added EventLoop-power to NodeCocoaHelloWorld.app example, and a fork of cocoa-hello-world2.js gist demoing integration into Cocoa UI application.

TooTallNate added a commit that referenced this pull request Jan 15, 2015
…tring

Working EventLoop by fixing Framework NSString* constant NSDefaultRunLoopMode
@TooTallNate TooTallNate merged commit 2eab617 into TooTallNate:master Jan 15, 2015
@TooTallNate
Copy link
Owner

Merged!

@bernhard-42
Copy link
Contributor

Via npm I currently don't get the Fix "Fixed resolution of Framework constants like NSDefaultRunLoopMode ..." in NodObjC. So to access Cocoa constants I have to patch import.js manually. Do I miss something?

@TooTallNate
Copy link
Owner

@bernhard-42 I'll do a new release to npm today.

@bernhard-42
Copy link
Contributor

@TooTallNate Seems there is still 1.0.0 on npm ...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants