Skip to content

Commit

Permalink
Merge pull request #5 from stigoleg/fix
Browse files Browse the repository at this point in the history
Fix
  • Loading branch information
stigoleg authored Dec 26, 2024
2 parents 90bd759 + eeb5ad7 commit d4ead90
Show file tree
Hide file tree
Showing 17 changed files with 980 additions and 400 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ jobs:
- name: Set up Go
uses: actions/setup-go@v4
with:
go-version: '1.20'
go-version: '1.21'
- name: Build
run: go build ./...
- name: Test
Expand Down
39 changes: 27 additions & 12 deletions cmd/keepalive/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,17 @@ package main

import (
"log"
"os"
"os/signal"
"syscall"

"github.com/stigoleg/keep-alive/internal/config"
"github.com/stigoleg/keep-alive/internal/ui"

tea "github.com/charmbracelet/bubbletea"
)

const appVersion = "1.1.0"
const appVersion = "1.2.0"

func main() {
cfg, err := config.ParseFlags(appVersion)
Expand All @@ -23,27 +26,39 @@ func main() {
}
defer f.Close()

// Set up signal handling
sigChan := make(chan os.Signal, 1)
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)

var model ui.Model
if cfg.Duration > 0 {
model = ui.InitialModelWithDuration(cfg.Duration)
p := tea.NewProgram(
model,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
)
if _, err := p.Run(); err != nil {
log.Fatal(err)
}
return
} else {
model = ui.InitialModel()
}

model = ui.InitialModel()
// Create program with signal handling
p := tea.NewProgram(
model,
tea.WithAltScreen(),
tea.WithMouseCellMotion(),
tea.WithoutSignalHandler(),
)

// Handle signals in a separate goroutine
go func() {
sig := <-sigChan
log.Printf("Received signal: %v", sig)
if model.KeepAlive != nil {
if err := model.KeepAlive.Stop(); err != nil {
log.Printf("Error stopping keep-alive: %v", err)
}
}
p.Kill()
}()

if _, err := p.Run(); err != nil {
log.Fatal(err)
log.Printf("Error running program: %v", err)
os.Exit(1)
}
}
8 changes: 5 additions & 3 deletions go.mod
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
module github.com/stigoleg/keep-alive

go 1.22.0

toolchain go1.23.4
go 1.21.0

require (
github.com/charmbracelet/bubbletea v1.2.4
github.com/charmbracelet/lipgloss v1.0.0
github.com/stretchr/testify v1.10.0
golang.org/x/sys v0.28.0
)

