Skip to content

Commit d0072a4

Browse files
Consolidate user directory bootstrapping into single idempotent function
Previously, directory creation and ownership for users was scattered across handleCreateUser(), generateUserCaddyfile(), ensureUserSocketDirs(), and ensureUserSocketDir() -- each covering a partial subset. The installer also skipped bootstrapping entirely for the admin user. This introduces bootstrapUserEnvironment() which creates everything a user needs (~/apps, ~/.fastcp/run, ~/.tmp/{sessions,uploads,cache, phpmyadmin,wsdl}, config dir, error log) with correct ownership. It runs from handleCreateUser(), before every PHP service start/reload, and as a startup migration for all existing users. Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent dccf60f commit d0072a4

File tree

2 files changed

+74
-107
lines changed

2 files changed

+74
-107
lines changed

internal/agent/handlers.go

Lines changed: 59 additions & 107 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ func (s *Server) runStartupMigrations() {
4343
s.ensurePMAConfig()
4444
s.ensureMySQLTuning()
4545
s.ensureSwap()
46-
s.ensureUserSocketDirs()
46+
s.bootstrapAllUsers()
4747
s.cleanStaleSocketsAndReload()
4848
}
4949

@@ -166,65 +166,28 @@ func (s *Server) ensureServiceFiles() {
166166
}
167167
}
168168

