unplugin-https-reverse-proxy 4.1.0-beta.2 → 4.1.0-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -4,6 +4,7 @@ import { existsSync } from 'node:fs'
4
4
  import { readFile, writeFile } from 'node:fs/promises'
5
5
  import { platform } from 'node:os'
6
6
  import { join } from 'node:path'
7
+ import * as lockfile from 'proper-lockfile'
7
8
  import { connectExistingHelper, createHelperToken, startPrivilegedHelper } from './launch'
8
9
 
9
10
  export interface SessionScope {
@@ -24,6 +25,8 @@ export interface SessionScope {
24
25
  caddyBin: string
25
26
  /** Caddyfile the helper may load. */
26
27
  caddyConfigPath: string
28
+ /** Shown before the password prompt so the request is not a surprise. */
29
+ reason?: { target: string, https: boolean }
27
30
  log?: (message: string) => void
28
31
  }
29
32
 
@@ -48,26 +51,50 @@ export function sessionSocketPath(runtimeDir: string): string {
48
51
  * Connects to the helper already serving this runtime directory, or starts a
49
52
  * new one. Reuse matters for multi-instance dev servers: the second instance
50
53
  * attaches to the running Caddy instead of asking for a password again.
54
+ *
55
+ * The check-then-start sequence runs under a lock. Two projects starting at the
56
+ * same moment would otherwise both fail to find a socket, both spawn a helper,
57
+ * and the second would delete the first one's socket while it was still
58
+ * serving.
51
59
  */
52
60
  export async function openHelperSession(scope: SessionScope): Promise<OpenedSession> {
53
61
  const socketPath = sessionSocketPath(scope.runtimeDir)
62
+ const release = await acquireStartLock(scope.runtimeDir)
54
63
 
55
- if (existsSync(socketPath) || platform() === 'win32') {
56
- const token = await readToken(scope.runtimeDir)
57
- if (token) {
58
- const reused = await connectExistingHelper(socketPath, token)
59
- if (reused) {
60
- return {
61
- client: reused,
62
- reused: true,
63
- close: async () => {
64
- await reused.close()
65
- },
66
- }
67
- }
68
- }
64
+ try {
65
+ const reused = await tryReuse(socketPath, scope)
66
+ if (reused)
67
+ return reused
68
+
69
+ return await startNew(socketPath, scope)
69
70
  }
71
+ finally {
72
+ release()
73
+ }
74
+ }
75
+
76
+ async function tryReuse(socketPath: string, scope: SessionScope): Promise<OpenedSession | undefined> {
77
+ if (!existsSync(socketPath) && platform() !== 'win32')
78
+ return undefined
79
+
80
+ const token = await readToken(scope.runtimeDir)
81
+ if (!token)
82
+ return undefined
83
+
84
+ const reused = await connectExistingHelper(socketPath, token)
85
+ if (!reused)
86
+ return undefined
70
87
 
88
+ return {
89
+ client: reused,
90
+ reused: true,
91
+ close: async () => {
92
+ await reused.close()
93
+ },
94
+ }
95
+ }
96
+
97
+ async function startNew(socketPath: string, scope: SessionScope): Promise<OpenedSession> {
71
98
  const token = createHelperToken()
72
99
  const started = await startPrivilegedHelper({
73
100
  socketPath,
@@ -81,6 +108,7 @@ export async function openHelperSession(scope: SessionScope): Promise<OpenedSess
81
108
  caddyStoragePath: scope.caddyStoragePath,
82
109
  caddyBin: scope.caddyBin,
83
110
  caddyConfigPath: scope.caddyConfigPath,
111
+ reason: scope.reason,
84
112
  log: scope.log,
85
113
  })
86
114
 
@@ -95,6 +123,30 @@ export async function openHelperSession(scope: SessionScope): Promise<OpenedSess
95
123
  }
96
124
  }
97
125
 
126
+ /**
127
+ * Serialises "is a helper running? if not, start one" across processes.
128
+ *
129
+ * The lock file is separate from the socket: the socket only exists once a
130
+ * helper is listening, while this lock guards the window before that.
131
+ */
132
+ async function acquireStartLock(runtimeDir: string): Promise<() => void> {
133
+ const lockPath = join(runtimeDir, 'helper.start.lock')
134
+ try {
135
+ await writeFile(lockPath, '', { flag: 'a' })
136
+ }
137
+ catch {}
138
+
139
+ const release = await lockfile.lock(lockPath, {
140
+ realpath: false,
141
+ stale: 30_000,
142
+ retries: { retries: 100, minTimeout: 50, maxTimeout: 200 },
143
+ })
144
+
145
+ return () => {
146
+ void release().catch(() => {})
147
+ }
148
+ }
149
+
98
150
  async function readToken(runtimeDir: string): Promise<string | undefined> {
99
151
  try {
100
152
  const content = await readFile(tokenPath(runtimeDir), 'utf8')