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
20 changes: 10 additions & 10 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ curl -sSL https://shelltime.xyz/i | bash

3. **Optional: Enable daemon mode** (recommended for optimal performance):
```bash
sudo shelltime daemon install
shelltime daemon install
```

## Configuration
Expand Down Expand Up @@ -274,20 +274,20 @@ shelltime daemon <subcommand>
```

**Subcommands:**
- `install`: Install the daemon service (requires sudo)
- `uninstall`: Remove the daemon service (requires sudo)
- `reinstall`: Reinstall the daemon service (requires sudo)
- `install`: Install the daemon service
- `uninstall`: Remove the daemon service
- `reinstall`: Reinstall the daemon service

**Examples:**
```bash
# Install daemon for better performance
sudo shelltime daemon install
shelltime daemon install

# Remove daemon service
sudo shelltime daemon uninstall
shelltime daemon uninstall

# Reinstall (useful for updates)
sudo shelltime daemon reinstall
shelltime daemon reinstall
```

#### `shelltime hooks`
Expand Down Expand Up @@ -413,7 +413,7 @@ Default synchronization behavior and expected latencies:
For optimal performance and minimal shell latency, enable daemon mode:

```bash
sudo ~/.shelltime/bin/shelltime daemon install
~/.shelltime/bin/shelltime daemon install
```

**Key Benefits:**
Expand All @@ -423,7 +423,7 @@ sudo ~/.shelltime/bin/shelltime daemon install
- **Resilient Delivery**: Automatic retry and buffering during network issues

**Technical Implementation:**
- Operates as a system-level service
- Operates as a user-level service
- Manages all network synchronization operations
- Implements intelligent command buffering
- Provides automatic retry mechanisms for failed synchronizations
Expand Down Expand Up @@ -500,7 +500,7 @@ ShellTime implements a hybrid RSA/AES-GCM encryption scheme:
Remove the daemon service when no longer needed:

```bash
sudo ~/.shelltime/bin/shelltime daemon uninstall
~/.shelltime/bin/shelltime daemon uninstall
```

**Uninstallation Process:**
Expand Down
27 changes: 9 additions & 18 deletions commands/daemon.install.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package commands
import (
"fmt"
"os"
"os/user"
"path/filepath"

"github.com/gookit/color"
Expand All @@ -17,20 +18,17 @@ var DaemonInstallCommand *cli.Command = &cli.Command{
}

func commandDaemonInstall(c *cli.Context) error {
color.Yellow.Println("⚠️ Warning: This daemon service is currently not ready for use. Please proceed with caution.")

// Check if running as root
if os.Geteuid() != 0 {
return fmt.Errorf("this command must be run as root (sudo shelltime daemon install)")
}
color.Yellow.Println("🔍 Detecting system architecture...")

// TODO: the username is not stable in multiple user system
baseFolder, username, err := model.SudoGetBaseFolder()
// Get current user's home directory and username
currentUser, err := user.Current()
if err != nil {
return err
return fmt.Errorf("failed to get current user: %w", err)
}

baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
username := currentUser.Username

installer, err := model.NewDaemonInstaller(baseFolder, username)
if err != nil {
return err
Expand Down Expand Up @@ -59,15 +57,8 @@ func commandDaemonInstall(c *cli.Context) error {
return nil
}

// Copy to final location
binaryPath := "/usr/local/bin/shelltime-daemon"

if _, err := os.Stat(binaryPath); err != nil {
color.Yellow.Println("🔍 Creating daemon symlink...")
if err := os.Symlink(filepath.Join(baseFolder, "bin/shelltime-daemon"), binaryPath); err != nil {
return fmt.Errorf("failed to create daemon symlink: %w", err)
}
}
// User-level installation - no system-wide symlink needed
color.Yellow.Println("🔍 Setting up user-level daemon installation...")

if err := installer.InstallService(username); err != nil {
return err
Expand Down
4 changes: 2 additions & 2 deletions commands/daemon.reinstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ func commandDaemonReinstall(c *cli.Context) error {
color.Yellow.Println("🔄 Starting daemon service reinstallation...")

// First, uninstall the existing service
color.Yellow.Println("🗑 Uninstalling existing daemon service...")
color.Yellow.Println("🗑 Uninstalling existing daemon service...")
if err := commandDaemonUninstall(c); err != nil {
return err
}
Expand All @@ -28,4 +28,4 @@ func commandDaemonReinstall(c *cli.Context) error {

color.Green.Println("✅ Daemon service has been successfully reinstalled!")
return nil
}
}
27 changes: 10 additions & 17 deletions commands/daemon.uninstall.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@ package commands

import (
"fmt"
"os"
"os/user"
"path/filepath"

"github.com/gookit/color"
"github.com/malamtime/cli/model"
Expand All @@ -16,19 +17,17 @@ var DaemonUninstallCommand = &cli.Command{
}

func commandDaemonUninstall(c *cli.Context) error {
// Check if running as root
if os.Geteuid() != 0 {
return fmt.Errorf("this command must be run as root (sudo shelltime daemon uninstall)")
}

color.Yellow.Println("🔍 Starting daemon service uninstallation...")

// TODO: the username is not stable in multiple user system
baseFolder, username, err := model.SudoGetBaseFolder()
// Get current user's home directory and username
currentUser, err := user.Current()
if err != nil {
return err
return fmt.Errorf("failed to get current user: %w", err)
}

baseFolder := filepath.Join(currentUser.HomeDir, ".shelltime")
username := currentUser.Username

installer, err := model.NewDaemonInstaller(baseFolder, username)
if err != nil {
return err
Expand All @@ -39,14 +38,8 @@ func commandDaemonUninstall(c *cli.Context) error {
return fmt.Errorf("failed to unregister service: %w", err)
}

// Remove symlink from /usr/local/bin
binaryPath := "/usr/local/bin/shelltime-daemon"
if _, err := os.Stat(binaryPath); err == nil {
color.Yellow.Println("🗑 Removing daemon symlink...")
if err := os.Remove(binaryPath); err != nil {
return fmt.Errorf("failed to remove daemon symlink: %w", err)
}
}
// No need to remove system-wide symlink for user-level installation
color.Yellow.Println("🗑 User-level daemon service cleanup completed...")

color.Green.Println("✅ Daemon service has been successfully uninstalled!")
// color.Yellow.Println("ℹ️ Note: Your commands will now be synced to shelltime.xyz on the next login")
Expand Down
70 changes: 54 additions & 16 deletions model/daemon-installer.darwin.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"fmt"
"os"
"os/exec"
"os/user"
"path/filepath"
"text/template"

Expand All @@ -31,7 +32,7 @@ func NewMacDaemonInstaller(baseFolder, user string) *MacDaemonInstaller {
}

func (m *MacDaemonInstaller) Check() error {
cmd := exec.Command("launchctl", "print", "system/"+m.serviceName)
cmd := exec.Command("launchctl", "print", "user/"+fmt.Sprintf("%d", os.Getuid())+"/"+m.serviceName)
if err := cmd.Run(); err == nil {
return nil
}
Expand All @@ -41,13 +42,16 @@ func (m *MacDaemonInstaller) Check() error {
func (m *MacDaemonInstaller) CheckAndStopExistingService() error {
color.Yellow.Println("🔍 Checking if service is running...")

if err := m.Check(); err != nil {
return err
}

color.Yellow.Println("🛑 Stopping existing service...")
if err := exec.Command("launchctl", "unload", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run(); err != nil {
return fmt.Errorf("failed to stop existing service: %w", err)
if err := m.Check(); err == nil {
color.Yellow.Println("🛑 Stopping existing service...")
currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}
Comment on lines +47 to +50
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The call to user.Current() with its error handling is repeated in several methods (CheckAndStopExistingService, RegisterService, StartService, UnregisterService). To avoid this repetition and improve maintainability, consider getting the current user information once when the MacDaemonInstaller is created and storing the *user.User object in the struct. This would involve:

  1. Adding a currentUser *user.User field to the MacDaemonInstaller struct.
  2. Updating NewMacDaemonInstaller to call user.Current() once, store the result in the new field, and return an error if it fails.
  3. Using the cached m.currentUser in all methods that need it, which would remove the repeated calls and error handling.

agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName))
if err := exec.Command("launchctl", "unload", agentPath).Run(); err != nil {
return fmt.Errorf("failed to stop existing service: %w", err)
}
}
return nil
}
Expand All @@ -62,6 +66,12 @@ func (m *MacDaemonInstaller) InstallService(username string) error {
return fmt.Errorf("failed to create daemon directory: %w", err)
}

// Create logs directory if not exists
logsPath := filepath.Join(m.baseFolder, "logs")
if err := os.MkdirAll(logsPath, 0755); err != nil {
return fmt.Errorf("failed to create logs directory: %w", err)
}

plistPath := filepath.Join(daemonPath, fmt.Sprintf("%s.plist", m.serviceName))
if _, err := os.Stat(plistPath); err == nil {
if err := os.Remove(plistPath); err != nil {
Expand All @@ -84,7 +94,19 @@ func (m *MacDaemonInstaller) RegisterService() error {
if m.baseFolder == "" {
return fmt.Errorf("base folder is not set")
}
plistPath := fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)

currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}

// Create LaunchAgents directory if it doesn't exist
launchAgentsDir := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents")
if err := os.MkdirAll(launchAgentsDir, 0755); err != nil {
return fmt.Errorf("failed to create LaunchAgents directory: %w", err)
}

plistPath := filepath.Join(launchAgentsDir, fmt.Sprintf("%s.plist", m.serviceName))
if _, err := os.Stat(plistPath); err != nil {
sourceFile := filepath.Join(m.baseFolder, fmt.Sprintf("daemon/%s.plist", m.serviceName))
if err := os.Symlink(sourceFile, plistPath); err != nil {
Expand All @@ -96,7 +118,14 @@ func (m *MacDaemonInstaller) RegisterService() error {

func (m *MacDaemonInstaller) StartService() error {
color.Yellow.Println("🚀 Starting service...")
if err := exec.Command("launchctl", "load", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run(); err != nil {

currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}

agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName))
if err := exec.Command("launchctl", "load", agentPath).Run(); err != nil {
return fmt.Errorf("failed to start service: %w", err)
}
return nil
Expand All @@ -106,14 +135,22 @@ func (m *MacDaemonInstaller) UnregisterService() error {
if m.baseFolder == "" {
return fmt.Errorf("base folder is not set")
}

currentUser, err := user.Current()
if err != nil {
return fmt.Errorf("failed to get current user: %w", err)
}

agentPath := filepath.Join(currentUser.HomeDir, "Library/LaunchAgents", fmt.Sprintf("%s.plist", m.serviceName))

color.Yellow.Println("🛑 Stopping service if running...")
// Try to stop the service first
_ = exec.Command("launchctl", "unload", fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)).Run()
_ = exec.Command("launchctl", "unload", agentPath).Run()

color.Yellow.Println("🗑 Removing service files...")
// Remove symlink from LaunchDaemons
if err := os.Remove(fmt.Sprintf("/Library/LaunchDaemons/%s.plist", m.serviceName)); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove launch daemon plist: %w", err)
color.Yellow.Println("🗑 Removing service files...")
// Remove symlink from LaunchAgents
if err := os.Remove(agentPath); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove launch agent plist: %w", err)
}

color.Green.Println("✅ Service unregistered successfully")
Expand All @@ -126,7 +163,8 @@ func (m *MacDaemonInstaller) GetDaemonServiceFile(username string) (buf bytes.Bu
return
}
err = tmpl.Execute(&buf, map[string]string{
"UserName": username,
"UserName": username,
"BaseFolder": m.baseFolder,
})
return
}
2 changes: 1 addition & 1 deletion model/daemon-installer.linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ func (l *LinuxDaemonInstaller) UnregisterService() error {
_ = exec.Command("systemctl", "stop", "shelltime").Run()
_ = exec.Command("systemctl", "disable", "shelltime").Run()

color.Yellow.Println("🗑 Removing service files...")
color.Yellow.Println("🗑 Removing service files...")
// Remove symlink from systemd
if err := os.Remove("/etc/systemd/system/shelltime.service"); err != nil && !os.IsNotExist(err) {
return fmt.Errorf("failed to remove systemd service symlink: %w", err)
Expand Down
6 changes: 3 additions & 3 deletions model/sys-desc/xyz.shelltime.daemon.plist
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,16 @@
<string>xyz.shelltime.daemon</string>
<key>ProgramArguments</key>
<array>
<string>/usr/local/bin/shelltime-daemon</string>
<string>{{.BaseFolder}}/bin/shelltime-daemon</string>
</array>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardErrorPath</key>
<string>/var/log/shelltime-daemon.err</string>
<string>{{.BaseFolder}}/logs/shelltime-daemon.err</string>
<key>StandardOutPath</key>
<string>/var/log/shelltime-daemon.log</string>
<string>{{.BaseFolder}}/logs/shelltime-daemon.log</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOSTING_USER</key>
Expand Down
Loading