169-
func (s *Server) ensureUserSocketDirs() {
170-
// Check if any old-style sockets exist in the shared dir -- if so, we need to migrate
169+
// bootstrapAllUsers ensures every user that has a config directory also has
170+
// all required runtime directories with correct ownership. Also migrates
171+
// away from the legacy shared-socket layout if still present.
172+
func (s *Server) bootstrapAllUsers() {
173+
// Migrate away from old shared /opt/fastcp/run/php-*.sock layout
171174
oldSockets, _ := filepath.Glob(filepath.Join(fastcpRunDir, "php-*.sock"))
172-
if len(oldSockets) == 0 {
173-
// Also check if any user config dirs exist without the new socket dir
174-
userDirs, _ := filepath.Glob("/opt/fastcp/config/users/*")
175-
needsMigration := false
176-
for _, dir := range userDirs {
177-
username := filepath.Base(dir)
178-
sockDir := userSocketDir(username)
179-
if _, err := os.Stat(sockDir); err != nil {
180-
needsMigration = true
181-
break
182-
}
175+
if len(oldSockets) > 0 {
176+
slog.Info("migrating legacy shared sockets to per-user home directories")
177+
for _, sock := range oldSockets {
178+
os.Remove(sock)
183179
}
184-
if !needsMigration {
185-
return
180+
oldPids, _ := filepath.Glob(filepath.Join(fastcpRunDir, "php-*.pid"))
181+
for _, pid := range oldPids {
182+
os.Remove(pid)
186183
}
184+
exec.Command("pkill", "-f", "frankenphp run --config /opt/fastcp/config/users").Run()
185+
time.Sleep(1 * time.Second)
187186
}
188187

189-
slog.Info("migrating user socket directories to home directories")
190-
191-
// Create ~/.fastcp/run/ for all existing users with config dirs
192188
userDirs, _ := filepath.Glob("/opt/fastcp/config/users/*")
193189
for _, dir := range userDirs {
194-
username := filepath.Base(dir)
195-
u, err := user.Lookup(username)
196-
if err != nil {
197-
continue
198-
}
199-
uid, _ := strconv.Atoi(u.Uid)
200-
gid, _ := strconv.Atoi(u.Gid)
201-
202-
sockDir := userSocketDir(username)
203-
os.MkdirAll(sockDir, 0755)
204-
os.Chown(sockDir, uid, gid)
205-
// Also ensure parent .fastcp dir has correct ownership
206-
os.Chown(filepath.Dir(sockDir), uid, gid)
207-
}
208-
209-
// Remove old sockets so user PHP processes restart with new paths
210-
for _, sock := range oldSockets {
211-
os.Remove(sock)
212-
}
213-
oldPids, _ := filepath.Glob(filepath.Join(fastcpRunDir, "php-*.pid"))
214-
for _, pid := range oldPids {
215-
os.Remove(pid)
216-
}
217-
218-
// Kill old user FrankenPHP processes so they restart with new socket paths
219-
exec.Command("pkill", "-f", "frankenphp run --config /opt/fastcp/config/users").Run()
220-
time.Sleep(1 * time.Second)
221-
222-
// Regenerate Caddyfiles with new socket paths and reload
223-
if err := s.generateCaddyfile(); err != nil {
224-
slog.Error("failed to regenerate Caddyfile during migration", "error", err)
225-
} else {
226-
s.reloadCaddy()
227-
slog.Info("regenerated Caddyfile with new user socket paths")
190+
bootstrapUserEnvironment(filepath.Base(dir))
228191
}
229192
}
230193

@@ -876,17 +839,46 @@ func userSocketPath(username string) string {
876839
return filepath.Join(userSocketDir(username), "php.sock")
877840
}
878841

879-
func ensureUserSocketDir(username string) {
880-
sockDir := userSocketDir(username)
842+
// bootstrapUserEnvironment creates all required directories and fixes
843+
// ownership for a system user. Safe to call repeatedly (idempotent).
844+
func bootstrapUserEnvironment(username string) {
881845
u, err := user.Lookup(username)
882846
if err != nil {
883847
return
884848
}
885849
uid, _ := strconv.Atoi(u.Uid)
886850
gid, _ := strconv.Atoi(u.Gid)
887-
os.MkdirAll(sockDir, 0755)
888-
os.Chown(sockDir, uid, gid)
889-
os.Chown(filepath.Dir(sockDir), uid, gid)
851+
852+
own := func(p string) { os.Chown(p, uid, gid) }
853+
mkown := func(p string, mode os.FileMode) {
854+
os.MkdirAll(p, mode)
855+
own(p)
856+
}
857+
858+
// ~/apps/
859+
mkown(filepath.Join(u.HomeDir, appsDir), 0755)
860+
861+
// ~/.fastcp/ and ~/.fastcp/run/ (socket directory)
862+
fastcpPath := filepath.Join(u.HomeDir, fastcpDir)
863+
mkown(fastcpPath, 0755)
864+
mkown(filepath.Join(fastcpPath, "run"), 0755)
865+
866+
// ~/.tmp/ tree (sessions, uploads, cache, phpmyadmin, wsdl)
867+
tmpDir := filepath.Join(u.HomeDir, ".tmp")
868+
for _, sub := range []string{"", "sessions", "uploads", "cache", "phpmyadmin", "wsdl"} {
869+
mkown(filepath.Join(tmpDir, sub), 0700)
870+
}
871+
872+
// /opt/fastcp/config/users/{username}/
873+
userConfigDir := filepath.Join("/opt/fastcp/config/users", username)
874+
os.MkdirAll(userConfigDir, 0755)
875+
876+
// PHP error log
877+
logPath := fmt.Sprintf("/var/log/fastcp/php-%s-error.log", username)
878+
if _, err := os.Stat(logPath); err != nil {
879+
os.WriteFile(logPath, nil, 0644)
880+
}
881+
own(logPath)
890882
}
891883

892884
func (s *Server) startUserPHP(username string) error {
@@ -1113,7 +1105,7 @@ func (s *Server) generateCaddyfile() error {
11131105

11141106
// Start user's PHP service (skip if suspended)
11151107
if len(sites) > 0 && !isSuspended {
1116-
ensureUserSocketDir(username)
1108+
bootstrapUserEnvironment(username)
11171109
if useSystemd {
11181110
serviceName := fmt.Sprintf("fastcp-php@%s.service", username)
11191111
exec.Command("systemctl", "start", serviceName).Run()
@@ -1286,45 +1278,20 @@ func (s *Server) generateUserCaddyfile(username string, sites []siteInfo) error
12861278
`, matcherName, hostList, matcherName, site.DocumentRoot))
12871279
}
12881280

1289-
// Create per-user temp directory
1281+
// Per-user temp directory paths (directories are created by bootstrapUserEnvironment)
12901282
userTmpDir := filepath.Join(homeBase, username, ".tmp")
12911283
userSessionDir := filepath.Join(userTmpDir, "sessions")
12921284
userUploadDir := filepath.Join(userTmpDir, "uploads")
1293-
userPmaTmpDir := filepath.Join(userTmpDir, "phpmyadmin")
1294-
1295-
// Get user info for ownership
1296-
u, err := user.Lookup(username)
1297-
if err == nil {
1298-
uid, _ := strconv.Atoi(u.Uid)
1299-
gid, _ := strconv.Atoi(u.Gid)
1300-
1301-
for _, dir := range []string{userTmpDir, userSessionDir, userUploadDir, userPmaTmpDir} {
1302-
os.MkdirAll(dir, 0700)
1303-
os.Chown(dir, uid, gid)
1304-
}
1305-
}
1285+
userCacheDir := filepath.Join(userTmpDir, "cache")
1286+
userWsdlDir := filepath.Join(userTmpDir, "wsdl")
13061287

1307-
// Create per-user php.ini with security settings
13081288
var docRoots []string
13091289
for _, site := range sites {
13101290
docRoots = append(docRoots, filepath.Dir(site.DocumentRoot))
13111291
docRoots = append(docRoots, site.DocumentRoot)
13121292
}
1313-
// Include user's own temp directory, NOT shared /tmp
13141293
openBasedir := strings.Join(docRoots, ":") + ":" + userTmpDir + ":/opt/fastcp/phpmyadmin"
13151294

1316-
// Additional cache directories
1317-
userCacheDir := filepath.Join(userTmpDir, "cache")
1318-
userWsdlDir := filepath.Join(userTmpDir, "wsdl")
1319-
os.MkdirAll(userCacheDir, 0700)
1320-
os.MkdirAll(userWsdlDir, 0700)
1321-
if err == nil {
1322-
uid, _ := strconv.Atoi(u.Uid)
1323-
gid, _ := strconv.Atoi(u.Gid)
1324-
os.Chown(userCacheDir, uid, gid)
1325-
os.Chown(userWsdlDir, uid, gid)
1326-
}
1327-
13281295
phpIni := fmt.Sprintf(`; PHP security settings for user: %s
13291296
; Isolation: Each user has their own temp/session/cache directories
13301297
; Generated by FastCP
@@ -1386,7 +1353,7 @@ session.cookie_samesite = Strict
13861353
}
13871354

13881355
// Reload user's FrankenPHP service
1389-
ensureUserSocketDir(username)
1356+
bootstrapUserEnvironment(username)
13901357
serviceName := fmt.Sprintf("fastcp-php@%s.service", username)
13911358
exec.Command("systemctl", "reload-or-restart", serviceName).Run()
13921359

@@ -1876,29 +1843,14 @@ func (s *Server) handleCreateUser(ctx context.Context, params json.RawMessage) (
18761843
return nil, fmt.Errorf("failed to set password: %w: %s", err, output)
18771844
}
18781845

1879-
// Create apps directory
1846+
// Bootstrap all directories and fix ownership
1847+
bootstrapUserEnvironment(req.Username)
1848+
18801849
u, _ := user.Lookup(req.Username)
18811850
uid, _ := strconv.Atoi(u.Uid)
1882-
gid, _ := strconv.Atoi(u.Gid)
1883-
1884-
appsPath := filepath.Join(u.HomeDir, appsDir)
1885-
os.MkdirAll(appsPath, 0755)
1886-
os.Chown(appsPath, uid, gid)
1887-
1888-
// Create .fastcp directory and runtime subdirectory
1889-
fastcpPath := filepath.Join(u.HomeDir, fastcpDir)
1890-
os.MkdirAll(fastcpPath, 0755)
1891-
os.Chown(fastcpPath, uid, gid)
1892-
1893-
runPath := filepath.Join(fastcpPath, "run")
1894-
os.MkdirAll(runPath, 0755)
1895-
os.Chown(runPath, uid, gid)
1896-
1897-
// Create per-user FrankenPHP configuration directory
1898-
userConfigDir := filepath.Join("/opt/fastcp/config/users", req.Username)
1899-
os.MkdirAll(userConfigDir, 0755)
19001851

19011852
// Create user's Caddyfile (initially empty, will be populated when sites are created)
1853+
userConfigDir := filepath.Join("/opt/fastcp/config/users", req.Username)
19021854
userCaddyfile := filepath.Join(userConfigDir, "Caddyfile")
19031855
initialCaddyfile := fmt.Sprintf(`# FrankenPHP config for user: %s
19041856
{

scripts/install.sh

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,6 +401,21 @@ else
401401
log "Updated password for existing 'fastcp' user"
402402
fi
403403

404+
# Bootstrap all required directories for the admin user
405+
FASTCP_HOME=$(eval echo ~fastcp)
406+
mkdir -p "${FASTCP_HOME}/apps"
407+
mkdir -p "${FASTCP_HOME}/.fastcp/run"
408+
mkdir -p "${FASTCP_HOME}/.tmp/sessions"
409+
mkdir -p "${FASTCP_HOME}/.tmp/uploads"
410+
mkdir -p "${FASTCP_HOME}/.tmp/cache"
411+
mkdir -p "${FASTCP_HOME}/.tmp/phpmyadmin"
412+
mkdir -p "${FASTCP_HOME}/.tmp/wsdl"
413+
mkdir -p /opt/fastcp/config/users/fastcp
414+
chown -R fastcp:fastcp "${FASTCP_HOME}/apps" "${FASTCP_HOME}/.fastcp" "${FASTCP_HOME}/.tmp"
415+
touch /var/log/fastcp/php-fastcp-error.log
416+
chown fastcp:fastcp /var/log/fastcp/php-fastcp-error.log
417+
log "Bootstrapped admin user directories"
418+
404419
# Wait for FastCP API to be ready
405420
log "Waiting for FastCP API..."
406421
for i in {1..30}; do

0 commit comments

Comments
 (0)