Skip to content
C.W.K.
Lesson 01 of 05 · published

Identity Is Three Facts, Not One Number

~13 min · process-identity, pid, launch-date, bundle-id

Level 0Idle Hands
0 XP0/39 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"A successful hide is owned by its process ID plus launch time and bundle ID, never by PID alone." — Tinker architecture, hide invariant 2

The Number Everyone Reaches For

A process ID looks like the obvious handle for "that app I hid". It is unique among running processes, it is cheap to store, and every API accepts it. The trouble is the word running. A PID identifies a process only while that process lives. When it exits, the number goes back into the pool and a later process can receive it. Between "Tinker hid Eagle" and "Tinker restores Eagle" there may be hours, a sleep, a crash of Eagle, a relaunch. A receipt that says only "PID 4127" could, after enough time, describe some entirely unrelated process.

Why the Bundle ID Is Not Enough Either

The opposite mistake is to own the app rather than the process: "Tinker hid Eagle, so Tinker shows Eagle". But if Eagle quit while hidden and Dad relaunched it, the new Eagle is a different process whose visibility Tinker never touched. Showing it on restore would be acting on a process Tinker has no receipt for — the exact thing the design forbids. The architecture states it as a separate invariant: a quit and relaunch can never make Tinker act on a different process.

Three Facts Together

So Tinker's ApplicationIdentity stores the bundle ID, the PID and the process's launch date, plus a display name for people. The key used in the journal is built from all three. Before any effect — hiding, unhiding, even answering "is it hidden?" — the adapter looks the process up by PID and then checks that it has not terminated, that its bundle ID still matches and that its launch date is exactly the recorded one. If any check fails, the answer is "not this process any more", which is nil, not true or false.

What Counts as an App at All

The snapshot that feeds prepare is narrower than "every process". It skips Tinker's own process, keeps only apps with a regular activation policy (the ones with a Dock icon and a menu bar — not background agents or menu-bar extras), and drops anything without a bundle identifier or launch date, because without those three facts Tinker could not prove ownership later. An app it cannot identify is an app it does not touch.

Code

The identity and its journal key·swift
import Foundation

public struct ApplicationIdentity: Codable, Equatable, Hashable, Sendable {
  public var bundleID: String
  public var pid: Int32
  public var launchedAt: Date
  public var name: String          // for people; not part of the key
  public var key: String {
    "application:\(bundleID):\(pid):\(launchedAt.timeIntervalSince1970)"
  }
}
Matching a recorded identity against the live process·swift
import AppKit

func match(_ identity: ApplicationIdentity) -> NSRunningApplication? {
  guard let app = NSRunningApplication(processIdentifier: identity.pid),
        !app.isTerminated,
        app.bundleIdentifier == identity.bundleID,
        app.launchDate == identity.launchedAt else { return nil }
  return app
}

// hidden(identity) is match(identity)?.isHidden — so a vanished or
// replaced process answers nil: neither visible nor hidden.
Which running processes are candidates at all·swift
func running() -> [ApplicationSnapshot] {
  NSWorkspace.shared.runningApplications.compactMap { app in
    guard app.processIdentifier != ProcessInfo.processInfo.processIdentifier,
          app.activationPolicy == .regular,
          let bundle = app.bundleIdentifier,
          let launched = app.launchDate else { return nil }
    return ApplicationSnapshot(
      identity: .init(bundleID: bundle, pid: app.processIdentifier,
                      launchedAt: launched, name: app.localizedName ?? bundle),
      hidden: app.isHidden)
  }
}

External links

Exercise

Think of another kind of thing a reversible tool might change: a window, a file, a network interface, a Bluetooth device. For one of them, what are the 'three facts' that identify one instance across time? Which single identifier would be the tempting but wrong choice, and what event makes it wrong?
Hint
For a file, the path is the tempting identifier, and a rename or a delete-and-recreate makes it wrong; device and inode plus a modification time or content hash get closer. For a window, a window number can be reused after the window closes. The pattern is always the same: find the identifier that survives the event you care about, and pair it with something that changes when the thing is replaced.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign in — Please sign in to comment.

No comments yet — be the first.