Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions internal/database/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -349,6 +349,11 @@ func (db *Database) initTables() error {
return fmt.Errorf("failed to migrate service name constraint: %w", err)
}

// Add verbose_logging column for build tool verbose/debug output
if err := db.migrateAddVerboseLoggingColumn(); err != nil {
return fmt.Errorf("failed to add verbose_logging column: %w", err)
}

return nil
}

Expand Down Expand Up @@ -943,3 +948,32 @@ func (db *Database) DeleteDockerConfig(profileID string) error {
}
return nil
}

// migrateAddVerboseLoggingColumn adds the verbose_logging column to the services table
func (db *Database) migrateAddVerboseLoggingColumn() error {
// Check if column already exists
var columnExists bool
var sql string
err := db.QueryRow("SELECT sql FROM sqlite_master WHERE type='table' AND name='services'").Scan(&sql)
if err != nil {
return fmt.Errorf("failed to query services table schema: %w", err)
}

columnExists = strings.Contains(sql, "verbose_logging")

if columnExists {
log.Println("[INFO] Column 'verbose_logging' already exists in services table")
return nil
}

log.Println("[INFO] Adding 'verbose_logging' column to services table")

// Add the column with default value of FALSE
_, err = db.Exec(`ALTER TABLE services ADD COLUMN verbose_logging BOOLEAN DEFAULT FALSE`)
if err != nil {
return fmt.Errorf("failed to add verbose_logging column: %w", err)
}

log.Println("[INFO] Successfully added 'verbose_logging' column to services table")
return nil
}
61 changes: 31 additions & 30 deletions internal/models/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,34 +8,35 @@ import (
)

type Service struct {
ID string `json:"id"` // UUID - unique identifier for the service
Name string `json:"name"`
Dir string `json:"dir"`
ExtraEnv string `json:"extraEnv"`
JavaOpts string `json:"javaOpts"`
Status string `json:"status"`
HealthStatus string `json:"healthStatus"`
HealthURL string `json:"healthUrl"`
Port int `json:"port"`
PID int `json:"pid"`
Order int `json:"order"`
LastStarted time.Time `json:"lastStarted"`
Uptime string `json:"uptime"`
Description string `json:"description"`
IsEnabled bool `json:"isEnabled"`
BuildSystem string `json:"buildSystem"` // "maven", "gradle", or "auto"
EnvVars map[string]EnvVar `json:"envVars"`
Cmd *exec.Cmd `json:"-"`
Logs []LogEntry `json:"logs"`
Mutex sync.RWMutex `json:"-"`
CPUPercent float64 `json:"cpuPercent"`
MemoryUsage uint64 `json:"memoryUsage"` // in bytes
MemoryPercent float32 `json:"memoryPercent"`
DiskUsage uint64 `json:"diskUsage"` // in bytes
NetworkRx uint64 `json:"networkRx"` // bytes received
NetworkTx uint64 `json:"networkTx"` // bytes transmitted
Metrics ServiceMetrics `json:"metrics"`
Dependencies []ServiceDependency `json:"dependencies"`
DependentOn []string `json:"dependentOn"` // Services that depend on this one
StartupDelay time.Duration `json:"startupDelay"` // Delay before starting after dependencies
ID string `json:"id"` // UUID - unique identifier for the service
Name string `json:"name"`
Dir string `json:"dir"`
ExtraEnv string `json:"extraEnv"`
JavaOpts string `json:"javaOpts"`
Status string `json:"status"`
HealthStatus string `json:"healthStatus"`
HealthURL string `json:"healthUrl"`
Port int `json:"port"`
PID int `json:"pid"`
Order int `json:"order"`
LastStarted time.Time `json:"lastStarted"`
Uptime string `json:"uptime"`
Description string `json:"description"`
IsEnabled bool `json:"isEnabled"`
BuildSystem string `json:"buildSystem"` // "maven", "gradle", or "auto"
VerboseLogging bool `json:"verboseLogging"` // Enable verbose/debug logging for build tools
EnvVars map[string]EnvVar `json:"envVars"`
Cmd *exec.Cmd `json:"-"`
Logs []LogEntry `json:"logs"`
Mutex sync.RWMutex `json:"-"`
CPUPercent float64 `json:"cpuPercent"`
MemoryUsage uint64 `json:"memoryUsage"` // in bytes
MemoryPercent float32 `json:"memoryPercent"`
DiskUsage uint64 `json:"diskUsage"` // in bytes
NetworkRx uint64 `json:"networkRx"` // bytes received
NetworkTx uint64 `json:"networkTx"` // bytes transmitted
Metrics ServiceMetrics `json:"metrics"`
Dependencies []ServiceDependency `json:"dependencies"`
DependentOn []string `json:"dependentOn"` // Services that depend on this one
StartupDelay time.Duration `json:"startupDelay"` // Delay before starting after dependencies
}
13 changes: 12 additions & 1 deletion internal/services/buildsystem.go
Original file line number Diff line number Diff line change
Expand Up @@ -107,7 +107,7 @@ func HasGradleWrapper(serviceDir string) bool {
}

