Welcome to the final chapter of our Native Installer tutorial!
In the previous chapter, Symlink-Based Activation, we learned how to instantly switch between software versions using symlinks. We now have a system that can download, install, and activate updates.
But there is one final danger lurking in the shadows: Concurrency.
Imagine you have two terminal windows open.
claude update.claude update.Both processes wake up. Both see an update is available. Both try to download the same file to the same folder at the same time.
This results in a Race Condition. They might overwrite each other's data, corrupt the installation, or crash the application.
To solve this, we need a Lock.
Think of the installation folder as a private meeting room. Only one person (process) can use it at a time.
To enforce this, we put a "Meeting in Progress" sign on the door.
In standard software, a "Lock" is just a file named install.lock. If the file exists, the room is occupied.
But what if Process A crashes? If Process A has a heart attack (crashes) while inside the room, it never takes the sign down. Process B arrives later, sees the sign, and waits... forever. The room is locked by a "ghost."
We improve the sign. Instead of just saying "Occupied," the sign says: "Occupied by Employee #12345."
In computers, every running program has a Process ID (PID).
When Process B arrives:
This is PID-Based Concurrency Locking. It is self-healing.
In our system, a lock file isn't empty. It contains JSON data describing exactly who is holding the lock.
// Example content of a .lock file
{
"pid": 86753, // The ID of the process holding the lock
"version": "1.0.5", // What version they are working on
"acquiredAt": 1678888, // Timestamp
"execPath": "/usr/bin/node"
}
The core of this system is the ability to check if a specific PID is still running.
Node.js allows us to send a "signal" to a process. Signal 0 is special: it doesn't kill the process; it just checks if it exists.
// pidLock.ts
export function isProcessRunning(pid: number): boolean {
try {
// process.kill(pid, 0) throws an error if the PID doesn't exist.
// It does NOT actually kill the process.
process.kill(pid, 0);
return true;
} catch {
return false;
}
}
Explanation:
true), the process is alive. The lock is valid.false), the process is gone. The lock is stale.Before we start an update, we look at the lock file. We don't just check if the file exists; we check if it is active.
// pidLock.ts
export function isLockActive(lockFilePath: string): boolean {
// 1. Read the file
const content = readLockContent(lockFilePath);
if (!content) return false;
// 2. The Critical Check
// Is the specific PID written in the file still alive?
return isProcessRunning(content.pid);
}
Analogy: This is the security guard walking up to the meeting room, reading the name on the door ("Process 86753"), and calling HR to see if that employee still works there.
If the lock is inactive (or doesn't exist), we claim it. We write our own PID into the file.
// pidLock.ts
function writeLockFile(lockFilePath, content) {
const tempPath = `${lockFilePath}.tmp`;
// Write to a temp file first
writeFileSync(tempPath, JSON.stringify(content));
// Atomic rename (Instant switch)
renameSync(tempPath, lockFilePath);
}
Let's visualize exactly what happens when a crash occurs using a diagram.
Every time our application starts, or before we attempt an update, we run a cleanup crew. This ensures that if the user's laptop battery died in the middle of an update yesterday, we don't block updates today.
// pidLock.ts
export function cleanupStaleLocks(locksDir: string) {
const files = getLockFiles(locksDir);
for (const file of files) {
// If the process listed in the file is dead...
if (!isLockActive(file)) {
// ...delete the file.
unlinkSync(file);
console.log(`Cleaned up stale lock: ${file}`);
}
}
}
We wrap this entire logic into a simple helper function called withLock. This allows other parts of our code (like the Installer from Chapter 1) to be safe without worrying about the details.
// Usage in installer.ts
await withLock(versionPath, lockFilePath, async () => {
// This code only runs if we successfully grab the lock!
console.log("I am the only process running.");
await downloadAndInstall(version);
});
If withLock cannot acquire the lock (because a real process is currently updating), it returns false, and we tell the user: "Update currently in progress in another window."
Congratulations! You have navigated the entire architecture of a robust Native Installer.
Let's review the journey:
You now possess the knowledge to build a professional-grade, auto-updating installation system that is resilient, secure, and user-friendly.
Generated by Code IQ