lockbox

password manager
Log | Files | Refs | README | LICENSE

commit 7b35fc4b5fe720dd3e48acc53140f3eaaa892ecc
parent 7a7be64c4577f260441f4ade1f1b86ffd3b31b5b
Author: Sean Enck <sean@ttypty.com>
Date:   Mon, 30 Jun 2025 09:02:44 -0400

clipmgr should really be a utility outside lb

Diffstat:
Mcmd/lb/main.go | 2--
Mcmd/lb/main_test.go | 5+----
Mcmd/lb/tests/expected.log | 4+---
Minternal/app/commands/core.go | 4----
Minternal/app/help/core.go | 7-------
Minternal/app/help/core_test.go | 4++--
Dinternal/app/help/doc/clipmanager.txt | 5-----
Minternal/app/showclip.go | 2+-
Minternal/app/totp.go | 2+-
Minternal/config/toml_test.go | 30+++++++++++++++---------------
Minternal/config/vars.go | 34+---------------------------------
Minternal/config/vars_test.go | 6------
Minternal/platform/clip.go | 97++++++++++++++++---------------------------------------------------------------
Minternal/platform/clip_test.go | 114++++++++++++++++---------------------------------------------------------------
Dinternal/platform/clipmanager.go | 220-------------------------------------------------------------------------------
Dinternal/platform/clipmanager_test.go | 188-------------------------------------------------------------------------------
16 files changed, 63 insertions(+), 661 deletions(-)

