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 Mode Mode
72 Engine string
73 Shared SharedPaths
74 EnginePaths EnginePaths
75 IPC IPCPaths
76}
77
78// Resolve builds a runtime profile from pure inputs.
79func Resolve(input ResolveInput) (Profile, error) {
80 engine := normalizeEngine(input.Engine)
81 if envValue(input.Env, "ENV") == string(ModeDev) {
82 cwd, err := cwdValue(input.CWD)
83 if err != nil {
84 return Profile{}, fmt.Errorf("resolve cwd: %w", err)
85 }
86 root := filepath.Join(cwd, ".dev", "dumber")
87 engineRoot := filepath.Join(root, "engines", engine)
88 ipcRoot, err := devIPCRoot(input.Env, root, engine)
89 if err != nil {
90 return Profile{}, err
91 }
92 return Profile{
93 Mode: ModeDev,
94 Engine: engine,
95 Shared: SharedPaths{
96 RootDir: root,
97 ConfigDir: filepath.Join(root, "config"),
98 DataDir: filepath.Join(root, "data"),
99 StateDir: filepath.Join(root, "state"),
100 CacheDir: filepath.Join(root, "cache"),
101 LogDir: filepath.Join(root, "logs"),
102 },
103 EnginePaths: EnginePaths{
104 Name: engine,
105 RootDir: engineRoot,
106 RuntimeDir: filepath.Join(engineRoot, "runtime"),
107 LogDir: filepath.Join(engineRoot, "logs"),
108 },
109 IPC: IPCPaths{
110 RuntimeDir: ipcRoot,
111 BrowserLaunchSocket: filepath.Join(ipcRoot, browserLaunchSocketName),
112 },
113 }, nil
114 }
115
116 shared := SharedPaths{
117 ConfigDir: input.Base.ConfigHome,
118 DataDir: input.Base.DataHome,
119 StateDir: input.Base.StateHome,
120 CacheDir: input.Base.CacheHome,
121 LogDir: filepath.Join(input.Base.StateHome, "logs"),
122 }
123 engineRoot := filepath.Join(shared.StateDir, "engines", engine)
124 ipcRoot := filepath.Join(shared.StateDir, "runtime", engine)
125 return Profile{
126 Mode: ModeProd,
127 Engine: engine,
128 Shared: shared,
129 EnginePaths: EnginePaths{
130 Name: engine,
131 RootDir: engineRoot,
132 RuntimeDir: filepath.Join(engineRoot, "runtime"),
133 LogDir: filepath.Join(engineRoot, "logs"),
134 },
135 IPC: IPCPaths{
136 RuntimeDir: ipcRoot,
137 BrowserLaunchSocket: filepath.Join(ipcRoot, browserLaunchSocketName),
138 },
139 }, nil
140}
141
142func devIPCRoot(env func(string) string, root, engine string) (string, error) {
143 rootHash := devIPCRootHash(root)
144 for _, base := range []string{envValue(env, "XDG_RUNTIME_DIR"), envValue(env, "TMPDIR")} {
145 if base == "" {
146 continue
147 }
148 ipcRoot := devIPCRootFromBase(base, rootHash, engine)
149 if len(filepath.Join(ipcRoot, browserLaunchSocketName)) < devIPCSocketPathLimit {
150 return ipcRoot, nil
151 }
152 }
153
154 ipcRoot := filepath.Join(root, "runtime", engine)
155 socketPath := filepath.Join(ipcRoot, browserLaunchSocketName)
156 if len(socketPath) < devIPCSocketPathLimit {
157 return ipcRoot, nil
158 }
159 return "", fmt.Errorf(
160 "browser launch socket path too long: %d bytes; set XDG_RUNTIME_DIR or TMPDIR to a shorter runtime directory",
161 len(socketPath),
162 )
163}
164
165func devIPCRootFromBase(base, rootHash, engine string) string {
166 return filepath.Join(base, "dumber", "dev-"+rootHash, engine)
167}
168
169func devIPCRootHash(root string) string {
170 sum := sha256.Sum256([]byte(root))
171 return hex.EncodeToString(sum[:])[:12]
172}
173
174// normalizeEngine trims and lowercases the runtime engine name.
175// Supported engines are "webkit" and "cef"; any other value falls back to
176// the default "webkit" runtime namespace.
177func normalizeEngine(engine string) string {
178 switch normalized := strings.ToLower(strings.TrimSpace(engine)); normalized {
179 case "", engineWebKit:
180 return engineWebKit
181 case engineCEF:
182 return engineCEF
183 default:
184 return engineWebKit
185 }
186}
187
188func envValue(get func(string) string, key string) string {
189 if get == nil {
190 return ""
191 }
192 return get(key)
193}
194
195func cwdValue(get func() (string, error)) (string, error) {
196 if get == nil {
197 return "", fmt.Errorf("cwd getter is nil")
198 }
199 return get()
200}
201
202// CEFUserDataDir returns the resolved CEF root cache/user-data directory.
203func (p Profile) CEFUserDataDir() string {
204 if p.Mode == ModeDev {
205 return filepath.Join(p.EnginePaths.RootDir, "data")
206 }
207 return filepath.Join(p.Shared.DataDir, "cef_user_data")
208}
209
210// CEFLogFile returns the resolved default CEF runtime log path.
211func (p Profile) CEFLogFile() string {
212 return filepath.Join(p.EnginePaths.LogDir, "cef_runtime.log")
213}
214
215// WebKitDataDir returns the resolved WebKit website data directory.
216func (p Profile) WebKitDataDir() string {
217 if p.Mode == ModeDev {
218 return filepath.Join(p.EnginePaths.RootDir, "data")
219 }
220 return p.Shared.DataDir
221}
222
223// WebKitCacheDir returns the resolved WebKit cache directory.
224func (p Profile) WebKitCacheDir() string {
225 if p.Mode == ModeDev {
226 return filepath.Join(p.EnginePaths.RootDir, "cache")
227 }
228 // Keep the current prod layout under state for non-user-managed technical
229 // engine cache data; this intentionally preserves existing WebKit behavior.
230 return filepath.Join(p.Shared.StateDir, "webkit-cache")
231}