openvibers/openvibe.link Commits

v0.37.2: each Node directory has its own Windows sign-in start (#99)

OP OpenVibers committed 10 days ago · 68df66bb4f · 8 files, +106 −14

install --user-session wrote the one Run value 'OpenVibe Node' whatever --home it was for, so installing a
second Node (a test node beside a working robot's, the LOOI team's case on 11-02) replaced the first's sign-in
start. Each --home now has its own value, 'OpenVibe Node (<home>)'. The old single value is moved only when it
starts this same home, and left alone when it starts another. uninstall --user-session now needs --home and
removes that directory's start only. parseRegQuery and legacyRunsHome are plain text, tested everywhere.

Changes

ddiff --git a/STATUS.json b/STATUS.json
iindex f1bcebc..e420345 100644
---- a/STATUS.json
++++ b/STATUS.json
@@@ -5,7 +5,7 @@
"wave": "T15",
"plan": "Plan track T15 (Bot complete) and ADR-043 (Bot: devices, pairing, control and safety); the computer plugin and the job worker serve OpenVibe.Actor and the Codes job runner (tracks T14/T17)",
"kind": "client",
-- "version": "0.37.1",
++ "version": "0.37.2",
"release": "v0.36.0: the ai plugin (0.2.0) passes tools to the machine's own model server and hands back the calls the model asked for, never running one, so an agent on OpenVibe (Actor) can work on a model this machine serves; its capabilities say tools: true (contracts 0.277.0). v0.35.0: the Node applies the roles the network's machines hub gives it at each check-in (codes.worker starts the job worker it was installed with, desks.host and ai.host are checked against what the machine was installed for, an unknown role is ignored, and the local configuration always wins), and its status reports them back; a desk's first browser start in a cold sandbox gets 90 s. v0.34.0: on Windows a killed plugin takes every process it started with it (a Job Object with kill-on-close), as the process group does on Linux and macOS, so a bridge helper cannot keep a robot's link open after the Node stops or restarts its plugin; the Node reads what the coding tools remember (read-only, local, redacted) and `openvibe-node memory` uploads it to your own store; the heartbeat carries the machine's fleet of coding agents; the Codes job runner pin is v0.5.2",
"runtime": "go 1.24 (one static binary per platform); plugins in Python 3; an ESP32 Arduino library",
"database": "none",
ddiff --git a/cmd/openvibe-node/main.go b/cmd/openvibe-node/main.go
iindex 3429ad3..fd91be0 100644
---- a/cmd/openvibe-node/main.go
++++ b/cmd/openvibe-node/main.go
@@@ -193,7 +193,7 @@ func runWithStdin(args []string, stdin io.Reader, stdout, stderr io.Writer) int
err = cmdInstall(g, *svcUser, stdout)
case "uninstall":
if *session {
-- err = cmdUninstallUserSession(stdout)
++ err = cmdUninstallUserSession(g, stdout)
break
}
err = service.Control(serviceOptions(g, ""), "stop")
ddiff --git a/cmd/openvibe-node/session.go b/cmd/openvibe-node/session.go
iindex 9172c16..e2f015f 100644
---- a/cmd/openvibe-node/session.go
++++ b/cmd/openvibe-node/session.go
@@@ -21,6 +21,20 @@ const (
runValueName = "OpenVibe Node"
)
++// runValueFor is the Run value of one Node directory. A machine may run more than one Node at sign-in (a working
++// robot and a test node, 11-02), so each --home has its own value; before v0.37.2 every install wrote the one
++// "OpenVibe Node" value, and installing a second home replaced the first's sign-in start.
++func runValueFor(home string) string { return runValueName + " (" + home + ")" }
++
++// legacyRunsHome says whether a Run value's command line (the old single "OpenVibe Node" value) starts this home:
++// only then is it this Node's to move or remove. A value that starts another home is another Node's and stays.
++func legacyRunsHome(data, home string) bool {
++ if data == "" || home == "" {
++ return false
++ }
++ return strings.Contains(data, " --home "+quoteArg(home)+" ") || strings.HasSuffix(data, " --home "+quoteArg(home))
++}
++
var errNeedHome = errors.New(`--user-session needs --home DIR: the directory you paired with, for example --home "%LOCALAPPDATA%\OpenVibe Node"`)
// userSessionCommand is the command line the Run key starts: the executable, `run`, and the Node's own directory.
@@@ -59,26 +73,61 @@ func cmdInstallUserSession(g *globals, stdout io.Writer) error {
return err
}
line := userSessionCommand(exe, home, g.logLevel)
-- if err := registerUserSession(line); err != nil {
++ if err := registerUserSession(runValueFor(home), line); err != nil {
return err
}
++ // The old single value, if it started this same home, is this Node's: the new value replaces it.
++ if data, ok := readUserSession(runValueName); ok && legacyRunsHome(data, home) {
++ _ = unregisterUserSession(runValueName)
++ }
fmt.Fprintln(stdout, "The Node now starts when you sign in, in its own window:", line)
if err := startUserSession(exe, home, g.logLevel); err != nil {
fmt.Fprintln(stdout, "It did not start now (", err, "); sign out and in, or run the line above.")
} else {
fmt.Fprintln(stdout, "Started it now. Closing its window stops the Node and every actuator.")
}
-- fmt.Fprintln(stdout, "Remove the sign-in start with `openvibe-node uninstall --user-session`.")
++ fmt.Fprintf(stdout, "Remove the sign-in start with `openvibe-node uninstall --user-session --home %s`.\n", quoteArg(home))
return nil
}
--func cmdUninstallUserSession(stdout io.Writer) error {
++func cmdUninstallUserSession(g *globals, stdout io.Writer) error {
if err := userSessionSupported(); err != nil {
return err
}
-- if err := unregisterUserSession(); err != nil {
++ if g.home == "" {
++ return errNeedHome
++ }
++ home, err := filepath.Abs(g.home)
++ if err != nil {
++ return err
++ }
++ if err := unregisterUserSession(runValueFor(home)); err != nil {
return err
}
++ // An install from before v0.37.2 wrote the single "OpenVibe Node" value; it is removed only if it starts this home.
++ if data, ok := readUserSession(runValueName); ok && legacyRunsHome(data, home) {
++ if err := unregisterUserSession(runValueName); err != nil {
++ return err
++ }
++ }
fmt.Fprintln(stdout, "The Node no longer starts at sign-in. Close its window to stop the running one; config and credential are kept.")
return nil
}
++
++// parseRegQuery finds a value's data in `reg query <key> /v <name>` output, whose line reads
++// " <name> REG_SZ <data>". ok is false when the value is not there.
++func parseRegQuery(out, name string) (string, bool) {
++ for _, line := range strings.Split(strings.ReplaceAll(out, "\r\n", "\n"), "\n") {
++ t := strings.TrimSpace(line)
++ if !strings.HasPrefix(t, name+" ") {
++ continue
++ }
++ rest := strings.TrimSpace(strings.TrimPrefix(t, name))
++ for _, typ := range []string{"REG_SZ", "REG_EXPAND_SZ"} {
++ if strings.HasPrefix(rest, typ) {
++ return strings.TrimSpace(strings.TrimPrefix(rest, typ)), true
++ }
++ }
++ }
++ return "", false
++}
ddiff --git a/cmd/openvibe-node/session_other.go b/cmd/openvibe-node/session_other.go
iindex 3ef11ec..bb6150c 100644
---- a/cmd/openvibe-node/session_other.go
++++ b/cmd/openvibe-node/session_other.go
@@@ -7,6 +7,7 @@ import "errors"
var errUserSessionWindowsOnly = errors.New("--user-session is for Windows; on Linux and macOS install the service (sudo openvibe-node install), or start `openvibe-node run --home DIR` from your desktop's autostart")
func userSessionSupported() error { return errUserSessionWindowsOnly }
--func registerUserSession(string) error { return errUserSessionWindowsOnly }
--func unregisterUserSession() error { return errUserSessionWindowsOnly }
++func registerUserSession(string, string) error { return errUserSessionWindowsOnly }
++func unregisterUserSession(string) error { return errUserSessionWindowsOnly }
++func readUserSession(string) (string, bool) { return "", false }
func startUserSession(string, string, string) error { return errUserSessionWindowsOnly }
ddiff --git a/cmd/openvibe-node/session_test.go b/cmd/openvibe-node/session_test.go
iindex 10ad8cf..9238c57 100644
---- a/cmd/openvibe-node/session_test.go
++++ b/cmd/openvibe-node/session_test.go
@@@ -27,3 +27,36 @@ func TestUserSessionRefusedOffWindows(t *testing.T) {
t.Fatalf("code %d, stderr %q", code, errb.String())
}
}
++
++// Each Node directory has its own sign-in start (v0.37.2): a test node beside a working robot's Node no longer replaces
++// its Run value, and the old single value is this Node's only when it starts this same home.
++func TestUserSessionPerHome(t *testing.T) {
++ cozmo, test := `C:\Users\m\AppData\Local\OpenVibe Node`, `D:\looi-test`
++ if a, b := runValueFor(cozmo), runValueFor(test); a == b || !strings.HasPrefix(a, "OpenVibe Node (") {
++ t.Fatalf("one value per home: %q %q", a, b)
++ }
++ legacy := userSessionCommand(`C:\bin\openvibe-node.exe`, cozmo, "info")
++ if !legacyRunsHome(legacy, cozmo) {
++ t.Fatal("the old value starts the Cozmo home")
++ }
++ if legacyRunsHome(legacy, test) {
++ t.Fatal("the old value is another Node's: never this test home's")
++ }
++ if legacyRunsHome(userSessionCommand(`C:\bin\n.exe`, `D:\ov2`, "debug"), `D:\ov`) {
++ t.Fatal(`D:\ov is not D:\ov2`)
++ }
++ if !legacyRunsHome(userSessionCommand(`C:\bin\n.exe`, `D:\ov`, "debug"), `D:\ov`) {
++ t.Fatal("with a log level after it, the home still matches")
++ }
++}
++
++func TestParseRegQuery(t *testing.T) {
++ out := "\r\nHKEY_CURRENT_USER\\Software\\Microsoft\\Windows\\CurrentVersion\\Run\r\n OpenVibe Node REG_SZ \"C:\\bin\\openvibe-node.exe\" run --home \"C:\\Users\\m\\OpenVibe Node\"\r\n\r\n"
++ got, ok := parseRegQuery(out, "OpenVibe Node")
++ if !ok || got != `"C:\bin\openvibe-node.exe" run --home "C:\Users\m\OpenVibe Node"` {
++ t.Fatalf("got %q %v", got, ok)
++ }
++ if _, ok := parseRegQuery(out, "OpenVibe Node (D:\\x)"); ok {
++ t.Fatal("another value is not found")
++ }
++}
ddiff --git a/cmd/openvibe-node/session_windows.go b/cmd/openvibe-node/session_windows.go
iindex 9b3e85c..29b6b2d 100644
---- a/cmd/openvibe-node/session_windows.go
++++ b/cmd/openvibe-node/session_windows.go
@@@ -10,22 +10,31 @@ import (
func userSessionSupported() error { return nil }
--func registerUserSession(line string) error {
-- out, err := exec.Command("reg", "add", runKey, "/v", runValueName, "/t", "REG_SZ", "/d", line, "/f").CombinedOutput()
++func registerUserSession(name, line string) error {
++ out, err := exec.Command("reg", "add", runKey, "/v", name, "/t", "REG_SZ", "/d", line, "/f").CombinedOutput()
if err != nil {
return fmt.Errorf("reg add %s: %v: %s", runKey, err, strings.TrimSpace(string(out)))
}
return nil
}
--func unregisterUserSession() error {
-- out, err := exec.Command("reg", "delete", runKey, "/v", runValueName, "/f").CombinedOutput()
++func unregisterUserSession(name string) error {
++ out, err := exec.Command("reg", "delete", runKey, "/v", name, "/f").CombinedOutput()
if err != nil && !strings.Contains(strings.ToLower(string(out)), "unable to find") {
return fmt.Errorf("reg delete %s: %v: %s", runKey, err, strings.TrimSpace(string(out)))
}
return nil
}
++// readUserSession reads one Run value's command line; ok is false when there is no such value.
++func readUserSession(name string) (string, bool) {
++ out, err := exec.Command("reg", "query", runKey, "/v", name).CombinedOutput()
++ if err != nil {
++ return "", false
++ }
++ return parseRegQuery(string(out), name)
++}
++
// startUserSession starts the Node now in a window of its own, as the Run key will at the next sign-in.
func startUserSession(exe, home, logLevel string) error {
args := []string{"/c", "start", "OpenVibe Node", exe, "run", "--home", home}
ddiff --git a/docs/hardware-looi.md b/docs/hardware-looi.md
iindex 5be2535..159a4fd 100644
---- a/docs/hardware-looi.md
++++ b/docs/hardware-looi.md
@@@ -99,7 +99,7 @@ openvibe-node install --user-session --home "%OVHOME%"
`install --user-session` starts the Node when you sign in, in a console window that shows its log (Windows keeps no
service log yet). Closing the window stops the Node and every actuator; nothing restarts it until the next sign-in.
--`openvibe-node uninstall --user-session` removes the sign-in start. On Windows a killed plugin takes the processes it
++`openvibe-node uninstall --user-session --home DIR` removes that directory's sign-in start (each Node directory has its own, since v0.37.2, so a test node never replaces a working one). On Windows a killed plugin takes the processes it
started with it (a Job Object), as it does on Linux and macOS, so a bridge helper cannot keep the Bluetooth link open
after the Node stops or restarts the plugin.
ddiff --git a/docs/install.md b/docs/install.md
iindex 470f564..d23cd98 100644
---- a/docs/install.md
++++ b/docs/install.md
@@@ -149,7 +149,7 @@ console window that shows its log; closing the window stops the Node and every a
| `openvibe-node install [--user NAME]` | install and start the service |
| `openvibe-node uninstall` | stop and remove the service (config and credential are kept) |
| `openvibe-node install --user-session --home DIR` | Windows: start at sign-in in your own session instead of as a service |
--| `openvibe-node uninstall --user-session` | Windows: remove that sign-in start |
++| `openvibe-node uninstall --user-session --home DIR` | Windows: remove that directory's sign-in start (one per directory) |
| `openvibe-node status [--json]` | link, stop latch, limits, plugins, video, battery, running jobs |
| `openvibe-node stop` | **kill switch**: stop every actuator now and hold it stopped |
| `openvibe-node resume` | release the local stop (and a server e-stop) |