diff --git a/cmd/lb/main.go b/cmd/lb/main.go @@ -43,8 +43,6 @@ func handleEarly(command string, args []string) (bool, error) { } fmt.Printf("version: %s\n", vers) return true, nil - case commands.ClipManager, commands.ClipManagerDaemon: - return true, platform.ClipboardManager(args, command == commands.ClipManagerDaemon, platform.DefaultClipboardDaemon{}) } return false, nil } diff --git a/cmd/lb/main_test.go b/cmd/lb/main_test.go @@ -306,10 +306,7 @@ func test(profile string) error { r.section("clipboard") copyFile := filepath.Join(r.testDir, "clip.copy") - pasteFile := filepath.Join(r.testDir, "clip.paste") - c["clip.copy_command"] = fmt.Sprintf("[\"touch\", \"%s\"]", copyFile) - c["clip.paste_command"] = fmt.Sprintf("[\"touch\", \"%s\"]", pasteFile) - c["clip.timeout"] = "3" + c["clip.copy"] = fmt.Sprintf("[\"touch\", \"%s\"]", copyFile) r.writeConfig(c) r.run("", "clip test6/multiline/password") clipPassed := false diff --git a/cmd/lb/tests/expected.log b/cmd/lb/tests/expected.log @@ -322,9 +322,7 @@ test6/multiline/notes test6/multiline/otp test6/multiline/password env -LOCKBOX_CLIP_COPY_COMMAND=[touch testdata/datadir/clip.copy] -LOCKBOX_CLIP_PASTE_COMMAND=[touch testdata/datadir/clip.paste] -LOCKBOX_CLIP_TIMEOUT=3 +LOCKBOX_CLIP_COPY=[touch testdata/datadir/clip.copy] LOCKBOX_JSON_HASH_LENGTH=3 LOCKBOX_JSON_MODE=hash LOCKBOX_STORE=testdata/datadir/pass.kdbx diff --git a/internal/app/commands/core.go b/internal/app/commands/core.go @@ -12,10 +12,6 @@ const ( TOTP = "totp" // Conv handles text conversion of the data store Conv = "conv" - // ClipManager can handle simple clipboard management - ClipManager = "clipmgr" - // ClipManagerDaemon is the backing call to perform clipboard management - ClipManagerDaemon = "clipmgrd" // Clip will copy values to the clipboard Clip = "clip" // Insert adds a value diff --git a/internal/app/help/core.go b/internal/app/help/core.go @@ -38,8 +38,6 @@ type ( HelpConfigCommand string NoColor string ReadOnlyCommands string - ClipManager string - ClipManagerStop string Config struct { Env string Home string @@ -107,9 +105,6 @@ func Usage(verbose bool, exe string) ([]string, error) { results = append(results, subCommand(commands.TOTP, commands.TOTPSeed, isEntry, "show the TOTP seed (only)")) results = append(results, subCommand(commands.TOTP, commands.TOTPShow, isEntry, "show the totp entry")) results = append(results, command(commands.Version, "", "display version information")) - results = append(results, command(commands.ClipManager, "", "run the clipboard manager daemon")) - results = append(results, command(commands.ClipManagerDaemon, "", "clipboard manager daemonized function")) - results = append(results, subCommand(commands.ClipManager, commands.ClipManagerStop, "", "stop a running clipboard manager daemon")) sort.Strings(results) usage := []string{fmt.Sprintf("%s usage:", exe)} if verbose { @@ -122,8 +117,6 @@ func Usage(verbose bool, exe string) ([]string, error) { CompletionsCommand: commands.Completions, HelpCommand: commands.Help, HelpConfigCommand: commands.HelpConfig, - ClipManager: commands.ClipManager, - ClipManagerStop: commands.ClipManagerStop, NoColor: config.NoColorFlag, ReadOnlyCommands: strings.Join(commands.ReadOnly, ", "), } diff --git a/internal/app/help/core_test.go b/internal/app/help/core_test.go @@ -9,11 +9,11 @@ import ( func TestUsage(t *testing.T) { u, _ := help.Usage(false, "lb") - if len(u) != 30 { + if len(u) != 27 { t.Errorf("invalid usage, out of date? %d", len(u)) } u, _ = help.Usage(true, "lb") - if len(u) != 137 { + if len(u) != 128 { t.Errorf("invalid verbose usage, out of date? %d", len(u)) } for _, usage := range u { diff --git a/internal/app/help/doc/clipmanager.txt b/internal/app/help/doc/clipmanager.txt @@ -1,5 +0,0 @@ -The `{{ $.ClipManager }}` functionality allows running a local clipboard -manager for the system that will monitor the clipboard and clear it when -it has been idle for a (configured) period of time. This is specifically -invoked via `{{ $.Executable }} {{ $.ClipManager }}` (and can be killed -by adding `{{ $.ClipManagerStop }}` as an argument). diff --git a/internal/app/showclip.go b/internal/app/showclip.go @@ -19,7 +19,7 @@ func ShowClip(cmd CommandOptions, isShow bool) error { clipboard := platform.Clipboard{} if !isShow { var err error - clipboard, err = platform.NewClipboard(platform.DefaultClipboardLoader{Full: false}) + clipboard, err = platform.NewClipboard(platform.DefaultClipboardLoader{}) if err != nil { return fmt.Errorf("unable to get clipboard: %w", err) } diff --git a/internal/app/totp.go b/internal/app/totp.go @@ -121,7 +121,7 @@ func (args *TOTPArguments) display(opts TOTPOptions) error { } clipboard := platform.Clipboard{} if clipMode { - clipboard, err = platform.NewClipboard(platform.DefaultClipboardLoader{Full: false}) + clipboard, err = platform.NewClipboard(platform.DefaultClipboardLoader{}) if err != nil { return err } diff --git a/internal/config/toml_test.go b/internal/config/toml_test.go @@ -93,7 +93,7 @@ func TestArrayLoad(t *testing.T) { t.Setenv("TEST", "abc") data := `store="xyz" [clip] -copy_command = ["'xyz/$TEST'", "s", 1] +copy = ["'xyz/$TEST'", "s", 1] ` r := strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err == nil || err.Error() != "value is not string in array: 1" { @@ -102,7 +102,7 @@ copy_command = ["'xyz/$TEST'", "s", 1] data = `include = [] store="xyz" [clip] -copy_command = ["'xyz/$TEST'", "s"] +copy = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err != nil { @@ -115,14 +115,14 @@ copy_command = ["'xyz/$TEST'", "s"] if val != "xyz" || !ok { t.Errorf("invalid object: %v", val) } - a, ok := store.GetArray("LOCKBOX_CLIP_COPY_COMMAND") + a, ok := store.GetArray("LOCKBOX_CLIP_COPY") if fmt.Sprintf("%v", a) != "['xyz/abc' s]" || !ok { t.Errorf("invalid object: %v", a) } data = `include = [] store="xyz" [clip] -copy_command = ["'xyz/$TEST'", "s"] +copy = ["'xyz/$TEST'", "s"] ` r = strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err != nil { @@ -135,7 +135,7 @@ copy_command = ["'xyz/$TEST'", "s"] if val != "xyz" || !ok { t.Errorf("invalid object: %v", val) } - a, ok = store.GetArray("LOCKBOX_CLIP_COPY_COMMAND") + a, ok = store.GetArray("LOCKBOX_CLIP_COPY") if fmt.Sprintf("%v", a) != "['xyz/abc' s]" || !ok { t.Errorf("invalid object: %v", val) } @@ -144,16 +144,16 @@ copy_command = ["'xyz/$TEST'", "s"] func TestReadInt(t *testing.T) { store.Clear() data := ` -[clip] -timeout = true +[json] +hash_length = true ` r := strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err == nil || err.Error() != "non-int64 found where expected: true" { t.Errorf("invalid error: %v", err) } data = `include = [] -[clip] -timeout = 1 +[json] +hash_length = 1 ` r = strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err != nil { @@ -162,13 +162,13 @@ timeout = 1 if len(store.List()) != 1 { t.Errorf("invalid store") } - val, ok := store.GetInt64("LOCKBOX_CLIP_TIMEOUT") + val, ok := store.GetInt64("LOCKBOX_JSON_HASH_LENGTH") if val != 1 || !ok { t.Errorf("invalid object: %v", val) } data = `include = [] -[clip] -timeout = -1 +[json] +hash_length = -1 ` r = strings.NewReader(data) if err := config.LoadConfig(r, emptyRead); err == nil || err.Error() != "-1 is negative (not allowed here)" { @@ -251,7 +251,7 @@ func TestDefaultTOMLToLoadFile(t *testing.T) { if err := config.LoadConfigFile(file); err != nil { t.Errorf("invalid error: %v", err) } - if len(store.List()) != 18 { + if len(store.List()) != 15 { t.Errorf("invalid environment after load: %d", len(store.List())) } } @@ -261,7 +261,7 @@ func TestExpands(t *testing.T) { t.Setenv("TEST", "1") data := `include = [] store = "$TEST" -clip.copy_command = ["$TEST", "$TEST"] +clip.copy = ["$TEST", "$TEST"] [totp] otp_format = "$TEST" ` @@ -280,7 +280,7 @@ otp_format = "$TEST" if val != "1" || !ok { t.Errorf("invalid object: %v", val) } - a, ok := store.GetArray("LOCKBOX_CLIP_COPY_COMMAND") + a, ok := store.GetArray("LOCKBOX_CLIP_COPY") if fmt.Sprintf("%v", a) != "[1 1]" || !ok { t.Errorf("invalid object: %v", a) } diff --git a/internal/config/vars.go b/internal/config/vars.go @@ -33,27 +33,6 @@ var ( description: "Enable terminal color feature.", }), }) - // EnvClipTimeout gets the maximum clipboard time - EnvClipTimeout = environmentRegister(EnvironmentInt{ - environmentDefault: newDefaultedEnvironment(120, - environmentBase{ - key: clipCategory + "TIMEOUT", - description: "Override the amount of time before clearing the clipboard (seconds).", - }), - short: "clipboard entry max time", - }) - // EnvClipProcessFile configures the clip manager daemon pidfile - EnvClipProcessFile = environmentRegister(EnvironmentString{ - environmentStrings: environmentStrings{ - environmentDefault: newDefaultedEnvironment("", - environmentBase{ - key: clipCategory + "PIDFILE", - description: "Set the pidfile for clipboard management", - }), - allowed: []string{fileExample}, - flags: []stringsFlags{canExpandFlag}, - }, - }) // EnvJSONHashLength handles the hashing output length EnvJSONHashLength = environmentRegister(EnvironmentInt{ environmentDefault: newDefaultedEnvironment(0, @@ -99,23 +78,12 @@ var ( environmentStrings: environmentStrings{ environmentDefault: newDefaultedEnvironment("", environmentBase{ - key: clipCategory + "COPY_COMMAND", + key: clipCategory + "COPY", description: "Override the detected platform copy command.", }), flags: []stringsFlags{isCommandFlag}, }, }) - // EnvClipPaste allows overriding the clipboard paste command - EnvClipPaste = environmentRegister(EnvironmentArray{ - environmentStrings: environmentStrings{ - environmentDefault: newDefaultedEnvironment("", - environmentBase{ - key: clipCategory + "PASTE_COMMAND", - description: "Override the detected platform paste command.", - }), - flags: []stringsFlags{isCommandFlag}, - }, - }) // EnvTOTPColorBetween handles terminal coloring for TOTP windows (seconds) EnvTOTPColorBetween = environmentRegister(EnvironmentArray{ environmentStrings: environmentStrings{ diff --git a/internal/config/vars_test.go b/internal/config/vars_test.go @@ -61,10 +61,6 @@ func TestFormatTOTP(t *testing.T) { } } -func TestClipboardMax(t *testing.T) { - checkInt(config.EnvClipTimeout, "LOCKBOX_CLIP_TIMEOUT", "clipboard entry max time", 120, false, t) -} - func TestHashLength(t *testing.T) { checkInt(config.EnvJSONHashLength, "LOCKBOX_JSON_HASH_LENGTH", "hash length", 0, true, t) } @@ -122,7 +118,6 @@ func TestUnsetArrays(t *testing.T) { store.Clear() for _, i := range []config.EnvironmentArray{ config.EnvClipCopy, - config.EnvClipPaste, } { val := i.Get() if len(val) != 0 { @@ -160,7 +155,6 @@ func TestEmptyStrings(t *testing.T) { config.EnvStore, config.EnvKeyFile, config.EnvDefaultModTime, - config.EnvClipProcessFile, } { val := v.Get() if val != "" { diff --git a/internal/platform/clip.go b/internal/platform/clip.go @@ -14,22 +14,14 @@ import ( type ( // Clipboard represent system clipboard operations. - Clipboard struct { - PIDFile string - copying []string - pasting []string - MaxTime int64 - } + Clipboard []string // ClipboardLoader handles how the system is detected ClipboardLoader interface { Name() (string, error) Runtime() string - Complete() bool } // DefaultClipboardLoader is the default system detector - DefaultClipboardLoader struct { - Full bool - } + DefaultClipboardLoader struct{} ) // Name will get the uname results @@ -46,101 +38,50 @@ func (l DefaultClipboardLoader) Runtime() string { return runtime.GOOS } -// Complete indicates if the loader needs a full complete -func (l DefaultClipboardLoader) Complete() bool { - return l.Full -} - -func newBoard(copying, pasting []string) (Clipboard, error) { - maximum, err := config.EnvClipTimeout.Get() - if err != nil { - return Clipboard{}, err - } - pid := config.EnvClipProcessFile.Get() - return Clipboard{copying: copying, pasting: pasting, MaxTime: maximum, PIDFile: pid}, nil -} - // NewClipboard creates a new clipboard func NewClipboard(loader ClipboardLoader) (Clipboard, error) { if !config.EnvFeatureClip.Get() { return Clipboard{}, config.NewFeatureError("clip") } - overridePaste := config.EnvClipPaste.Get() overrideCopy := config.EnvClipCopy.Get() - setPaste := len(overridePaste) > 0 - setCopy := len(overrideCopy) > 0 - if setPaste && setCopy { - return newBoard(overrideCopy, overridePaste) - } - if setCopy && !loader.Complete() { - return newBoard(overrideCopy, []string{}) + if len(overrideCopy) > 0 { + return overrideCopy, nil } - var copying []string - var pasting []string switch loader.Runtime() { case "darwin": - copying = []string{"pbcopy"} - pasting = []string{"pbpaste"} + return []string{"pbcopy"}, nil case "linux": name, err := loader.Name() if err != nil { return Clipboard{}, err } if strings.Contains(strings.ToLower(name), "microsoft") { - copying = []string{"clip.exe"} - pasting = []string{"powershell.exe", "-command", "Get-Clipboard"} - } else { - if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) != "" { - copying = []string{"wl-copy"} - pasting = []string{"wl-paste"} - } else { - if strings.TrimSpace(os.Getenv("DISPLAY")) != "" { - copying = []string{"xclip"} - pasting = []string{"xclip", "-o"} - } else { - return Clipboard{}, errors.New("unable to detect linux clipboard") - } - } + return []string{"clip.exe"}, nil + } + if strings.TrimSpace(os.Getenv("WAYLAND_DISPLAY")) != "" { + return []string{"wl-copy"}, nil } + if strings.TrimSpace(os.Getenv("DISPLAY")) != "" { + return []string{"xclip"}, nil + } + return Clipboard{}, errors.New("unable to detect linux clipboard") default: return Clipboard{}, errors.New("clipboard is unavailable") } - if setPaste { - pasting = overridePaste - } - if setCopy { - copying = overrideCopy - } - return newBoard(copying, pasting) } // CopyTo will copy to clipboard, if non-empty will clear later. func (c Clipboard) CopyTo(value string) error { - cmd, args, err := c.Args(true) - if err != nil { - return err - } - pipeTo(cmd, value, args...) - return nil -} - -// Args returns clipboard args for execution. -func (c Clipboard) Args(copying bool) (string, []string, error) { - var using []string - if copying { - using = c.copying - } else { - using = c.pasting - } - if len(using) == 0 { - return "", nil, fmt.Errorf("command is not set (copying? %v)", copying) + if len(c) == 0 { + return errors.New("copy command is not set") } + cmd := c[0] var args []string - if len(using) > 1 { - args = using[1:] + if len(c) > 1 { + args = c[1:] } - return using[0], args, nil + return pipeTo(cmd, value, args...) } func pipeTo(command, value string, args ...string) error { diff --git a/internal/platform/clip_test.go b/internal/platform/clip_test.go @@ -12,7 +12,6 @@ type mockLoader struct { err error name string runtime string - full bool } func (m mockLoader) Name() (string, error) { @@ -23,10 +22,6 @@ func (m mockLoader) Runtime() string { return m.runtime } -func (m mockLoader) Complete() bool { - return m.full -} - func TestDisabled(t *testing.T) { defer store.Clear() store.SetBool("LOCKBOX_FEATURE_CLIP", false) @@ -35,37 +30,10 @@ func TestDisabled(t *testing.T) { } } -func TestMaxTime(t *testing.T) { - store.Clear() - defer store.Clear() - t.Setenv("WAYLAND_DISPLAY", "1") - loader := mockLoader{name: "linux", runtime: "linux"} - c, err := platform.NewClipboard(loader) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c.MaxTime != 120 { - t.Error("invalid default") - } - store.SetInt64("LOCKBOX_CLIP_TIMEOUT", 1) - c, err = platform.NewClipboard(loader) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if c.MaxTime != 1 { - t.Error("invalid default") - } - store.SetInt64("LOCKBOX_CLIP_TIMEOUT", -1) - c, err = platform.NewClipboard(loader) - if err == nil || err.Error() != "clipboard entry max time must be > 0" { - t.Errorf("invalid max time error: %v", err) - } -} - func TestInstance(t *testing.T) { store.Clear() defer store.Clear() - fxn := func(runtime, name, c, p, e string) { + fxn := func(runtime, name, c, e string) { l := mockLoader{runtime: runtime, name: name} b, err := platform.NewClipboard(l) if err != nil { @@ -74,87 +42,49 @@ func TestInstance(t *testing.T) { } return } - cmd, args, _ := b.Args(true) - copying := fmt.Sprintf("%s (%v)", cmd, args) - cmd, args, _ = b.Args(false) - pasting := fmt.Sprintf("%s (%v)", cmd, args) - if copying != c { - t.Errorf("invalid copy: %s != %s", c, copying) - } - if pasting != p { - t.Errorf("invalid copy: %s != %s", p, pasting) + if fmt.Sprintf("%v", b) != c { + t.Errorf("invalid copy: %s != %v", c, b) } } - fxn("darwin", "", "pbcopy ([])", "pbpaste ([])", "") - fxn("linux", "microsoft", "clip.exe ([])", "powershell.exe ([-command Get-Clipboard])", "") - fxn("linux", "linux", "", "", "unable to detect linux clipboard") + fxn("darwin", "", "[pbcopy]", "") + fxn("linux", "microsoft", "[clip.exe]", "") + fxn("linux", "linux", "", "unable to detect linux clipboard") t.Setenv("DISPLAY", "1") t.Setenv("WAYLAND_DISPLAY", "1") - fxn("linux", "linux", "wl-copy ([])", "wl-paste ([])", "") + fxn("linux", "linux", "[wl-copy]", "") t.Setenv("WAYLAND_DISPLAY", "") - fxn("linux", "linux", "xclip ([])", "xclip ([-o])", "") + fxn("linux", "linux", "[xclip]", "") } -func TestFullPartial(t *testing.T) { +func TestCopy(t *testing.T) { store.Clear() defer store.Clear() - store.SetArray("LOCKBOX_CLIP_COPY_COMMAND", []string{"abc", "xyz", "111"}) - if _, err := platform.NewClipboard(mockLoader{}); err != nil { + store.SetArray("LOCKBOX_CLIP_COPY", []string{}) + if _, err := platform.NewClipboard(mockLoader{}); err == nil || err.Error() != "clipboard is unavailable" { t.Errorf("invalid error: %v", err) } - if _, err := platform.NewClipboard(mockLoader{full: true}); err == nil || err.Error() != "clipboard is unavailable" { - t.Errorf("invalid error: %v", err) - } - store.SetArray("LOCKBOX_CLIP_PASTE_COMMAND", []string{"abc", "xyz", "111"}) - if _, err := platform.NewClipboard(mockLoader{full: true}); err != nil { - t.Errorf("invalid error: %v", err) - } -} - -func TestArgsOverride(t *testing.T) { - store.Clear() - defer store.Clear() - store.SetArray("LOCKBOX_CLIP_PASTE_COMMAND", []string{"abc", "xyz", "111"}) + store.SetArray("LOCKBOX_CLIP_COPY", []string{"x"}) c, err := platform.NewClipboard(mockLoader{name: "microsoft", runtime: "linux"}) if err != nil { t.Errorf("invalid error: %v", err) } - cmd, args, err := c.Args(true) - if cmd != "clip.exe" || len(args) != 0 || err != nil { - t.Error("invalid parse") + if fmt.Sprintf("%v", c) != "[x]" { + t.Errorf("invalid override: %v", c) } - cmd, args, err = c.Args(false) - if cmd != "abc" || len(args) != 2 || args[0] != "xyz" || args[1] != "111" || err != nil { - t.Error("invalid parse") - } - store.SetArray("LOCKBOX_CLIP_COPY_COMMAND", []string{"zzz", "lll", "123"}) - c, err = platform.NewClipboard(mockLoader{}) - if err != nil { - t.Errorf("invalid error: %v", err) - } - cmd, args, err = c.Args(true) - if cmd != "zzz" || len(args) != 2 || args[0] != "lll" || args[1] != "123" || err != nil { - t.Error("invalid parse") - } - cmd, args, err = c.Args(false) - if cmd != "abc" || len(args) != 2 || args[0] != "xyz" || args[1] != "111" || err != nil { - t.Error("invalid parse") - } - store.Clear() + store.SetArray("LOCKBOX_CLIP_COPY", []string{"x", "y", "z"}) c, err = platform.NewClipboard(mockLoader{name: "microsoft", runtime: "linux"}) if err != nil { t.Errorf("invalid error: %v", err) } - cmd, args, err = c.Args(true) - if cmd != "clip.exe" || len(args) != 0 || err != nil { - t.Error("invalid parse") - } - cmd, args, err = c.Args(false) - if cmd != "powershell.exe" || len(args) != 2 || args[0] != "-command" || args[1] != "Get-Clipboard" || err != nil { - t.Errorf("invalid parse %s %v", cmd, args) + if fmt.Sprintf("%v", c) != "[x y z]" { + t.Errorf("invalid override: %v", c) } c = platform.Clipboard{} - if _, _, err := c.Args(true); err == nil || err.Error() != "command is not set (copying? true)" { + if err := c.CopyTo(""); err == nil || err.Error() != "copy command is not set" { + t.Errorf("invalid error: %v", err) + } + c = platform.Clipboard{"echo"} + if err := c.CopyTo(""); err != nil { t.Errorf("invalid error: %v", err) } } diff --git a/internal/platform/clipmanager.go b/internal/platform/clipmanager.go @@ -1,220 +0,0 @@ -package platform - -import ( - "crypto/sha256" - "errors" - "fmt" - "os" - "os/exec" - "strconv" - "strings" - "syscall" - "time" - - "git.sr.ht/~enckse/lockbox/internal/app/commands" -) - -type ( - // ClipboardDaemon is the manager interface - ClipboardDaemon interface { - WriteFile(string, string) - ReadFile(string) ([]byte, error) - Output(string, ...string) ([]byte, error) - Start(string, ...string) error - Getpid() int - Copy(Clipboard, string) - Sleep() - Loader() ClipboardLoader - Checkpid(int) error - } - // DefaultClipboardDaemon is the default functioning daemon - DefaultClipboardDaemon struct{} -) - -// WriteFile will write the necessary file to backing filesystem -func (d DefaultClipboardDaemon) WriteFile(file, data string) { - os.WriteFile(file, []byte(data), 0o644) -} - -// ReadFile will read a file from the filesystem -func (d DefaultClipboardDaemon) ReadFile(file string) ([]byte, error) { - return os.ReadFile(file) -} - -// Output will run a command and get output -func (d DefaultClipboardDaemon) Output(cmd string, args ...string) ([]byte, error) { - return exec.Command(cmd, args...).Output() -} - -// Start will start an disconnected execution -func (d DefaultClipboardDaemon) Start(cmd string, args ...string) error { - return exec.Command(cmd, args...).Start() -} - -// Getpid will return the pid -func (d DefaultClipboardDaemon) Getpid() int { - return os.Getpid() -} - -// Copy will copy data to the clipboard -func (d DefaultClipboardDaemon) Copy(c Clipboard, val string) { - c.CopyTo(val) -} - -// Sleep will cause a pause/delay/wait -func (d DefaultClipboardDaemon) Sleep() { - time.Sleep(1 * time.Second) -} - -// Loader will get the backing loader to use -func (d DefaultClipboardDaemon) Loader() ClipboardLoader { - return DefaultClipboardLoader{Full: true} -} - -// Checkpid will check if a pid is still active -func (d DefaultClipboardDaemon) Checkpid(pid int) error { - process, err := os.FindProcess(pid) - if err != nil { - return err - } - return process.Signal(syscall.Signal(0)) -} - -// ClipboardManager handles the daemon runner -func ClipboardManager(args []string, daemon bool, manager ClipboardDaemon) error { - if manager == nil { - return errors.New("manager is nil") - } - clipboard, err := NewClipboard(manager.Loader()) - if err != nil { - return err - } - if clipboard.PIDFile == "" { - return errors.New("pidfile is unset") - } - getProcess := func() (string, error) { - b, err := manager.ReadFile(clipboard.PIDFile) - if err != nil { - return "", err - } - val := strings.TrimSpace(string(b)) - return val, nil - } - if !daemon { - invalid := false - switch len(args) { - case 0: - break - case 1: - invalid = args[0] != commands.ClipManagerStop - if !invalid { - if PathExists(clipboard.PIDFile) { - return os.WriteFile(clipboard.PIDFile, []byte("0"), 0o644) - } - return nil - } - default: - invalid = true - } - if invalid { - return fmt.Errorf("invalid manager arguments: %v", args) - } - if PathExists(clipboard.PIDFile) { - p, err := getProcess() - if err != nil { - return err - } - pid, err := strconv.Atoi(p) - if err != nil { - return err - } - if err := manager.Checkpid(pid); err == nil { - return nil - } - } - return manager.Start(commands.Executable, commands.ClipManagerDaemon) - } - if len(args) > 0 { - return fmt.Errorf("invalid daemon arguments: %v", args) - } - paste, pasteArgs, err := clipboard.Args(false) - if err != nil { - return err - } - pasteFxn := func() (string, error) { - b, err := manager.Output(paste, pasteArgs...) - if err != nil { - return "", err - } - val := strings.TrimSpace(string(b)) - if val == "" { - return "", nil - } - hash := sha256.New() - if _, err := hash.Write([]byte(val)); err != nil { - return "", err - } - return fmt.Sprintf("%x", hash.Sum(nil)), nil - } - pid := strings.TrimSpace(fmt.Sprintf("%d", manager.Getpid())) - isCurrentProcess := func() (bool, error) { - val, err := getProcess() - return val == pid, err - } - manager.WriteFile(clipboard.PIDFile, pid) - var errs []error - for { - if len(errs) > 5 { - return errors.Join(errs...) - } - manager.Sleep() - ok, err := isCurrentProcess() - if err != nil { - errs = append(errs, err) - continue - } - if !ok { - return nil - } - current, err := pasteFxn() - if err != nil { - errs = append(errs, err) - continue - } - if current != "" { - ok, err := wait(current, clipboard, manager, isCurrentProcess, pasteFxn) - if err != nil { - errs = append(errs, err) - continue - } - if !ok { - return nil - } - } - errs = []error{} - } -} - -func wait(val string, clip Clipboard, mgr ClipboardDaemon, isCurrent func() (bool, error), pasteFxn func() (string, error)) (bool, error) { - var count int64 - for count < clip.MaxTime { - ok, err := isCurrent() - if err != nil { - return false, err - } - if !ok { - return false, nil - } - cur, err := pasteFxn() - if err != nil { - return false, err - } - if cur != val { - return true, nil - } - mgr.Sleep() - count++ - } - mgr.Copy(clip, "") - return true, nil -} diff --git a/internal/platform/clipmanager_test.go b/internal/platform/clipmanager_test.go @@ -1,188 +0,0 @@ -package platform_test - -import ( - "errors" - "fmt" - "os" - "path/filepath" - "strings" - "testing" - - "git.sr.ht/~enckse/lockbox/internal/config/store" - "git.sr.ht/~enckse/lockbox/internal/platform" -) - -type mock struct { - err error - file string - data string - cmd string - args []string - pid int - pasted int -} - -func (d *mock) WriteFile(file, data string) { - d.file = file - d.data = data -} - -func (d *mock) ReadFile(file string) ([]byte, error) { - if file == "falsepid" { - return []byte("1"), nil - } - d.file = file - return []byte(d.data), d.err -} - -func (d *mock) Output(cmd string, args ...string) ([]byte, error) { - d.pasted++ - d.cmd = cmd - d.args = args - val := fmt.Sprintf("%d", min(d.pasted, 100)) - return []byte(val), d.err -} - -func (d *mock) Start(cmd string, args ...string) error { - d.cmd = cmd - d.args = args - return d.err -} - -func (d *mock) Getpid() int { - return d.pid -} - -func (d *mock) Copy(_ platform.Clipboard, val string) { - d.err = fmt.Errorf("copied%s: %d", val, d.pasted) -} - -func (d *mock) Sleep() { -} - -func (d *mock) Loader() platform.ClipboardLoader { - return mockLoader{name: "linux", runtime: "linux"} -} - -func (d *mock) Checkpid(_ int) error { - return d.err -} - -func TestErrors(t *testing.T) { - store.Clear() - defer store.Clear() - t.Setenv("WAYLAND_DISPLAY", "1") - if err := platform.ClipboardManager(nil, false, nil); err == nil || err.Error() != "manager is nil" { - t.Errorf("invalid error: %v", err) - } - if err := platform.ClipboardManager(nil, false, &mock{}); err == nil || err.Error() != "pidfile is unset" { - t.Errorf("invalid error: %v", err) - } - store.SetString("LOCKBOX_CLIP_PIDFILE", "a") - if err := platform.ClipboardManager([]string{"x", "y"}, false, &mock{}); err == nil || err.Error() != "invalid manager arguments: [x y]" { - t.Errorf("invalid error: %v", err) - } - if err := platform.ClipboardManager([]string{"x"}, false, &mock{}); err == nil || err.Error() != "invalid manager arguments: [x]" { - t.Errorf("invalid error: %v", err) - } - m := &mock{} - m.err = errors.New("xyz") - if err := platform.ClipboardManager(nil, true, m); err == nil || strings.Count(err.Error(), "xyz") != 6 { - t.Errorf("invalid error: %v", err) - } - if err := platform.ClipboardManager([]string{"x"}, true, m); err == nil || err.Error() != "invalid daemon arguments: [x]" { - t.Errorf("invalid error: %v", err) - } -} - -func TestStartKill(t *testing.T) { - store.Clear() - defer store.Clear() - store.SetString("LOCKBOX_CLIP_PIDFILE", "a") - t.Setenv("WAYLAND_DISPLAY", "1") - m := &mock{} - if err := platform.ClipboardManager([]string{"-kill"}, false, m); err != nil { - t.Errorf("invalid error: %v", err) - } - if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { - t.Errorf("invalid calls: %s %v", m.cmd, m.args) - } - pidFile := "testdata" - os.MkdirAll(pidFile, 0o755) - pidFile = filepath.Join("testdata", "pidfile") - store.SetString("LOCKBOX_CLIP_PIDFILE", pidFile) - os.WriteFile(pidFile, []byte("123"), 0o644) - defer os.Remove(pidFile) - m.cmd = "" - m.args = []string{} - if err := platform.ClipboardManager([]string{"-kill"}, false, m); err != nil { - t.Errorf("invalid error: %v", err) - } - if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { - t.Errorf("invalid calls: %s %v", m.cmd, m.args) - } - b, err := os.ReadFile(pidFile) - if err != nil { - t.Errorf("invalid error: %v", err) - } - if strings.TrimSpace(string(b)) != "0" { - t.Errorf("invalid pid kill: %s", string(b)) - } -} - -func TestStart(t *testing.T) { - store.Clear() - defer store.Clear() - store.SetString("LOCKBOX_CLIP_PIDFILE", "a") - t.Setenv("WAYLAND_DISPLAY", "1") - m := &mock{} - if err := platform.ClipboardManager(nil, false, m); err != nil { - t.Errorf("invalid error: %v", err) - } - if m.cmd != "lb" || fmt.Sprintf("%v", m.args) != "[clipmgrd]" { - t.Errorf("invalid calls: %s %v", m.cmd, m.args) - } - pidFile := "testdata" - os.MkdirAll(pidFile, 0o755) - pidFile = filepath.Join("testdata", "pidfile") - store.SetString("LOCKBOX_CLIP_PIDFILE", pidFile) - os.WriteFile(pidFile, []byte("123"), 0o644) - defer os.Remove(pidFile) - m.cmd = "" - m.args = []string{} - if err := platform.ClipboardManager(nil, false, m); err == nil || !strings.Contains(err.Error(), "Atoi") { - t.Errorf("invalid error: %v", err) - } - m.data = "1234" - m.cmd = "" - m.args = []string{} - if err := platform.ClipboardManager(nil, false, m); err != nil { - t.Errorf("invalid error: %v", err) - } - if m.cmd != "" || fmt.Sprintf("%v", m.args) != "[]" { - t.Errorf("invalid calls: %s %v", m.cmd, m.args) - } -} - -func TestPIDMismatch(t *testing.T) { - store.Clear() - defer store.Clear() - store.SetString("LOCKBOX_CLIP_PIDFILE", "falsepid") - t.Setenv("WAYLAND_DISPLAY", "1") - m := &mock{} - if err := platform.ClipboardManager(nil, true, m); err != nil { - t.Errorf("invalid error: %v", err) - } -} - -func TestChange(t *testing.T) { - store.Clear() - defer store.Clear() - store.SetString("LOCKBOX_CLIP_PIDFILE", "a") - t.Setenv("WAYLAND_DISPLAY", "1") - m := &mock{} - // NOTE: 100 (count before static) + 120 (default timeout) + 1 (caused break of loop) - if err := platform.ClipboardManager(nil, true, m); err == nil || strings.Count(err.Error(), "copied: 221") != 6 { - t.Errorf("invalid error: %v", err) - } -}