diff --git a/cmd/arduino-flasher-cli/flash/flash.go b/cmd/arduino-flasher-cli/flash/flash.go index d7f5862..bd3487b 100644 --- a/cmd/arduino-flasher-cli/flash/flash.go +++ b/cmd/arduino-flasher-cli/flash/flash.go @@ -113,7 +113,7 @@ func runFlashCommand(ctx context.Context, args []string, forceYes bool, preserve } } - err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir) + err = updater.Flash(ctx, imagePath, args[0], forceYes, preserveUser, tempDir, nil) if err != nil { feedback.Fatal(i18n.Tr("error flashing the board: %v", err), feedback.ErrBadArgument) } diff --git a/internal/helper/helper.go b/internal/helper/helper.go new file mode 100644 index 0000000..a18de4a --- /dev/null +++ b/internal/helper/helper.go @@ -0,0 +1,34 @@ +package helper + +import ( + "bytes" +) + +// CallbackWriter is a custom writer that processes each line calling the callback. +type CallbackWriter struct { + callback func(line string) + buffer []byte +} + +// NewCallbackWriter creates a new CallbackWriter. +func NewCallbackWriter(process func(line string)) *CallbackWriter { + return &CallbackWriter{ + callback: process, + buffer: make([]byte, 0, 1024), + } +} + +// Write implements the io.Writer interface. +func (p *CallbackWriter) Write(data []byte) (int, error) { + p.buffer = append(p.buffer, data...) + for { + idx := bytes.IndexByte(p.buffer, '\n') + if idx == -1 { + break + } + line := p.buffer[:idx] // Do not include \n + p.buffer = p.buffer[idx+1:] + p.callback(string(line)) + } + return len(data), nil +} diff --git a/internal/updater/flasher.go b/internal/updater/flasher.go index 94a621f..f4c5edb 100644 --- a/internal/updater/flasher.go +++ b/internal/updater/flasher.go @@ -28,6 +28,7 @@ import ( "github.com/arduino/arduino-flasher-cli/cmd/feedback" "github.com/arduino/arduino-flasher-cli/cmd/i18n" + "github.com/arduino/arduino-flasher-cli/internal/helper" "github.com/arduino/arduino-flasher-cli/internal/updater/artifacts" ) @@ -36,7 +37,7 @@ const DownloadDiskSpace = uint64(12) const ExtractDiskSpace = uint64(10) const yesPrompt = "yes" -func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string) error { +func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes bool, preserveUser bool, tempDir string, callback FlahsCallback) error { if !imagePath.Exist() { temp, err := SetTempDir("download-", tempDir) if err != nil { @@ -90,10 +91,26 @@ func Flash(ctx context.Context, imagePath *paths.Path, version string, forceYes imagePath = tempContent[0] } - return FlashBoard(ctx, imagePath.String(), version, preserveUser) + return FlashBoard(ctx, imagePath.String(), version, preserveUser, nil) } -func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool) error { +type EventType int + +const ( + EventLog EventType = iota + EventProgress +) + +type FlashEvent struct { + Type EventType + Log string + Progress int + Total int +} + +type FlahsCallback func(FlashEvent) + +func FlashBoard(ctx context.Context, downloadedImagePath string, version string, preserveUser bool, callback FlahsCallback) error { var flashDir *paths.Path for _, entry := range []string{"flash", "flash_UnoQ"} { if p := paths.New(downloadedImagePath, entry); p.Exist() { @@ -161,6 +178,11 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, } + totalPartitions, err := getTotalPartition(flashDir.Join(rawProgram)) + if err != nil { + return err + } + feedback.Print(i18n.Tr("Flashing with qdl")) cmd, err := paths.NewProcess(nil, qdlPath.String(), "--allow-missing", "--storage", "emmc", "prog_firehose_ddr.elf", rawProgram, "patch0.xml") if err != nil { @@ -168,8 +190,32 @@ func FlashBoard(ctx context.Context, downloadedImagePath string, version string, } // Setting the directory is needed because rawprogram0.xml contains relative file paths cmd.SetDir(flashDir.String()) - cmd.RedirectStderrTo(stdout) - cmd.RedirectStdoutTo(stdout) + + w := stdout + if callback != nil { + progress := 0 + w = helper.NewCallbackWriter(func(line string) { + parsedLine := parseQdlLogLine(line) + + switch parsedLine.Op { + case Flashed: + progress++ + callback(FlashEvent{ + Type: EventProgress, + Log: line, + Progress: progress, + Total: totalPartitions, + }) + default: + callback(FlashEvent{ + Type: EventLog, + Log: line, + }) + } + }) + } + cmd.RedirectStderrTo(w) + cmd.RedirectStdoutTo(w) if err := cmd.RunWithinContext(ctx); err != nil { return err } diff --git a/internal/updater/parseqdl.go b/internal/updater/parseqdl.go new file mode 100644 index 0000000..26e23e7 --- /dev/null +++ b/internal/updater/parseqdl.go @@ -0,0 +1,82 @@ +package updater + +import ( + "encoding/xml" + "strings" + + "github.com/arduino/go-paths-helper" +) + +type Op int + +const ( + Waiting Op = iota + Flashed + Unknown +) + +type QDLLogLine struct { + Op Op + Log string +} + +func parseQdlLogLine(line string) QDLLogLine { + lower := strings.ToLower(line) + + switch { + case strings.HasPrefix(lower, "waiting for"): + return QDLLogLine{ + Op: Waiting, + Log: line, + } + case strings.HasPrefix(lower, "flashed"): + return QDLLogLine{ + Op: Flashed, + Log: line, + } + default: + return QDLLogLine{ + Op: Unknown, + Log: line, + } + } +} + +func getTotalPartition(path *paths.Path) (int, error) { + rawProgramFile, err := parseRawProgramFile(path) + if err != nil { + return 0, err + } + + var total int + for _, program := range rawProgramFile.Programs { + if program.Filename != "" { + total++ + } + } + + return total, nil +} + +type RawProgramFile struct { + Programs []Program `xml:"program"` +} + +type Program struct { + Filename string `xml:"filename,attr"` +} + +func parseRawProgramFile(path *paths.Path) (RawProgramFile, error) { + f, err := path.Open() + if err != nil { + return RawProgramFile{}, err + } + defer f.Close() + + var data RawProgramFile + if err := xml.NewDecoder(f).Decode(&data); err != nil { + return RawProgramFile{}, err + } + + return data, nil +} diff --git a/internal/updater/parseqdl_test.go b/internal/updater/parseqdl_test.go new file mode 100644 index 0000000..8c44484 --- /dev/null +++ b/internal/updater/parseqdl_test.go @@ -0,0 +1,72 @@ +package updater + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestParseQdlLogLine(t *testing.T) { + tests := []struct { + line string + want QDLLogLine + wantErr bool + }{ + { + line: "Waiting for EDL device...", + want: QDLLogLine{ + Op: Waiting, + Log: "Waiting for EDL device...", + }, + }, + { + line: "waiting for programmer...", + want: QDLLogLine{ + Op: Waiting, + Log: "waiting for programmer...", + }, + }, + { + line: `flashed "xbl_a" successfully`, + want: QDLLogLine{ + Op: Flashed, + Log: `flashed "xbl_a" successfully`, + }, + }, + { + line: `flashed "rootfs" successfully at 65058kB/s`, + want: QDLLogLine{ + Op: Flashed, + Log: `flashed "rootfs" successfully at 65058kB/s`, + }, + }, + { + + line: "13 patches applied", + want: QDLLogLine{ + Op: Unknown, + Log: "13 patches applied", + }, + }, + { + line: "partition 0 is now bootable", + want: QDLLogLine{ + Op: Unknown, + Log: "partition 0 is now bootable", + }, + }, + } + + for _, tt := range tests { + t.Run(tt.line, func(t *testing.T) { + result, err := parseQdlLogLine(tt.line) + if tt.wantErr { + require.Error(t, err) + } else { + require.NoError(t, err) + require.Equal(t, tt.want, result) + } + + }) + } +} diff --git a/rpc/cc/arduino/flasher/v1/common.pb.go b/rpc/cc/arduino/flasher/v1/common.pb.go index 07f5db1..3f7bfb2 100644 --- a/rpc/cc/arduino/flasher/v1/common.pb.go +++ b/rpc/cc/arduino/flasher/v1/common.pb.go @@ -316,8 +316,10 @@ type TaskProgress struct { Message string `protobuf:"bytes,2,opt,name=message,proto3" json:"message,omitempty"` // Whether the task is complete. Completed bool `protobuf:"varint,3,opt,name=completed,proto3" json:"completed,omitempty"` - // Amount in percent of the task completion (optional). - Percent float32 `protobuf:"fixed32,4,opt,name=percent,proto3" json:"percent,omitempty"` + // Current step of the task. + Progress int64 `protobuf:"varint,5,opt,name=progress,proto3" json:"progress,omitempty"` + // Total steps of the task. + Total int64 `protobuf:"varint,6,opt,name=total,proto3" json:"total,omitempty"` } func (x *TaskProgress) Reset() { @@ -373,9 +375,16 @@ func (x *TaskProgress) GetCompleted() bool { return false } -func (x *TaskProgress) GetPercent() float32 { +func (x *TaskProgress) GetProgress() int64 { if x != nil { - return x.Percent + return x.Progress + } + return 0 +} + +func (x *TaskProgress) GetTotal() int64 { + if x != nil { + return x.Total } return 0 } @@ -416,19 +425,21 @@ var file_cc_arduino_flasher_v1_common_proto_rawDesc = []byte{ 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x73, 0x75, 0x63, 0x63, 0x65, 0x73, 0x73, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x22, - 0x74, 0x0a, 0x0c, 0x54, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, - 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6e, - 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x02, - 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c, 0x0a, - 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, - 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x18, 0x0a, 0x07, 0x70, - 0x65, 0x72, 0x63, 0x65, 0x6e, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x02, 0x52, 0x07, 0x70, 0x65, - 0x72, 0x63, 0x65, 0x6e, 0x74, 0x42, 0x4a, 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, - 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x64, 0x75, 0x69, 0x6e, 0x6f, 0x2f, 0x61, 0x72, 0x64, 0x75, - 0x69, 0x6e, 0x6f, 0x2d, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x2f, - 0x72, 0x70, 0x63, 0x2f, 0x63, 0x63, 0x2f, 0x61, 0x72, 0x64, 0x75, 0x69, 0x6e, 0x6f, 0x2f, 0x66, - 0x6c, 0x61, 0x73, 0x68, 0x65, 0x72, 0x2f, 0x76, 0x31, 0x3b, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x65, - 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x8c, 0x01, 0x0a, 0x0c, 0x54, 0x61, 0x73, 0x6b, 0x50, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, + 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, + 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, + 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x1c, + 0x0a, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x09, 0x63, 0x6f, 0x6d, 0x70, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x12, 0x1a, 0x0a, 0x08, + 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, + 0x70, 0x72, 0x6f, 0x67, 0x72, 0x65, 0x73, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, + 0x6c, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x42, 0x4a, + 0x5a, 0x48, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x61, 0x72, 0x64, + 0x75, 0x69, 0x6e, 0x6f, 0x2f, 0x61, 0x72, 0x64, 0x75, 0x69, 0x6e, 0x6f, 0x2d, 0x66, 0x6c, 0x61, + 0x73, 0x68, 0x65, 0x72, 0x2d, 0x63, 0x6c, 0x69, 0x2f, 0x72, 0x70, 0x63, 0x2f, 0x63, 0x63, 0x2f, + 0x61, 0x72, 0x64, 0x75, 0x69, 0x6e, 0x6f, 0x2f, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x72, 0x2f, + 0x76, 0x31, 0x3b, 0x66, 0x6c, 0x61, 0x73, 0x68, 0x65, 0x72, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x33, } var ( diff --git a/rpc/cc/arduino/flasher/v1/common.proto b/rpc/cc/arduino/flasher/v1/common.proto index 4f5cc14..5aa33fb 100644 --- a/rpc/cc/arduino/flasher/v1/common.proto +++ b/rpc/cc/arduino/flasher/v1/common.proto @@ -1,64 +1,66 @@ -// This file is part of arduino-flasher-cli. -// -// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-flasher-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to -// modify or otherwise use the software for commercial activities involving the -// Arduino software without disclosing the source code of your own applications. -// To purchase a commercial license, send an email to license@arduino.cc. - -syntax = "proto3"; - -package cc.arduino.flasher.v1; - -option go_package = "github.com/arduino/arduino-flasher-cli/rpc/cc/arduino/flasher/v1;flasher"; - -message DownloadProgress { - oneof message { - // Sent when a download is started. - DownloadProgressStart start = 1; - // Progress updates for a download. - DownloadProgressUpdate update = 2; - // Sent when a download is finished or failed. - DownloadProgressEnd end = 3; - } -} - -message DownloadProgressStart { - // URL of the download. - string url = 1; - // The label to display on the progress bar. - string label = 2; -} - -message DownloadProgressUpdate { - // Size of the downloaded portion of the file. - int64 downloaded = 1; - // Total size of the file being downloaded. - int64 total_size = 2; -} - -message DownloadProgressEnd { - // True if the download is successful. - bool success = 1; - // Info or error message, depending on the value of 'success'. Some examples: - // "File xxx already downloaded" or "Connection timeout". - string message = 2; -} - -message TaskProgress { - // Description of the task. - string name = 1; - // Additional information about the task. - string message = 2; - // Whether the task is complete. - bool completed = 3; - // Amount in percent of the task completion (optional). - float percent = 4; -} +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +syntax = "proto3"; + +package cc.arduino.flasher.v1; + +option go_package = "github.com/arduino/arduino-flasher-cli/rpc/cc/arduino/flasher/v1;flasher"; + +message DownloadProgress { + oneof message { + // Sent when a download is started. + DownloadProgressStart start = 1; + // Progress updates for a download. + DownloadProgressUpdate update = 2; + // Sent when a download is finished or failed. + DownloadProgressEnd end = 3; + } +} + +message DownloadProgressStart { + // URL of the download. + string url = 1; + // The label to display on the progress bar. + string label = 2; +} + +message DownloadProgressUpdate { + // Size of the downloaded portion of the file. + int64 downloaded = 1; + // Total size of the file being downloaded. + int64 total_size = 2; +} + +message DownloadProgressEnd { + // True if the download is successful. + bool success = 1; + // Info or error message, depending on the value of 'success'. Some examples: + // "File xxx already downloaded" or "Connection timeout". + string message = 2; +} + +message TaskProgress { + // Description of the task. + string name = 1; + // Additional information about the task. + string message = 2; + // Whether the task is complete. + bool completed = 3; + // Current step of the task. + int64 progress = 5; + // Total steps of the task. + int64 total = 6; +} diff --git a/service/service_flash.go b/service/service_flash.go index 458085d..22be2cf 100644 --- a/service/service_flash.go +++ b/service/service_flash.go @@ -1,139 +1,157 @@ -// This file is part of arduino-flasher-cli. -// -// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) -// -// This software is released under the GNU General Public License version 3, -// which covers the main part of arduino-flasher-cli. -// The terms of this license can be found at: -// https://www.gnu.org/licenses/gpl-3.0.en.html -// -// You can be released from the requirements of the above licenses by purchasing -// a commercial license. Buying such a license is mandatory if you want to -// modify or otherwise use the software for commercial activities involving the -// Arduino software without disclosing the source code of your own applications. -// To purchase a commercial license, send an email to license@arduino.cc. - -package service - -import ( - "fmt" - - "github.com/arduino/go-paths-helper" - "github.com/codeclysm/extract/v4" - "github.com/shirou/gopsutil/v4/disk" - "go.bug.st/downloader/v2" - - "github.com/arduino/arduino-flasher-cli/internal/updater" - flasher "github.com/arduino/arduino-flasher-cli/rpc/cc/arduino/flasher/v1" -) - -func (s *flasherServerImpl) Flash(req *flasher.FlashRequest, stream flasher.Flasher_FlashServer) error { - // Setup callback functions - var responseCallback func(*flasher.FlashResponse) error - if stream != nil { - syncSend := NewSynchronizedSend(stream.Send) - responseCallback = syncSend.Send - } else { - responseCallback = func(*flasher.FlashResponse) error { return nil } - } - ctx := stream.Context() - downloadCB := func(msg *flasher.DownloadProgress) { - responseCallback(&flasher.FlashResponse{ - Message: &flasher.FlashResponse_DownloadProgress{ - DownloadProgress: &flasher.DownloadProgress{ - Message: msg.GetMessage(), - }, - }, - }) - } - extractCB := func(msg *flasher.TaskProgress) { - responseCallback(&flasher.FlashResponse{ - Message: &flasher.FlashResponse_ExtractionProgress{ - ExtractionProgress: &flasher.TaskProgress{ - Name: msg.GetName(), - Message: msg.GetMessage(), - Completed: msg.GetCompleted(), - }, - }, - }) - } - flashCB := func(msg *flasher.TaskProgress) { - responseCallback(&flasher.FlashResponse{ - Message: &flasher.FlashResponse_FlashProgress{ - FlashProgress: &flasher.TaskProgress{ - Name: msg.GetName(), - Message: msg.GetMessage(), - Completed: msg.GetCompleted(), - }, - }, - }) - } - - // Check if there is enough free disk space before downloading and extracting an image - d, err := disk.Usage(req.TempPath) - if err != nil { - return err - } - if d.Free/updater.GiB < updater.DownloadDiskSpace { - return fmt.Errorf("download and extraction requires up to %d GiB of free space", updater.DownloadDiskSpace) - } - - client := updater.NewClient() - manifest, err := client.GetInfoManifest(ctx) - if err != nil { - return err - } - - var rel *updater.Release - if req.Version == "latest" || req.Version == manifest.Latest.Version { - rel = &manifest.Latest - } else { - for _, r := range manifest.Releases { - if req.Version == r.Version { - rel = &r - break - } - } - } - - if rel == nil { - return fmt.Errorf("could not find Debian image %s", req.Version) - } - - tmpZip := paths.New(req.GetTempPath(), "arduino-unoq-debian-image-"+rel.Version+".tar.zst") - - if err := updater.DownloadFile(ctx, tmpZip, rel.Url, rel.Version, downloadCB, downloader.Config{}); err != nil { - return err - } - defer tmpZip.RemoveAll() - - tmpZipFile, err := tmpZip.Open() - if err != nil { - return fmt.Errorf("could not open archive: %w", err) - } - defer tmpZipFile.Close() - - extractCB(&flasher.TaskProgress{Name: "extract", Message: "Start"}) - if err := extract.Archive(ctx, tmpZipFile, tmpZip.Parent().String(), func(s string) string { - extractCB(&flasher.TaskProgress{Name: "extract", Message: s}) - return s - }); err != nil { - return fmt.Errorf("could not extract archive: %w", err) - } - extractCB(&flasher.TaskProgress{Name: "extract", Completed: true}) - - imagePath := tmpZip.Parent().Join("arduino-unoq-debian-image-" + rel.Version) - defer imagePath.RemoveAll() - - flashCB(&flasher.TaskProgress{Name: "flash", Message: "Starting"}) - if err := updater.FlashBoard(ctx, imagePath.String(), rel.Version, req.GetPreserveUser()); err != nil { - return err - } - flashCB(&flasher.TaskProgress{Name: "flash", Completed: true}) - - return responseCallback(&flasher.FlashResponse{ - Message: &flasher.FlashResponse_Result_{ - Result: &flasher.FlashResponse_Result{}, - }, - }) -} +// This file is part of arduino-flasher-cli. +// +// Copyright 2025 ARDUINO SA (http://www.arduino.cc/) +// +// This software is released under the GNU General Public License version 3, +// which covers the main part of arduino-flasher-cli. +// The terms of this license can be found at: +// https://www.gnu.org/licenses/gpl-3.0.en.html +// +// You can be released from the requirements of the above licenses by purchasing +// a commercial license. Buying such a license is mandatory if you want to +// modify or otherwise use the software for commercial activities involving the +// Arduino software without disclosing the source code of your own applications. +// To purchase a commercial license, send an email to license@arduino.cc. + +package service + +import ( + "fmt" + + "github.com/arduino/go-paths-helper" + "github.com/codeclysm/extract/v4" + "github.com/shirou/gopsutil/v4/disk" + "go.bug.st/downloader/v2" + + "github.com/arduino/arduino-flasher-cli/internal/updater" + flasher "github.com/arduino/arduino-flasher-cli/rpc/cc/arduino/flasher/v1" +) + +func (s *flasherServerImpl) Flash(req *flasher.FlashRequest, stream flasher.Flasher_FlashServer) error { + // Setup callback functions + var responseCallback func(*flasher.FlashResponse) error + if stream != nil { + syncSend := NewSynchronizedSend(stream.Send) + responseCallback = syncSend.Send + } else { + responseCallback = func(*flasher.FlashResponse) error { return nil } + } + ctx := stream.Context() + downloadCB := func(msg *flasher.DownloadProgress) { + responseCallback(&flasher.FlashResponse{ + Message: &flasher.FlashResponse_DownloadProgress{ + DownloadProgress: &flasher.DownloadProgress{ + Message: msg.GetMessage(), + }, + }, + }) + } + extractCB := func(msg *flasher.TaskProgress) { + responseCallback(&flasher.FlashResponse{ + Message: &flasher.FlashResponse_ExtractionProgress{ + ExtractionProgress: &flasher.TaskProgress{ + Name: msg.GetName(), + Message: msg.GetMessage(), + Completed: msg.GetCompleted(), + }, + }, + }) + } + flashCB := func(msg *flasher.TaskProgress) { + responseCallback(&flasher.FlashResponse{ + Message: &flasher.FlashResponse_FlashProgress{ + FlashProgress: &flasher.TaskProgress{ + Name: msg.GetName(), + Message: msg.GetMessage(), + Completed: msg.GetCompleted(), + Progress: msg.GetProgress(), + Total: msg.GetTotal(), + }, + }, + }) + } + + // Check if there is enough free disk space before downloading and extracting an image + d, err := disk.Usage(req.TempPath) + if err != nil { + return err + } + if d.Free/updater.GiB < updater.DownloadDiskSpace { + return fmt.Errorf("download and extraction requires up to %d GiB of free space", updater.DownloadDiskSpace) + } + + client := updater.NewClient() + manifest, err := client.GetInfoManifest(ctx) + if err != nil { + return err + } + + var rel *updater.Release + if req.Version == "latest" || req.Version == manifest.Latest.Version { + rel = &manifest.Latest + } else { + for _, r := range manifest.Releases { + if req.Version == r.Version { + rel = &r + break + } + } + } + + if rel == nil { + return fmt.Errorf("could not find Debian image %s", req.Version) + } + + tmpZip := paths.New(req.GetTempPath(), "arduino-unoq-debian-image-"+rel.Version+".tar.zst") + + if err := updater.DownloadFile(ctx, tmpZip, rel.Url, rel.Version, downloadCB, downloader.Config{}); err != nil { + return err + } + defer tmpZip.RemoveAll() + + tmpZipFile, err := tmpZip.Open() + if err != nil { + return fmt.Errorf("could not open archive: %w", err) + } + defer tmpZipFile.Close() + + extractCB(&flasher.TaskProgress{Name: "extract", Message: "Start"}) + if err := extract.Archive(ctx, tmpZipFile, tmpZip.Parent().String(), func(s string) string { + extractCB(&flasher.TaskProgress{Name: "extract", Message: s}) + return s + }); err != nil { + return fmt.Errorf("could not extract archive: %w", err) + } + extractCB(&flasher.TaskProgress{Name: "extract", Completed: true}) + + imagePath := tmpZip.Parent().Join("arduino-unoq-debian-image-" + rel.Version) + defer imagePath.RemoveAll() + + flashCB(&flasher.TaskProgress{Name: "flash", Message: "Starting"}) + if err := updater.FlashBoard(ctx, imagePath.String(), rel.Version, req.GetPreserveUser(), func(fe updater.FlashEvent) { + switch fe.Type { + case updater.EventLog: + flashCB(&flasher.TaskProgress{ + Name: "flash", + Message: fe.Log, + }) + case updater.EventProgress: + flashCB(&flasher.TaskProgress{ + Name: "flash", + Message: fe.Log, + Progress: int64(fe.Progress), + Total: int64(fe.Total), + }) + } + + }); err != nil { + return err + } + flashCB(&flasher.TaskProgress{Name: "flash", Completed: true}) + + return responseCallback(&flasher.FlashResponse{ + Message: &flasher.FlashResponse_Result_{ + Result: &flasher.FlashResponse_Result{}, + }, + }) +}