Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions packages/next/src/server/lib/router-utils/setup-dev-bundler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import type { NextJsHotReloaderInterface } from '../../dev/hot-reloader-types'
import { createDefineEnv } from '../../../build/swc'
import { installBindings } from '../../../build/swc/install-bindings'
import fs from 'fs'
import os from 'os'
import path from 'path'
import qs from 'querystring'
import Watchpack from 'next/dist/compiled/watchpack'
Expand Down Expand Up @@ -218,6 +219,31 @@ async function startWatcher(
)
}

// Create a global lockfile at ~/.next/dev/<port>/lock so external tools can
// enumerate all running dev servers by scanning ~/.next/dev/*/lock.
const globalLockDir = path.join(
os.homedir(),
'.next',
'dev',
String(opts.port)
)
fs.mkdirSync(globalLockDir, { recursive: true })
const globalServerInfo: DevServerInfo = {
pid: process.pid,
port: opts.port,
hostname: 'localhost',
appUrl: `http://localhost:${opts.port}`,
startedAt: Date.now(),
}
const globalLockfile = Lockfile.tryAcquire(
path.join(globalLockDir, 'lock'),
true,
JSON.stringify(globalServerInfo)
)
opts.onDevServerCleanup?.(async () => {
await globalLockfile?.unlock()
})

const validFileMatcher = createValidFileMatcher(
nextConfig.pageExtensions,
appDir
Expand Down
39 changes: 39 additions & 0 deletions test/development/lockfile/lockfile.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { nextTestSetup } from 'e2e-utils'
import execa from 'execa'
import fs from 'fs'
import os from 'os'
import path from 'path'
import stripAnsi from 'strip-ansi'

Expand All @@ -9,6 +10,12 @@ describe('lockfile', () => {
files: __dirname,
})

afterAll(() => {
// Clean up global lockfile created during test
const globalLockDir = path.join(os.homedir(), '.next', 'dev', next.appPort)
fs.rmSync(globalLockDir, { recursive: true, force: true })
})

it('only allows a single instance of `next dev` to run at a time', async () => {
const browser = await next.browser('/')
expect(await browser.elementByCss('p').text()).toBe('Page')
Expand Down Expand Up @@ -67,4 +74,36 @@ describe('lockfile', () => {
await browser.refresh()
expect(await browser.elementByCss('p').text()).toBe('Page')
})

it('creates a global lockfile at ~/.next/dev/<port>/lock with flock held', async () => {
const globalLockfilePath = path.join(
os.homedir(),
'.next',
'dev',
next.appPort,
'lock'
)
expect(fs.existsSync(globalLockfilePath)).toBe(true)

// Verify lockfile content has correct server info
const serverInfo = JSON.parse(fs.readFileSync(globalLockfilePath, 'utf-8'))
expect(serverInfo).toMatchObject({
pid: expect.any(Number),
port: Number(next.appPort),
hostname: 'localhost',
appUrl: expect.stringContaining(next.appPort),
startedAt: expect.any(Number),
})

// Verify the process from the lockfile is actually alive
const isAlive = (() => {
try {
process.kill(serverInfo.pid, 0)
return true
} catch {
return false
}
})()
expect(isAlive).toBe(true)
})
})
Loading