Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add screen based boot mechanism #113

Merged
merged 20 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 8 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
8 changes: 8 additions & 0 deletions .web-docs/components/builder/macvm/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,14 @@ communicator is "ssh".

<!-- Code generated from the comments of the Config struct in builder/parallels/macvm/config.go; DO NOT EDIT MANUALLY -->

- `screen_configs` ([]parallelscommon.SingleScreenBootConfig) - Screens and it's boot configs
A screen is considered matched if all the matching strings are present in the screen.
The first matching screen will be considered & boot config of that screen will be used.
If matching strings are empty, then it is considered as empty screen,
which will be considered when none of the other screens are matched (You can use this screen to -
make system wait for some time / execute a common boot command etc.).
If more than one empty screen is found, then it is considered as an error.

- `vm_name` (string) - This is the name of the MACVM directory for the new
virtual machine, without the file extension. By default this is
"packer-BUILDNAME", where "BUILDNAME" is the name of the build.
Expand Down
3 changes: 3 additions & 0 deletions builder/parallels/common/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ type Driver interface {
// Prlctl executes the given Prlctl command
Prlctl(...string) error

// PrlctlGet executes the given Prlctl command and returns the output
PrlctlGet(...string) (string, error)

// Get the path to the Parallels Tools ISO for the given flavor.
ToolsISOPath(string) (string, error)

Expand Down
8 changes: 7 additions & 1 deletion builder/parallels/common/driver_9.go
Original file line number Diff line number Diff line change
Expand Up @@ -310,6 +310,12 @@ func (d *Parallels9Driver) Stop(name string) error {

// Prlctl executes the specified "prlctl" command.
func (d *Parallels9Driver) Prlctl(args ...string) error {
_, err := d.PrlctlGet(args...)
return err
}

// PrlctlGet executes the given "prlctl" command and returns the output
func (d *Parallels9Driver) PrlctlGet(args ...string) (string, error) {
var stdout, stderr bytes.Buffer

log.Printf("Executing prlctl: %#v", args)
Expand All @@ -328,7 +334,7 @@ func (d *Parallels9Driver) Prlctl(args ...string) error {
log.Printf("stdout: %s", stdoutString)
log.Printf("stderr: %s", stderrString)

return err
return stdoutString, err
}

// Verify raises an error if the builder could not be used on that host machine.
Expand Down
53 changes: 53 additions & 0 deletions builder/parallels/common/single_screen_boot_config.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
// Copyright (c) Parallels International GmBH
// SPDX-License-Identifier: MPL-2.0

//go:generate packer-sdc struct-markdown
//go:generate packer-sdc mapstructure-to-hcl2 -type SingleScreenBootConfig

package common

import (
"fmt"
"strings"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)

type SingleScreenBootConfig struct {
// BootConfig for this screen
bootcommand.BootConfig `mapstructure:",squash"`
// Screen name to identify
ScreenName string `mapstructure:"screen_name"`
// Strings present in the screen to identify this screen
MatchingStrings []string `mapstructure:"matching_strings"`
// Specifies if the current screen is the last screen
// Screen based boot will stop after this screen
IsLastScreen bool `mapstructure:"is_last_screen"`
}

func (c *SingleScreenBootConfig) Prepare(ctx *interpolate.Context) (errs []error) {

//Prepare bootconfig first
errs = append(errs, c.BootConfig.Prepare(ctx)...)

if len(c.ScreenName) == 0 {
errs = append(errs,
fmt.Errorf("screen name should not be empty"))
}

if len(errs) > 0 {
return errs
}

//Convert all matching strings to lowercase
for i, matchingString := range c.MatchingStrings {
c.MatchingStrings[i] = strings.ToLower(matchingString)
}

return nil
}

func (c *SingleScreenBootConfig) FlatBootCommand() string {
return strings.Join(c.BootCommand, "")
}
41 changes: 41 additions & 0 deletions builder/parallels/common/single_screen_boot_config.hcl2spec.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

172 changes: 172 additions & 0 deletions builder/parallels/common/step_screen_based_boot.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: MPL-2.0

package common

import (
"context"
"errors"
"fmt"
"log"
"os/exec"
"strings"
"time"

"github.com/hashicorp/packer-plugin-sdk/bootcommand"
"github.com/hashicorp/packer-plugin-sdk/multistep"
packersdk "github.com/hashicorp/packer-plugin-sdk/packer"
"github.com/hashicorp/packer-plugin-sdk/template/interpolate"
)

// This step creates the virtual disk that will be used as the
// hard drive for the virtual machine.
type StepScreenBasedBoot struct {
ScreenConfigs []SingleScreenBootConfig
VmName string
Ctx interpolate.Context
}

func (s *StepScreenBasedBoot) executeBinary(binaryPath string, args ...string) (string, error) {
// Run the binary with the specified path and arguments
cmd := exec.Command(binaryPath, args...)

// Capture the output
output, err := cmd.Output()
if err != nil {
return "", err
}

// Convert output to string and return
result := string(output)
return result, nil
}

func (s *StepScreenBasedBoot) detectScreen(text string) SingleScreenBootConfig {
text = strings.ToLower(text)
emptyBootConfig := SingleScreenBootConfig{}

for _, screenConfig := range s.ScreenConfigs {
// Empty screens are not considered for matching
if (screenConfig.MatchingStrings == nil) || (len(screenConfig.MatchingStrings) == 0) {
emptyBootConfig = screenConfig
continue
}

// Check if all screenConfig.MatchingStrings are present in text. If all are present, return the screenConfig
matching := true
for _, matchingString := range screenConfig.MatchingStrings {
if !strings.Contains(text, matchingString) {
matching = false
break
}
}

if matching {
return screenConfig
}
}

return emptyBootConfig
}

func (s *StepScreenBasedBoot) executeBootCommand(bootConfig bootcommand.BootConfig, ctx context.Context, state multistep.StateBag) error {
// Execute the boot command
step := StepTypeBootCommand{
BootWait: bootConfig.BootWait,
BootCommand: bootConfig.FlatBootCommand(),
HostInterfaces: []string{},
VMName: s.VmName,
Ctx: s.Ctx,
GroupInterval: bootConfig.BootGroupInterval,
}

resultAction := step.Run(ctx, state)
if resultAction == multistep.ActionContinue {
return nil
}

// Report any errors.
if rawErr, ok := state.GetOk("error"); ok {
return rawErr.(error)
}

return errors.New("boot command failed")
}

func (s *StepScreenBasedBoot) Run(ctx context.Context, state multistep.StateBag) multistep.StepAction {
//We don't have any screen configs, so no wasting of time here.
if (s.ScreenConfigs == nil) || (len(s.ScreenConfigs) == 0) {
return multistep.ActionContinue
}

// Retrieve the window ID
windowIDDetector := WindowIDDetector{}
windowId, err := windowIDDetector.DetectWindowId(s.VmName, state)
if err != nil || windowId == 0 {
log.Println("Error retrieving window ID:", err)
return multistep.ActionHalt
}

ui := state.Get("ui").(packersdk.Ui)
// Window ID option
windowIDOption := "-l" + fmt.Sprint(windowId)
// Path to the binary you want to execute
binaryPath := "/usr/sbin/screencapture"
// Command-line arguments to pass to the binary
args := []string{"-x", windowIDOption, "./screenshot.png"}
bineesh-n marked this conversation as resolved.
Show resolved Hide resolved

ui.Say("Starting screen based boot...")
ticker := time.NewTicker(1 * time.Second)
lastScreenName := ""
for range ticker.C {
log.Println("Checking screen...")

// Capturing the screenshot
_, err := s.executeBinary(binaryPath, args...)
if err != nil {
log.Println("Error:", err)
return multistep.ActionHalt
}

// Use OCR to detect the text in the screenshot
visionOCR := VisionOCR{}
text, err := visionOCR.RecognizeText("./screenshot.png")
if err != nil {
fmt.Println("Error:", err)
return multistep.ActionHalt
}

log.Println("Recognized text:", text)
// Detect the screen based on the text
screenConfig := s.detectScreen(text)
if screenConfig.ScreenName == "" {
log.Printf("No matching screen found for text on screen : %s.\n", text)
return multistep.ActionHalt
}

if screenConfig.ScreenName == lastScreenName {
continue
}

lastScreenName = screenConfig.ScreenName
ui.Say("Screen changed to " + screenConfig.ScreenName)
// Execute the boot command
bootCommand := screenConfig.FlatBootCommand()
if bootCommand != "" {
err := s.executeBootCommand(screenConfig.BootConfig, ctx, state)
if err != nil {
log.Println("Error:", err)
return multistep.ActionHalt
}
}

if screenConfig.IsLastScreen {
break
}

}

return multistep.ActionContinue
}

func (s *StepScreenBasedBoot) Cleanup(state multistep.StateBag) {}
Loading
Loading