-
Notifications
You must be signed in to change notification settings - Fork 10.3k
/
service-lock.ts
117 lines (94 loc) · 3.79 KB
/
service-lock.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
/*
* Service lock: handles service discovery for Gatsby develop processes
* The problem: the develop process starts a proxy server, the actual develop process and a websocket server for communication. The two latter ones have random ports that need to be discovered. We also cannot run multiple of the same site at the same time.
* The solution: lockfiles! We create a folder in `.config/gatsby/sites/${sitePathHash} and then for each service write a JSON file with its data (e.g. developstatusserver.json) and lock that file (with developstatusserver.json.lock) so nobody can start the same service again.
*
* NOTE(@mxstbr): This is NOT EXPORTED from the main index.ts due to this relying on Node.js-specific APIs but core-utils also being used in browser environments. See https://github.com/jprichardson/node-fs-extra/issues/743
*/
import path from "path"
import tmp from "tmp"
import fs from "fs-extra"
import xdgBasedir from "xdg-basedir"
import { createContentDigest } from "./create-content-digest"
import { isCI } from "./ci"
const globalConfigPath = xdgBasedir.config || tmp.fileSync().name
const getSiteDir = (programPath: string): string => {
const hash = createContentDigest(programPath)
return path.join(globalConfigPath, `gatsby`, `sites`, hash)
}
const DATA_FILE_EXTENSION = `.json`
const getDataFilePath = (siteDir: string, serviceName: string): string =>
path.join(siteDir, `${serviceName}${DATA_FILE_EXTENSION}`)
const lockfileOptions = {
// Use the minimum stale duration
stale: 5000,
}
export type UnlockFn = () => Promise<void>
// proper-lockfile has a side-effect that we only want to set when needed
function getLockFileInstance(): typeof import("proper-lockfile") {
return import(`proper-lockfile`)
}
const memoryServices = {}
export const createServiceLock = async (
programPath: string,
serviceName: string,
content: Record<string, any>
): Promise<UnlockFn | null> => {
// NOTE(@mxstbr): In CI, we cannot reliably access the global config dir and do not need cross-process coordination anyway
// so we fall back to storing the services in memory instead!
if (isCI()) {
if (memoryServices[serviceName]) return null
memoryServices[serviceName] = content
return async (): Promise<void> => {
delete memoryServices[serviceName]
}
}
const siteDir = getSiteDir(programPath)
await fs.ensureDir(siteDir)
const serviceDataFile = getDataFilePath(siteDir, serviceName)
try {
await fs.writeFile(serviceDataFile, JSON.stringify(content))
const lockfile = await getLockFileInstance()
const unlock = await lockfile.lock(serviceDataFile, lockfileOptions)
return unlock
} catch (err) {
return null
}
}
export const getService = async <T = Record<string, unknown>>(
programPath: string,
serviceName: string,
ignoreLockfile: boolean = false
): Promise<T | null> => {
if (isCI()) return memoryServices[serviceName] || null
const siteDir = getSiteDir(programPath)
const serviceDataFile = getDataFilePath(siteDir, serviceName)
try {
const lockfile = await getLockFileInstance()
if (
ignoreLockfile ||
(await lockfile.check(serviceDataFile, lockfileOptions))
) {
return JSON.parse(
await fs.readFile(serviceDataFile, `utf8`).catch(() => `null`)
)
}
return null
} catch (err) {
return null
}
}
export const getServices = async (programPath: string): Promise<any> => {
if (isCI()) return memoryServices
const siteDir = getSiteDir(programPath)
const serviceNames = (await fs.readdir(siteDir))
.filter(file => file.endsWith(DATA_FILE_EXTENSION))
.map(file => file.replace(DATA_FILE_EXTENSION, ``))
const services = {}
await Promise.all(
serviceNames.map(async service => {
services[service] = await getService(programPath, service, true)
})
)
return services
}