require (
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
github.com/charmbracelet/x/ansi v0.4.5 // indirect
github.com/charmbracelet/x/term v0.2.1 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -22,7 +22,9 @@ require (
github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect
github.com/muesli/cancelreader v0.2.2 // indirect
github.com/muesli/termenv v0.15.2 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
golang.org/x/sync v0.9.0 // indirect
golang.org/x/text v0.19.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
10 changes: 10 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ github.com/charmbracelet/x/ansi v0.4.5 h1:LqK4vwBNaXw2AyGIICa5/29Sbdq58GbGdFngSe
github.com/charmbracelet/x/ansi v0.4.5/go.mod h1:dk73KoMTT5AX5BsX0KrqhsTqAnhZZoCBjs7dGWp4Ktw=
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4=
github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand All @@ -24,9 +26,13 @@ github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELU
github.com/muesli/cancelreader v0.2.2/go.mod h1:3XuTXfFS2VjM+HTLZY9Ak0l6eUKfijIfMUZ4EgX0QYo=
github.com/muesli/termenv v0.15.2 h1:GohcuySI0QmI3wN8Ok9PtKGkgkFIk7y6Vpb5PvrY+Wo=
github.com/muesli/termenv v0.15.2/go.mod h1:Epx+iuz8sNs7mNKhxzH4fWXGNpZwUaJKRS1noLXviQ8=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ=
golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
Expand All @@ -35,3 +41,7 @@ golang.org/x/sys v0.28.0 h1:Fksou7UEQUWlKvIdsqzJmUmCX3cZuD2+P3XyyzwMhlA=
golang.org/x/sys v0.28.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
97 changes: 97 additions & 0 deletions internal/integration/integration_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
package integration

import (
"context"
"os/exec"
"runtime"
"testing"
"time"

"github.com/stigoleg/keep-alive/internal/keepalive"
"github.com/stigoleg/keep-alive/internal/platform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestKeepAliveIntegration verifies the integration between the keeper and platform layers
func TestKeepAliveIntegration(t *testing.T) {
tests := []struct {
name string
duration time.Duration
}{
{"short_duration", 2 * time.Second},
{"medium_duration", 5 * time.Second},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
keeper := &keepalive.Keeper{}
err := keeper.StartTimed(tt.duration)
require.NoError(t, err, "keeper should start without error")

// Verify the keeper is running
assert.True(t, keeper.IsRunning(), "keeper should be running")
assert.Greater(t, keeper.TimeRemaining(), time.Duration(0), "time remaining should be positive")

// Let it run for a short duration
time.Sleep(time.Second)

// Verify it's still running
assert.True(t, keeper.IsRunning(), "keeper should still be running")

// Stop the keeper
err = keeper.Stop()
require.NoError(t, err, "keeper should stop without error")
assert.False(t, keeper.IsRunning(), "keeper should not be running after stop")
})
}
}

// TestPlatformSpecificBehavior verifies platform-specific implementations
func TestPlatformSpecificBehavior(t *testing.T) {
ka, err := platform.NewKeepAlive()
require.NoError(t, err, "should create platform-specific keep-alive")

// Start the keep-alive
err = ka.Start(context.Background())
require.NoError(t, err, "should start without error")

// Platform-specific verification
switch runtime.GOOS {
case "darwin":
assertDarwinBehavior(t)
case "windows":
assertWindowsBehavior(t)
case "linux":
assertLinuxBehavior(t)
}

// Stop and verify cleanup
err = ka.Stop()
require.NoError(t, err, "should stop without error")
}

func assertDarwinBehavior(t *testing.T) {
cmd := exec.Command("pmset", "-g", "assertions")
output, err := cmd.Output()
require.NoError(t, err, "should get power management assertions")
assert.Contains(t, string(output), "PreventUserIdleSystemSleep")
}

func assertWindowsBehavior(t *testing.T) {
cmd := exec.Command("powercfg", "/requests")
output, err := cmd.Output()
require.NoError(t, err, "should get power requests")
assert.Contains(t, string(output), "keep-alive")
}

func assertLinuxBehavior(t *testing.T) {
cmd := exec.Command("systemctl", "status", "sleep.target")
output, err := cmd.Output()
if err == nil {
assert.Contains(t, string(output), "inactive")
}
}

// execCommand wraps exec.Command for testing
var execCommand = exec.Command
149 changes: 149 additions & 0 deletions internal/integration/system_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,149 @@
package integration

import (
"context"
"os"
"os/exec"
"runtime"
"syscall"
"testing"
"time"

"github.com/stigoleg/keep-alive/internal/keepalive"
"github.com/stigoleg/keep-alive/internal/platform"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

// TestSystemSleepPrevention verifies that the system actually stays awake
func TestSystemSleepPrevention(t *testing.T) {
if testing.Short() {
t.Skip("skipping system test in short mode")
}

ka, err := platform.NewKeepAlive()
require.NoError(t, err, "should create platform-specific keep-alive")

// Start keep-alive
err = ka.Start(context.Background())
require.NoError(t, err, "should start without error")

// Monitor system state for 20 seconds
startTime := time.Now()
for time.Since(startTime) < 20*time.Second {
assertSystemActive(t)
time.Sleep(2 * time.Second)
}

// Stop and verify cleanup
err = ka.Stop()
require.NoError(t, err, "should stop without error")
}

// TestUnexpectedTermination verifies proper cleanup on unexpected termination
func TestUnexpectedTermination(t *testing.T) {
if testing.Short() {
t.Skip("skipping system test in short mode")
}

// Start a separate process
cmd := exec.Command(os.Args[0], "-test.run=TestKeepAliveHelper")
cmd.Env = append(os.Environ(), "TEST_KEEPALIVE_HELPER=1")
err := cmd.Start()
require.NoError(t, err, "helper process should start")

// Let it run for a few seconds
time.Sleep(5 * time.Second)

// Force kill the process
err = cmd.Process.Signal(syscall.SIGKILL)
require.NoError(t, err, "should kill helper process")

// Wait a moment for cleanup
time.Sleep(2 * time.Second)

// Verify system returns to normal state
assertSystemNormal(t)
}

// TestKeepAliveHelper is a helper function for TestUnexpectedTermination
func TestKeepAliveHelper(t *testing.T) {
if os.Getenv("TEST_KEEPALIVE_HELPER") != "1" {
return
}

ka, _ := platform.NewKeepAlive()
ka.Start(context.Background())
select {} // Block forever
}

// TestConcurrentInstances verifies behavior with multiple instances
func TestConcurrentInstances(t *testing.T) {
if testing.Short() {
t.Skip("skipping system test in short mode")
}

// Start multiple instances
instances := make([]*keepalive.Keeper, 3)
for i := range instances {
keeper := &keepalive.Keeper{}
err := keeper.StartTimed(5 * time.Second)
require.NoError(t, err, "keeper %d should start", i)
instances[i] = keeper
defer keeper.Stop()
}

// Let them run concurrently
time.Sleep(3 * time.Second)

// Verify all are running
for i, keeper := range instances {
assert.True(t, keeper.IsRunning(), "keeper %d should be running", i)
}

// Stop them in reverse order
for i := len(instances) - 1; i >= 0; i-- {
require.NoError(t, instances[i].Stop(), "keeper %d should stop", i)
}
}

func assertSystemActive(t *testing.T) {
switch runtime.GOOS {
case "darwin":
cmd := exec.Command("pmset", "-g", "assertions")
output, err := cmd.Output()
require.NoError(t, err)
assert.Contains(t, string(output), "PreventUserIdleSystemSleep")
case "windows":
cmd := exec.Command("powercfg", "/requests")
output, err := cmd.Output()
require.NoError(t, err)
assert.Contains(t, string(output), "keep-alive")
case "linux":
cmd := exec.Command("systemctl", "status", "sleep.target")
output, err := cmd.Output()
if err == nil {
assert.Contains(t, string(output), "inactive")
}
}
}

func assertSystemNormal(t *testing.T) {
switch runtime.GOOS {
case "darwin":
cmd := exec.Command("pmset", "-g", "assertions")
output, err := cmd.Output()
require.NoError(t, err)
assert.NotContains(t, string(output), "keep-alive")
case "windows":
cmd := exec.Command("powercfg", "/requests")
output, err := cmd.Output()
require.NoError(t, err)
assert.NotContains(t, string(output), "keep-alive")
case "linux":
// On Linux, we just verify the process is gone
cmd := exec.Command("pgrep", "-f", "keep-alive")
err := cmd.Run()
assert.Error(t, err, "keep-alive process should not be running")
}
}
Loading

0 comments on commit d4ead90

Please sign in to comment.