explain how this all works
parent
3767628a0e
commit
a52ebde1d5
@ -0,0 +1,222 @@
|
|||||||
|
# procmon
|
||||||
|
|
||||||
|
procmon is a small Go program for OSX that watches App launch and terminate
|
||||||
|
events in AppKit. The Go program directly links against the
|
||||||
|
[AppKit](https://developer.apple.com/reference/appkit) framework and uses it to
|
||||||
|
subscribe to the
|
||||||
|
[NSNotificationCenter](https://developer.apple.com/reference/foundation/nsnotificationcenter)
|
||||||
|
notifications generated by the OS when the user launches or terminates an App.
|
||||||
|
The observer itself is writing in Objective-C. The Objective-C observer is
|
||||||
|
accessed through a simple C function that may be accessed by a Go program. The
|
||||||
|
Objective-C observer, upon seeing notifications, invokes a Go function
|
||||||
|
directly, passing control back to our Go program.
|
||||||
|
|
||||||
|
## installation
|
||||||
|
|
||||||
|
Via Go Get: `go get github.com/jordanorelli/procmon`
|
||||||
|
|
||||||
|
You can also clone this package and build it with `go build`. The Go toolchain
|
||||||
|
will invoke cgo transparently on your behalf. There should be no reason to
|
||||||
|
invoke the cgo toolchain manually; that should only be of interest for
|
||||||
|
debugging and learning purposes.
|
||||||
|
|
||||||
|
## construction
|
||||||
|
|
||||||
|
[`procmon.go`](blob/master/procmon.go) is the single Go file of interest to the
|
||||||
|
Go toolchain.
|
||||||
|
|
||||||
|
### triggering the cgo generation and link step
|
||||||
|
|
||||||
|
Accessing cgo requires importing the pseudo-package `C`. It's important to
|
||||||
|
understand that there is no literal `C` package in the Go standard library.
|
||||||
|
Every project that uses cgo generates _its own_ `C` package transparently.
|
||||||
|
|
||||||
|
When invoking `import "C"`, the commend that _immediately_ precedes the import
|
||||||
|
directive contains a set of instructions to feed to cgo, as follows:
|
||||||
|
|
||||||
|
```go
|
||||||
|
/*
|
||||||
|
#cgo CFLAGS: -x objective-c
|
||||||
|
#cgo LDFLAGS: -framework AppKit
|
||||||
|
#include "procmon.h"
|
||||||
|
*/
|
||||||
|
import "C"
|
||||||
|
```
|
||||||
|
|
||||||
|
Any lines starting with `#cgo` indicate compiler directives. These are passed
|
||||||
|
to the cgo tool and are used to invoke the necessary compiler and linker. We
|
||||||
|
use this flags to indicate that we want to invoke the Objective-C compiler and
|
||||||
|
link agains the AppKit framework.
|
||||||
|
|
||||||
|
The other lines in this comment, that is, the lines that do _not_ begin with
|
||||||
|
`#cgo` are passed to the C compiler as if thy were in a C header file. This is
|
||||||
|
where we `#include "procmon.h"`, the C header file for the C code that we want
|
||||||
|
to invoke.
|
||||||
|
|
||||||
|
Down in the Go program's `main` function, we spawn a goroutine to listen on a
|
||||||
|
channel for changes:
|
||||||
|
|
||||||
|
```go
|
||||||
|
go reportChanges()
|
||||||
|
```
|
||||||
|
|
||||||
|
the `reportChanges` function simply reads values off of a channel and prints
|
||||||
|
them:
|
||||||
|
|
||||||
|
```go
|
||||||
|
func reportChanges() {
|
||||||
|
for change := range appChanges {
|
||||||
|
switch change.stateChange {
|
||||||
|
case stateStarted:
|
||||||
|
fmt.Printf("started: %s\n", change.appname)
|
||||||
|
case stateEnded:
|
||||||
|
fmt.Printf("terminated: %s\n", change.appname)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Back in `main`, we invoke the C function defined in our C header file:
|
||||||
|
```go
|
||||||
|
C.MonitorProcesses()
|
||||||
|
```
|
||||||
|
|
||||||
|
The cgo toolchain automatically associated `procmon.c` with our header file
|
||||||
|
`procmon.h` that we imported in our cgo import comment. [The implementation of
|
||||||
|
the `MonitorProcesses` function appears in
|
||||||
|
`procmon.c`](blob/3767628a0e24c6bccd463a2616f7f5226d4e1c9c/procmon.c#L5):
|
||||||
|
|
||||||
|
```obj-c
|
||||||
|
void MonitorProcesses() {
|
||||||
|
[[ProcWatcher shared] startWatching];
|
||||||
|
[[NSRunLoop currentRunLoop] run];
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
This function does two things: it starts by accessing a singleton of our
|
||||||
|
Objective-C class `ProcWatcher` (that's `[ProcWatcher shared]`, which is defined
|
||||||
|
[here](blob/3767628a0e24c6bccd463a2616f7f5226d4e1c9c/ProcWatcher.m#L6)) and
|
||||||
|
invoking its `startWatching` method. This subscribes our `ProcWatcher` instance
|
||||||
|
to OS notifications. We'll take a look at what the notification subscription
|
||||||
|
looks like in a bit.
|
||||||
|
|
||||||
|
#### sidebar: the Run Loop
|
||||||
|
|
||||||
|
After signing up for the notifications, we access the current processes'
|
||||||
|
runloop with `[NSRunLoop currentRunLoop]` and call its
|
||||||
|
[`run`](https://developer.apple.com/reference/foundation/nsrunloop/1412430-run?language=objc)
|
||||||
|
method to run the run loop. There are two reasons why we need to start the
|
||||||
|
runloop. The first has to do with the mechanics of AppKit. NSRunLoop represents
|
||||||
|
the event loop underpinning our notification center. Without the runloop
|
||||||
|
running, the notification center won't ever pick up any notifications. Apple
|
||||||
|
has a wealth of documentation with respect to the mechanics of Run Loops. If
|
||||||
|
you're _extremely curious_ about this part of the project, [this
|
||||||
|
page](https://developer.apple.com/library/content/documentation/Cocoa/Conceptual/Multithreading/RunLoopManagement/RunLoopManagement.html)
|
||||||
|
has some great literature on how the Run Loop is operating inside of AppKit.
|
||||||
|
|
||||||
|
The other reason we invoke the runloop in this way is that calling our
|
||||||
|
runloop's run method blocks until the runloop itself terminates. Since we're
|
||||||
|
invoking the C function from within the Go program's `main` function, we're
|
||||||
|
blocking Go's `main` function, thus preventing `main` from returning. If `main`
|
||||||
|
returns in the Go program, the Go runtime ends the process, which is _not_ what
|
||||||
|
we want. So this call gives us two things: it sets up the notification system
|
||||||
|
infrastructure, and it prevents our program from terminating.
|
||||||
|
|
||||||
|
#### back to observing NSNotifications
|
||||||
|
|
||||||
|
[The `startWatching`
|
||||||
|
method](blob/3767628a0e24c6bccd463a2616f7f5226d4e1c9c/ProcWatcher.m#L6)
|
||||||
|
accesses the current OSX user's
|
||||||
|
[`NSWorkspace`](https://developer.apple.com/reference/appkit/nsworkspace). The
|
||||||
|
`NSWorkspace` handle allows us to hook into
|
||||||
|
[`NSNotificationCenter`](https://developer.apple.com/reference/foundation/nsnotificationcenter)
|
||||||
|
to subscribe to notifications in the user's workspace. We specifically
|
||||||
|
subscribe to the `NSWorkspaceDidLaunchApplicationNotification` and
|
||||||
|
`NSWorkspaceDidTerminateApplicationNotification` notifications. Here's the
|
||||||
|
subscription to the `NSWorkspaceDidLaunchApplicationNotification` notification,
|
||||||
|
which is signaled by the operating system to inform an observer that an
|
||||||
|
application has been launched by the user:
|
||||||
|
|
||||||
|
```obj-c
|
||||||
|
void (^handleAppLaunch) (NSNotification*) = ^(NSNotification* note) {
|
||||||
|
NSDictionary* info = note.userInfo;
|
||||||
|
NSRunningApplication* app = info[NSWorkspaceApplicationKey];
|
||||||
|
NSString* bundleId = app.bundleIdentifier;
|
||||||
|
|
||||||
|
AppStarted((GoString){bundleId.UTF8String, bundleId.length});
|
||||||
|
};
|
||||||
|
|
||||||
|
id observerLaunch = [notifications
|
||||||
|
addObserverForName: NSWorkspaceDidLaunchApplicationNotification
|
||||||
|
object: workspace
|
||||||
|
queue: [NSOperationQueue mainQueue]
|
||||||
|
usingBlock: handleAppLaunch];
|
||||||
|
```
|
||||||
|
|
||||||
|
The notable feature here is: we pass a callback to our notification center, and
|
||||||
|
within that callback, we invoke a curious function: `AppStarted`. That function
|
||||||
|
isn't defined anywhere in our C or Objective-C code: it's defined [in our
|
||||||
|
original Go file
|
||||||
|
`procmon.go`](blob/3767628a0e24c6bccd463a2616f7f5226d4e1c9c/procmon.go#L28-L31):
|
||||||
|
|
||||||
|
```go
|
||||||
|
//export AppStarted
|
||||||
|
func AppStarted(name string) {
|
||||||
|
appChanges <- appStateChange{stateStarted, name}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The `//export AppStarted` line before the definition of the Go function informs
|
||||||
|
cgo that we'd like the function to be exported for use by C with the name
|
||||||
|
AppStarted. I gave it the same name in C and Go but the names don't have to be
|
||||||
|
the same; you could `//export SomethingElse` or even `//export something_else`
|
||||||
|
and invoke it from C as `something_else`.
|
||||||
|
|
||||||
|
Because we're exporting a function for use by C, cgo will generate some
|
||||||
|
bridging code in C that can be imported by our own C code. This allows our own
|
||||||
|
C code to call back into the Go program and invoke Go functions. cgo will
|
||||||
|
silently generate this C header file behind the scenes. That C header file,
|
||||||
|
which is given the totally obvious and well-documented name `_cgo_export.h` is
|
||||||
|
generated when you run `go build` by cgo, used to help compile our C code, and
|
||||||
|
then deleted. You won't notice it getting written and deleted because it goes
|
||||||
|
by so quickly, but it's there, and it's on disk when our C code gets compiled.
|
||||||
|
In order to access those definitions from our C code, our C code has to import
|
||||||
|
this fleeting header file. In this project, that inclusion happens in
|
||||||
|
`ProcWatcher.m`
|
||||||
|
[here](blob/3767628a0e24c6bccd463a2616f7f5226d4e1c9c/ProcWatcher.m#L1), which
|
||||||
|
looks like this:
|
||||||
|
|
||||||
|
```c
|
||||||
|
#include "_cgo_export.h"
|
||||||
|
```
|
||||||
|
|
||||||
|
Any time you access a Go function from C, you almost certainly need to import
|
||||||
|
the `_cgo_export.h` header file. Importing this header file makes the Go
|
||||||
|
function accessible to the Objective-C code _as a C function_ which will
|
||||||
|
automatically cross-call into Go, having the following signature:
|
||||||
|
|
||||||
|
```c
|
||||||
|
void AppStarted(GoString p0);
|
||||||
|
```
|
||||||
|
|
||||||
|
And _that_ is the function that we're invoking in our NSNotification observer
|
||||||
|
when we call this:
|
||||||
|
|
||||||
|
```obj-c
|
||||||
|
AppStarted((GoString){bundleId.UTF8String, bundleId.length});
|
||||||
|
```
|
||||||
|
|
||||||
|
The `GoString` type is used to convert a null-terminated C string into a Go
|
||||||
|
string, which appears as a struct at the C level, having the following
|
||||||
|
definition (and transitive definitions):
|
||||||
|
|
||||||
|
```c
|
||||||
|
typedef struct { const char *p; GoInt n; } GoString;
|
||||||
|
typedef GoInt64 GoInt;
|
||||||
|
typedef long long GoInt64;
|
||||||
|
```
|
||||||
|
|
||||||
|
Anyway, calling that C function invokes the corresponding Go function
|
||||||
|
`AppStarted`, which writes a value onto a channel. That value is read off of
|
||||||
|
the channel by our `reportChanges` goroutine and used to print out the name of
|
||||||
|
the App that had been launched or terminated by the user.
|
Loading…
Reference in New Issue