File size: 7,270 Bytes
6a7089a | 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 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 | // profile_lock.go handles stale Chrome profile lock recovery.
//
// When a container restarts (or Chrome crashes), Chrome's SingletonLock,
// SingletonSocket, and SingletonCookie files may be left behind in the profile
// directory. On next startup Chrome sees these and refuses to launch with
// "The profile appears to be in use by another Chromium process".
//
// This code detects that error, checks whether the owning process is actually
// still running (via PID probe and process listing), and removes the stale
// lock files if it's safe to do so. It retries Chrome startup once after
// clearing the locks.
package bridge
import (
"bytes"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"regexp"
"strings"
)
var chromeProfileProcessLister = findChromeProfileProcesses
var chromePIDIsRunning = isChromePIDRunning
var killChromeProfileProcesses = killProcesses
var isProfileOwnedByRunningPinchtabMock = isProfileOwnedByRunningPinchtab
var chromeSingletonFiles = []string{
"SingletonLock",
"SingletonSocket",
"SingletonCookie",
}
var chromeProfileLockPIDPattern = regexp.MustCompile(`(?:Chromium|Chrome) process \((\d+)\)`)
type chromeProfileProcess struct {
PID string
Command string
}
func isChromeProfileLockError(msg string) bool {
if msg == "" {
return false
}
return strings.Contains(msg, "The profile appears to be in use by another Chromium process") ||
strings.Contains(msg, "The profile appears to be in use by another Chrome process") ||
strings.Contains(msg, "process_singleton")
}
func clearStaleChromeProfileLock(profileDir, errMsg string) (bool, error) {
if strings.TrimSpace(profileDir) == "" {
return false, nil
}
if pid, ok := extractChromeProfileLockPID(errMsg); ok {
running, err := chromePIDIsRunning(pid)
if err != nil {
slog.Warn("failed to probe chrome lock pid; falling back to process listing", "profile", profileDir, "pid", pid, "err", err)
} else if running {
// If we find the PID from the error message is still running,
// check if it's actually managed by another active PinchTab.
if owned, ptPid := isProfileOwnedByRunningPinchtabMock(profileDir); owned {
slog.Warn("chrome profile lock appears active and owned by another pinchtab; leaving singleton files in place", "profile", profileDir, "pid", pid, "pinchtab_pid", ptPid)
return false, nil
}
slog.Warn("chrome profile lock appears active but pinchtab owner is dead; proceeding with stale cleanup", "profile", profileDir, "pid", pid)
}
}
processes, err := chromeProfileProcessLister(profileDir)
if err != nil {
if _, ok := extractChromeProfileLockPID(errMsg); ok {
slog.Warn("profile process listing unavailable; proceeding with stale lock cleanup based on lock pid", "profile", profileDir, "err", err)
} else {
return false, err
}
}
if len(processes) > 0 {
if owned, ptPid := isProfileOwnedByRunningPinchtabMock(profileDir); owned {
pids := make([]string, 0, len(processes))
for _, proc := range processes {
pids = append(pids, proc.PID)
}
slog.Warn("chrome profile lock appears active and owned by another pinchtab; leaving singleton files in place", "profile", profileDir, "pids", strings.Join(pids, ","), "pinchtab_pid", ptPid)
return false, nil
}
// If no other PinchTab owns this profile, we can safely kill the stale Chrome processes.
slog.Warn("chrome profile lock appears active but no pinchtab owner found; killing stale processes", "profile", profileDir)
if err := killChromeProfileProcesses(processes); err != nil {
slog.Error("failed to kill stale chrome processes", "profile", profileDir, "err", err)
return false, nil
}
}
removed := false
for _, name := range chromeSingletonFiles {
path := filepath.Join(profileDir, name)
if _, err := os.Lstat(path); err != nil {
if os.IsNotExist(err) {
continue
}
return false, fmt.Errorf("inspect %s: %w", path, err)
}
if err := os.Remove(path); err != nil && !os.IsNotExist(err) {
return removed, fmt.Errorf("remove %s: %w", path, err)
}
removed = true
}
return removed, nil
}
func isProfileOwnedByRunningPinchtab(profileDir string) (bool, int) {
pidFile := filepath.Join(profileDir, "pinchtab.pid")
data, err := os.ReadFile(pidFile)
if err != nil {
if !os.IsNotExist(err) {
slog.Debug("failed to read pinchtab pid file", "path", pidFile, "err", err)
}
return false, 0
}
var pid int
if _, err := fmt.Sscanf(string(data), "%d", &pid); err != nil {
slog.Debug("failed to parse pinchtab pid file", "path", pidFile, "err", err)
return false, 0
}
if pid == os.Getpid() {
return false, pid // It's us
}
running, err := chromePIDIsRunning(pid)
if err == nil && running {
// Even if the PID is running, check if it's actually a pinchtab process
// to handle PID reuse.
if isPinchTabProcess(pid) {
slog.Debug("profile is owned by another active pinchtab", "profile", profileDir, "pid", pid)
return true, pid
}
slog.Debug("PID in lockfile is running but not a pinchtab process (PID reuse)", "profile", profileDir, "pid", pid)
} else {
slog.Debug("PID in lockfile is not running", "profile", profileDir, "pid", pid, "err", err)
}
return false, 0
}
func AcquireProfileLock(profileDir string) error {
if profileDir == "" {
return nil
}
if err := os.MkdirAll(profileDir, 0755); err != nil {
return fmt.Errorf("mkdir profile dir: %w", err)
}
if owned, pid := isProfileOwnedByRunningPinchtab(profileDir); owned {
return fmt.Errorf("profile %s is already in use by pinchtab process %d", profileDir, pid)
}
pidFile := filepath.Join(profileDir, "pinchtab.pid")
slog.Debug("acquiring profile lock", "profile", profileDir, "pid", os.Getpid())
return os.WriteFile(pidFile, []byte(fmt.Sprintf("%d", os.Getpid())), 0644)
}
func extractChromeProfileLockPID(msg string) (int, bool) {
if msg == "" {
return 0, false
}
match := chromeProfileLockPIDPattern.FindStringSubmatch(msg)
if len(match) != 2 {
return 0, false
}
pid := 0
for _, ch := range match[1] {
pid = pid*10 + int(ch-'0')
}
if pid <= 0 {
return 0, false
}
return pid, true
}
func findChromeProfileProcesses(profileDir string) ([]chromeProfileProcess, error) {
if strings.TrimSpace(profileDir) == "" {
return nil, nil
}
cmd := exec.Command("ps", "-axo", "pid=,args=")
out, err := cmd.Output()
if err != nil {
return nil, fmt.Errorf("list chrome processes: %w", err)
}
return parseChromeProfileProcesses(out, profileDir), nil
}
func parseChromeProfileProcesses(out []byte, profileDir string) []chromeProfileProcess {
if len(out) == 0 || strings.TrimSpace(profileDir) == "" {
return nil
}
needleEquals := "--user-data-dir=" + profileDir
needleSpace := "--user-data-dir " + profileDir
lines := bytes.Split(out, []byte{'\n'})
processes := make([]chromeProfileProcess, 0)
for _, rawLine := range lines {
line := strings.TrimSpace(string(rawLine))
if line == "" || (!strings.Contains(line, needleEquals) && !strings.Contains(line, needleSpace)) {
continue
}
fields := strings.Fields(line)
if len(fields) < 2 {
continue
}
processes = append(processes, chromeProfileProcess{
PID: fields[0],
Command: strings.TrimSpace(strings.TrimPrefix(line, fields[0])),
})
}
return processes
}
|