// GetStartCommand returns the appropriate start command for the service
func GetStartCommand(serviceDir, buildSystem string, javaOpts string, extraEnv string) (string, error) {
func GetStartCommand(serviceDir, buildSystem string, javaOpts string, extraEnv string, verboseLogging bool) (string, error) {
effectiveBuildSystem := GetEffectiveBuildSystem(serviceDir, buildSystem)
commands := GetBuildSystemCommands(effectiveBuildSystem)

Expand All @@ -124,6 +124,17 @@ func GetStartCommand(serviceDir, buildSystem string, javaOpts string, extraEnv s
baseCommand = commands.Start
}

// Add verbose/debug logging flags if enabled
if verboseLogging {
if effectiveBuildSystem == BuildSystemMaven {
// Maven: use -X for debug output
baseCommand = strings.Replace(baseCommand, "spring-boot:run", "spring-boot:run -X", 1)
} else if effectiveBuildSystem == BuildSystemGradle {
// Gradle: use -i for info level logging
baseCommand = strings.Replace(baseCommand, "bootRun", "bootRun -i", 1)
}
}

// Construct the full command with directory change and environment
var fullCommand string
if extraEnv != "" {
Expand Down
31 changes: 19 additions & 12 deletions internal/services/database.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,29 +46,31 @@ func (sm *Manager) loadServices(config models.Config) error {
// Try to load existing service from database
var dbService models.Service
row := sm.db.QueryRow(`
SELECT id, name, dir, extra_env, java_opts, status, health_status, health_url, port, pid, service_order, last_started, description, is_enabled, build_system
SELECT id, name, dir, extra_env, java_opts, status, health_status, health_url, port, pid, service_order, last_started, description, is_enabled, build_system, verbose_logging
FROM services WHERE id = ?`, service.ID)

var description sql.NullString
var isEnabled sql.NullBool
var buildSystem sql.NullString
var verboseLogging sql.NullBool
err := row.Scan(&dbService.ID, &dbService.Name, &dbService.Dir, &dbService.ExtraEnv, &dbService.JavaOpts,
&dbService.Status, &dbService.HealthStatus, &dbService.HealthURL, &dbService.Port,
&dbService.PID, &dbService.Order, &dbService.LastStarted, &description, &isEnabled, &buildSystem)
&dbService.PID, &dbService.Order, &dbService.LastStarted, &description, &isEnabled, &buildSystem, &verboseLogging)

if err == sql.ErrNoRows {
// Service doesn't exist in DB, insert it
_, err = sm.db.Exec(`
INSERT INTO services (id, name, dir, extra_env, java_opts, status, health_status, health_url, port, service_order, description, is_enabled, build_system, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
INSERT INTO services (id, name, dir, extra_env, java_opts, status, health_status, health_url, port, service_order, description, is_enabled, build_system, verbose_logging, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
service.ID, service.Name, service.Dir, service.ExtraEnv, service.JavaOpts, service.Status,
service.HealthStatus, service.HealthURL, service.Port, service.Order, "", true, "auto")
service.HealthStatus, service.HealthURL, service.Port, service.Order, "", true, "auto", false)
if err != nil {
return fmt.Errorf("failed to insert service UUID %s: %w", service.ID, err)
}
service.EnvVars = make(map[string]models.EnvVar)
service.Logs = []models.LogEntry{}
service.BuildSystem = "auto"
service.VerboseLogging = false
sm.services[service.ID] = service
} else if err != nil {
return fmt.Errorf("failed to query service UUID %s: %w", service.ID, err)
Expand Down Expand Up @@ -106,6 +108,11 @@ func (sm *Manager) loadServices(config models.Config) error {
} else {
dbService.BuildSystem = "auto"
}
if verboseLogging.Valid {
dbService.VerboseLogging = verboseLogging.Bool
} else {
dbService.VerboseLogging = false
}

// Load environment variables for this service
dbService.EnvVars = make(map[string]models.EnvVar)
Expand Down Expand Up @@ -368,11 +375,11 @@ func (sm *Manager) loadDynamicServices() error {

func (sm *Manager) insertServiceInDB(service *models.Service) error {
_, err := sm.db.Exec(`
INSERT INTO services (id, name, dir, extra_env, java_opts, status, health_status, health_url, port, service_order, description, is_enabled, build_system, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
INSERT INTO services (id, name, dir, extra_env, java_opts, status, health_status, health_url, port, service_order, description, is_enabled, build_system, verbose_logging, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)`,
service.ID, service.Name, service.Dir, service.ExtraEnv, service.JavaOpts, service.Status,
service.HealthStatus, service.HealthURL, service.Port, service.Order,
service.Description, service.IsEnabled, service.BuildSystem)
service.Description, service.IsEnabled, service.BuildSystem, service.VerboseLogging)

return err
}
Expand Down Expand Up @@ -418,12 +425,12 @@ func (sm *Manager) UpdateServiceInDB(service *models.Service) error {

func (sm *Manager) UpdateServiceConfigInDB(service *models.Service) error {
_, err := sm.db.Exec(`
UPDATE services
SET name = ?, java_opts = ?, health_url = ?, port = ?, service_order = ?, description = ?,
is_enabled = ?, build_system = ?, updated_at = CURRENT_TIMESTAMP
UPDATE services
SET name = ?, java_opts = ?, health_url = ?, port = ?, service_order = ?, description = ?,
is_enabled = ?, build_system = ?, verbose_logging = ?, updated_at = CURRENT_TIMESTAMP
WHERE id = ?`,
service.Name, service.JavaOpts, service.HealthURL, service.Port, service.Order,
service.Description, service.IsEnabled, service.BuildSystem, service.ID)
service.Description, service.IsEnabled, service.BuildSystem, service.VerboseLogging, service.ID)

return err
}
Expand Down
47 changes: 46 additions & 1 deletion internal/services/java_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,18 @@ func DetectJavaEnvironment() *JavaEnvironment {

// Method 1: Check JAVA_HOME environment variable
if javaHome := os.Getenv("JAVA_HOME"); javaHome != "" {
// Check if JAVA_HOME points to asdf shims directory and resolve it
if strings.Contains(javaHome, "/.asdf/shims") {
log.Printf("[DEBUG] JAVA_HOME points to asdf shims, attempting to resolve: %s", javaHome)
// Try to resolve using asdf
cmd := exec.Command("asdf", "which", "java")
if output, err := cmd.Output(); err == nil && len(output) > 0 {
realJavaPath := strings.TrimSpace(string(output))
javaHome = inferJavaHome(realJavaPath)
log.Printf("[DEBUG] Resolved JAVA_HOME from asdf to: %s", javaHome)
}
}

javaPath := filepath.Join(javaHome, "bin", getJavaExecutable())
if isExecutable(javaPath) && isWorkingJava(javaPath) {
env.JavaHome = javaHome
Expand Down Expand Up @@ -198,7 +210,40 @@ func isWorkingJava(javaPath string) bool {
}

func inferJavaHome(javaPath string) string {
// Remove /bin/java to get JAVA_HOME
// Check if this is an asdf shim and resolve it to the real Java path
if strings.Contains(javaPath, "/.asdf/shims/") {
log.Printf("[DEBUG] Detected asdf shim, attempting to resolve actual Java path: %s", javaPath)

// Try to use 'asdf which java' to get the real path
cmd := exec.Command("asdf", "which", "java")
if output, err := cmd.Output(); err == nil && len(output) > 0 {
realJavaPath := strings.TrimSpace(string(output))
log.Printf("[DEBUG] Resolved asdf shim to: %s", realJavaPath)
// Use the real Java path for inference
binDir := filepath.Dir(realJavaPath)
if filepath.Base(binDir) == "bin" {
resolvedHome := filepath.Dir(binDir)
log.Printf("[DEBUG] Inferred JAVA_HOME from resolved path: %s", resolvedHome)
return resolvedHome
}
} else {
log.Printf("[WARN] Failed to resolve asdf shim: %v", err)
}
}

// Check if this is an SDKMAN installation and use the current symlink
if strings.Contains(javaPath, "/.sdkman/candidates/java/") && strings.Contains(javaPath, "/current/") {
log.Printf("[DEBUG] Detected SDKMAN Java installation: %s", javaPath)
// For SDKMAN, we can use the path as-is since 'current' is already resolved
binDir := filepath.Dir(javaPath)
if filepath.Base(binDir) == "bin" {
resolvedHome := filepath.Dir(binDir)
log.Printf("[DEBUG] Using SDKMAN Java home: %s", resolvedHome)
return resolvedHome
}
}

// Standard inference: Remove /bin/java to get JAVA_HOME
binDir := filepath.Dir(javaPath)
if filepath.Base(binDir) == "bin" {
return filepath.Dir(binDir)
Expand Down
Loading