1package runtimeprofile
2
3import (
4 "crypto/sha256"
5 "encoding/hex"
6 "fmt"
7 "path/filepath"
8 "strings"
9)
10
11const (
12 browserLaunchSocketName = "browser-launch.sock"
13 devIPCSocketPathLimit = 104
14 engineWebKit = "webkit"
15 engineCEF = "cef"
16)
17
18// Mode identifies the execution sandbox mode.
19type Mode string
20
21const (
22 ModeProd Mode = "prod"
23 ModeDev Mode = "dev"
24)
25
26// BasePaths holds the already-resolved dumber XDG homes used in prod.
27type BasePaths struct {
28 ConfigHome string
29 DataHome string
30 StateHome string
31 CacheHome string
32}
33
34// ResolveInput contains the external inputs needed to resolve a runtime profile.
35type ResolveInput struct {
36 Env func(string) string
37 CWD func() (string, error)
38 Engine string
39 Base BasePaths
40}
41
42// SharedPaths are shared by the whole runtime sandbox.
43type SharedPaths struct {
44 // RootDir is only populated when the shared sandbox has a single root
45 // directory (for example ENV=dev under .dev/dumber). It is intentionally
46 // empty in prod, where shared paths come from distinct XDG homes.
47 RootDir string
48 ConfigDir string
49 DataDir string
50 StateDir string
51 CacheDir string
52 LogDir string
53}
54
55// EnginePaths are engine-specific technical paths.
56type EnginePaths struct {
57 Name string
58 RootDir string
59 RuntimeDir string
60 LogDir string
61}
62
63// IPCPaths are runtime coordination paths.
64type IPCPaths struct {
65 RuntimeDir string
66 BrowserLaunchSocket string
67}
68
69// Profile is the fully resolved runtime profile for one mode+engine namespace.
70type Profile struct {
71 InstanceRoot string
72 InstanceAppIDSuffix string
73 Mode Mode
74 Engine string
75 Shared SharedPaths
76 EnginePaths EnginePaths
77 IPC IPCPaths
78}
79
80// Resolve builds a runtime profile from pure inputs.
81func Resolve(input ResolveInput) (Profile, error) {
82 engine := normalizeEngine(input.Engine)
83 if envValue(input.Env, "ENV") == string(ModeDev) {
84 cwd, err := cwdValue(input.CWD)
85 if err != nil {
86 return Profile{}, fmt.Errorf("resolve cwd: %w", err)
87 }
88 root := filepath.Join(cwd, ".dev", "dumber")
89 engineRoot := filepath.Join(root, "engines", engine)
90 ipcRoot, err := devIPCRoot(input.Env, root, engine)
91 if err != nil {
92 return Profile{}, err
93 }
94 return Profile{
95 Mode: ModeDev,
96 Engine: engine,
97 Shared: SharedPaths{
98 RootDir: root,
99 ConfigDir: filepath.Join(root, "config"),
100 DataDir: filepath.Join(root, "data"),
101 StateDir: filepath.Join(root, "state"),
102 CacheDir: filepath.Join(root, "cache"),
103 LogDir: filepath.Join(root, "logs"),
104 },
105 EnginePaths: EnginePaths{
106 Name: engine,
107 RootDir: engineRoot,
108 RuntimeDir: filepath.Join(engineRoot, "runtime"),
109 LogDir: filepath.Join(engineRoot, "logs"),
110 },
111 IPC: IPCPaths{
112 RuntimeDir: ipcRoot,
113 BrowserLaunchSocket: filepath.Join(ipcRoot, browserLaunchSocketName),
114 },
115 }, nil
116 }
117
118 shared := SharedPaths{
119 ConfigDir: input.Base.ConfigHome,
120 DataDir: input.Base.DataHome,
121 StateDir: input.Base.StateHome,
122 CacheDir: input.Base.CacheHome,
123 LogDir: filepath.Join(input.Base.StateHome, "logs"),
124 }
125 engineRoot := filepath.Join(shared.StateDir, "engines", engine)
126 ipcRoot := filepath.Join(shared.StateDir, "runtime", engine)
127 return Profile{
128 Mode: ModeProd,
129 Engine: engine,
130 Shared: shared,
131 EnginePaths: EnginePaths{
132 Name: engine,
133 RootDir: engineRoot,
134 RuntimeDir: filepath.Join(engineRoot, "runtime"),
135 LogDir: filepath.Join(engineRoot, "logs"),
136 },
137 IPC: IPCPaths{
138 RuntimeDir: ipcRoot,
139 BrowserLaunchSocket: filepath.Join(ipcRoot, browserLaunchSocketName),
140 },
141 }, nil
142}
143
144func devIPCRoot(env func(string) string, root, engine string) (string, error) {
145 rootHash := devIPCRootHash(root)
146 for _, base := range []string{envValue(env, "XDG_RUNTIME_DIR"), envValue(env, "TMPDIR")} {
147 if base == "" {
148 continue
149 }
150 ipcRoot := devIPCRootFromBase(base, rootHash, engine)
151 if len(filepath.Join(ipcRoot, browserLaunchSocketName)) < devIPCSocketPathLimit {
152 return ipcRoot, nil
153 }
154 }
155
156 ipcRoot := filepath.Join(root, "runtime", engine)
157 socketPath := filepath.Join(ipcRoot, browserLaunchSocketName)
158 if len(socketPath) < devIPCSocketPathLimit {
159 return ipcRoot, nil
160 }
161 return "", fmt.Errorf(
162 "browser launch socket path too long: %d bytes; set XDG_RUNTIME_DIR or TMPDIR to a shorter runtime directory",
163 len(socketPath),
164 )
165}
166
167func devIPCRootFromBase(base, rootHash, engine string) string {
168 return filepath.Join(base, "dumber", "dev-"+rootHash, engine)
169}
170
171func devIPCRootHash(root string) string {
172 sum := sha256.Sum256([]byte(root))
173 return hex.EncodeToString(sum[:])[:12]
174}
175
176// normalizeEngine trims and lowercases the runtime engine name.
177// Supported engines are "webkit" and "cef"; any other value falls back to
178// the default "webkit" runtime namespace.
179func normalizeEngine(engine string) string {
180 switch normalized := strings.ToLower(strings.TrimSpace(engine)); normalized {
181 case "", engineWebKit:
182 return engineWebKit
183 case engineCEF:
184 return engineCEF
185 default:
186 return engineWebKit
187 }
188}
189
190func envValue(get func(string) string, key string) string {
191 if get == nil {
192 return ""
193 }
194 return get(key)
195}
196
197func cwdValue(get func() (string, error)) (string, error) {
198 if get == nil {
199 return "", fmt.Errorf("cwd getter is nil")
200 }
201 return get()
202}
203
204// CEFUserDataDir returns the resolved CEF root cache/user-data directory.
205func (p Profile) CEFUserDataDir() string {
206 if p.InstanceRoot != "" {
207 return filepath.Join(p.InstanceRoot, "cef")
208 }
209 if p.Mode == ModeDev {
210 return filepath.Join(p.EnginePaths.RootDir, "data")
211 }
212 return filepath.Join(p.Shared.DataDir, "cef_user_data")
213}
214
215// CEFLogFile returns the resolved default CEF runtime log path.
216func (p Profile) CEFLogFile() string {
217 return filepath.Join(p.EnginePaths.LogDir, "cef_runtime.log")
218}
219
220// WebKitDataDir returns the resolved WebKit website data directory.
221func (p Profile) WebKitDataDir() string {
222 if p.Mode == ModeDev {
223 return filepath.Join(p.EnginePaths.RootDir, "data")
224 }
225 return p.Shared.DataDir
226}
227
228// WebKitCacheDir returns the resolved WebKit cache directory.
229func (p Profile) WebKitCacheDir() string {
230 if p.Mode == ModeDev {
231 return filepath.Join(p.EnginePaths.RootDir, "cache")
232 }
233 // Keep the current prod layout under state for non-user-managed technical
234 // engine cache data; this intentionally preserves existing WebKit behavior.
235 return filepath.Join(p.Shared.StateDir, "webkit-cache")
236}