Skip to content

Commit

Permalink
New host-run command, CLI improvements and use of gRPC for inception (
Browse files Browse the repository at this point in the history
#7)

Key changes:
- Add new `host-run` that allows running commands on host to be
displayed on a given qubesome profile.
- Examples were added to the CLI help for most commands.
- Add profile inference to some commands, so that it is more user
friendly. So when a single profile is running, users won't need to type
them. Example: `qubesome clip from-host`.
- Profiles have a connection with the qubesome process at the host,
which enables it from triggering workload execution from the profile
itself. That communication is now established via gRPC based on a Unix
socket.
  • Loading branch information
pjbgf authored Nov 22, 2024
2 parents 6521347 + cb12899 commit f3d40b7
Show file tree
Hide file tree
Showing 24 changed files with 915 additions and 185 deletions.
1 change: 0 additions & 1 deletion .golangci.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ linters:
- bidichk
- bodyclose
- containedctx
- contextcheck
- decorder
- dogsled
- dupl
Expand Down
10 changes: 9 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ include hack/base.mk

TARGET_BIN ?= build/bin/qubesome

PROTO = pkg/inception/proto

GO_TAGS = -tags 'netgo,osusergo,static_build'
LDFLAGS = -ldflags '-extldflags -static -s -w -X \
github.com/qubesome/cli/cmd/cli.version=$(VERSION)'
Expand All @@ -18,7 +20,13 @@ build: ## build qubesome to the path set by TARGET_BIN.
test: ## run golang tests.
go test -race -parallel 10 ./...

verify: verify-lint verify-dirty ## Run verification checks.
verify: generate verify-lint verify-dirty ## Run verification checks.

verify-lint: $(GOLANGCI)
$(GOLANGCI) run

generate: $(PROTOC)
rm $(PROTO)/*.pb.go || true
PATH=$(TOOLS_BIN) $(PROTOC) --go_out=. --go_opt=paths=source_relative \
--go-grpc_out=. --go-grpc_opt=paths=source_relative \
$(PROTO)/host.proto
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ qubesome clip to-host i3

- `qubesome start`: Start a qubesome environment for a given profile.
- `qubesome run`: Run qubesome workloads.
- `qubesome host-run`: Run commands on the host but display them in a qubesome profile.
- `qubesome clip`: Manage the images within your workloads.
- `qubesome images`: Manage the images within your workloads.
- `qubesome xdg`: Handle xdg-open based via qubesome.
Expand Down
40 changes: 24 additions & 16 deletions cmd/cli/clipboard.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,17 @@ func clipboardCommand() *cli.Command {
{
Name: "from-host",
Usage: "copies the clipboard contents from the host to a profile",
Description: `Examples:
qubesome clip from-host - Copy clipboard contents from host to the active profile
qubesome clip from-host -type image/png - Copy image from host clipboard to the active profile
qubesome clip from-host -profile <name> - Copy clipboard contents from host to a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "target_profile",
Min: 1,
UsageText: "Required when multiple profiles are active",
Min: 0,
Max: 1,
Destination: &targetProfile,
},
Expand All @@ -42,11 +49,9 @@ func clipboardCommand() *cli.Command {
clipType,
},
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

target, ok := cfg.Profiles[targetProfile]
if !ok {
return fmt.Errorf("no active profile %q found", targetProfile)
target, err := profileOrActive(targetProfile)
if err != nil {
return err
}

opts := []command.Option[clipboard.Options]{
Expand All @@ -55,7 +60,6 @@ func clipboardCommand() *cli.Command {
}

if typ := c.String("type"); typ != "" {
fmt.Println(typ)
opts = append(opts, clipboard.WithContentType(typ))
}

Expand Down Expand Up @@ -87,12 +91,12 @@ func clipboardCommand() *cli.Command {
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

source, ok := cfg.Profiles[sourceProfile]
source, ok := cfg.Profile(sourceProfile)
if !ok {
return fmt.Errorf("no active profile %q found", sourceProfile)
}

target, ok := cfg.Profiles[targetProfile]
target, ok := cfg.Profile(targetProfile)
if !ok {
return fmt.Errorf("no active profile %q found", targetProfile)
}
Expand All @@ -103,7 +107,6 @@ func clipboardCommand() *cli.Command {
}

if typ := c.String("type"); typ != "" {
fmt.Println(typ)
opts = append(opts, clipboard.WithContentType(typ))
}

Expand All @@ -115,10 +118,17 @@ func clipboardCommand() *cli.Command {
{
Name: "to-host",
Usage: "copies the clipboard contents from a profile to the host",
Description: `Examples:
qubesome clip to-host - Copy clipboard contents from the active profile to the host
qubesome clip to-host -type image/png - Copy image from the active profile clipboard to the host
qubesome clip to-host -profile <name> - Copy clipboard contents from a specific profile to the host
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "source_profile",
Min: 1,
UsageText: "Required when multiple profiles are active",
Min: 0,
Max: 1,
Destination: &sourceProfile,
},
Expand All @@ -127,11 +137,9 @@ func clipboardCommand() *cli.Command {
clipType,
},
Action: func(ctx context.Context, c *cli.Command) error {
cfg := profileConfigOrDefault(sourceProfile)

target, ok := cfg.Profiles[sourceProfile]
if !ok {
return fmt.Errorf("no active profile %q found", sourceProfile)
target, err := profileOrActive(sourceProfile)
if err != nil {
return err
}

opts := []command.Option[clipboard.Options]{
Expand Down
51 changes: 51 additions & 0 deletions cmd/cli/host_run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package cli

import (
"context"
"fmt"
"os/exec"

"github.com/urfave/cli/v3"
)

func hostRunCommand() *cli.Command {
cmd := &cli.Command{
Name: "host-run",
Aliases: []string{"hr"},
Usage: "Runs a command at the host, but shows it in a given qubesome profile",
Description: `Examples:
qubesome host-run firefox - Run firefox on the host and display it on the active profile
qubesome host-run -profile <profile> firefox - Run firefox on the host and display it on a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "command",
Min: 1,
Max: 1,
Destination: &commandName,
},
},
Flags: []cli.Flag{
&cli.StringFlag{
Name: "profile",
Usage: "Required when multiple profiles are active",
Destination: &targetProfile,
},
},
Action: func(ctx context.Context, cmd *cli.Command) error {
prof, err := profileOrActive(targetProfile)
if err != nil {
return err
}

c := exec.Command(commandName)
c.Env = append(c.Env, fmt.Sprintf("DISPLAY=:%d", prof.Display))
out, err := c.CombinedOutput()
fmt.Println(out)

return err
},
}
return cmd
}
11 changes: 11 additions & 0 deletions cmd/cli/images.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package cli

import (
"context"
"errors"
"fmt"

"github.com/qubesome/cli/internal/images"
"github.com/urfave/cli/v3"
Expand All @@ -27,6 +29,15 @@ func imagesCommand() *cli.Command {
},
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)
if cfg == nil {
return errors.New("could not find qubesome config")
}

if targetProfile != "" {
if _, ok := cfg.Profile(targetProfile); !ok {
return fmt.Errorf("could not find profile %q", targetProfile)
}
}

return images.Run(
images.WithConfig(cfg),
Expand Down
87 changes: 79 additions & 8 deletions cmd/cli/root.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,12 @@ package cli

import (
"context"
"errors"
"fmt"
"log/slog"
"os"
"path/filepath"
"strings"

"github.com/qubesome/cli/internal/files"
"github.com/qubesome/cli/internal/log"
Expand All @@ -19,6 +23,7 @@ var (
path string
local string
runner string
commandName string
debug bool
)

Expand All @@ -33,9 +38,17 @@ func RootCommand() *cli.Command {
depsCommand(),
versionCommand(),
completionCommand(),
hostRunCommand(),
},
}

cmd.Before = func(ctx context.Context, c *cli.Command) (context.Context, error) {
if strings.EqualFold(os.Getenv("XDG_SESSION_TYPE"), "wayland") {
fmt.Println("\033[33mWARN: Running qubesome in Wayland is experimental. Some features may not work as expected.\033[0m")
}
return ctx, nil
}

cmd.Flags = append(cmd.Flags, &cli.BoolFlag{
Name: "debug",
Value: false,
Expand Down Expand Up @@ -70,18 +83,76 @@ func config(path string) *types.Config {
}

func profileConfigOrDefault(profile string) *types.Config {
path := files.ProfileConfig(profile)
target, err := os.Readlink(path)
if profile != "" {
// Try to load the profile specific config.
path := files.ProfileConfig(profile)
target, err := os.Readlink(path)

var c *types.Config
if err == nil {
c = config(target)
if err == nil {
c := config(target)
slog.Debug("using profile config", "path", path, "config", c)
if c != nil {
return c
}
}
}

if c != nil {
return c
cfgs := activeConfigs()
if len(cfgs) == 1 {
c := config(cfgs[0])
slog.Debug("using active profile config", "path", cfgs[0], "config", c)
if c != nil && len(c.Profiles) > 0 {
return c
}
}

// Try to load user-level qubesome config.
path = files.QubesomeConfig()
return config(path)
c := config(path)
slog.Debug("using user-level config", "path", path, "config", c)
if c != nil && len(c.Profiles) > 0 {
return c
}

return nil
}

func profileOrActive(profile string) (*types.Profile, error) {
if profile != "" {
cfg := profileConfigOrDefault(profile)
prof, ok := cfg.Profile(profile)
if !ok {
return nil, fmt.Errorf("profile %q not active", profile)
}
return prof, nil
}

cfgs := activeConfigs()
if len(cfgs) > 1 {
return nil, errors.New("multiple profiles active: pick one with -profile")
}
if len(cfgs) == 0 {
return nil, errors.New("no active profile found: start one with qubesome start")
}

f := cfgs[0]
name := strings.TrimSuffix(filepath.Base(f), filepath.Ext(f))
return profileOrActive(name)
}

func activeConfigs() []string {
var active []string

root := files.RunUserQubesome()
entries, err := os.ReadDir(root)
if err == nil {
for _, entry := range entries {
fn := entry.Name()
if filepath.Ext(fn) == ".config" {
active = append(active, filepath.Join(root, fn))
}
}
}

return active
}
16 changes: 13 additions & 3 deletions cmd/cli/run.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ func runCommand() *cli.Command {
cmd := &cli.Command{
Name: "run",
Aliases: []string{"r"},
Usage: "execute workloads",
Description: `Examples:
qubesome run chrome - Run the chrome workload on the active profile
qubesome run -profile <profile> chrome - Run the chrome workload on a specific profile
`,
Arguments: []cli.Argument{
&cli.StringArg{
Name: "workload",
Expand All @@ -29,13 +35,17 @@ func runCommand() *cli.Command {
Destination: &runner,
},
},
Usage: "execute workloads",
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)
prof, err := profileOrActive(targetProfile)
if err != nil {
return err
}

cfg := profileConfigOrDefault(prof.Name)

return qubesome.Run(
qubesome.WithWorkload(workload),
qubesome.WithProfile(targetProfile),
qubesome.WithProfile(prof.Name),
qubesome.WithConfig(cfg),
qubesome.WithRunner(runner),
qubesome.WithExtraArgs(cmd.Args().Slice()),
Expand Down
7 changes: 6 additions & 1 deletion cmd/cli/start.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,12 @@ func startCommand() *cli.Command {
cmd := &cli.Command{
Name: "start",
Aliases: []string{"s"},
Usage: "start qubesome profiles",
Description: `Examples:
qubesome start -git https://github.com/qubesome/sample-dotfiles awesome
qubesome start -git https://github.com/qubesome/sample-dotfiles i3
`,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "git",
Expand Down Expand Up @@ -40,7 +46,6 @@ func startCommand() *cli.Command {
Destination: &targetProfile,
},
},
Usage: "start qubesome profiles",
Action: func(ctx context.Context, cmd *cli.Command) error {
cfg := profileConfigOrDefault(targetProfile)

Expand Down
Loading

0 comments on commit f3d40b7

Please sign in to comment.