From 55904135749a8ecb53fe1ad718c6b5d61ea9a785 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 7 Jul 2024 23:37:37 +0200 Subject: [PATCH 1/5] Implement firmware updater tool from scratch. --- .gitignore | 16 +- devices.txt | 22 +- functions.sh | 4 +- generate-updater-win.sh | 30 +- go/Makefile | 100 +++ go/cmd/updater/main.go | 32 + go/cmd/updater/ui.go | 448 +++++++++++++ go/go.mod | 39 +- go/go.sum | 92 ++- go/pkg/fastboot/context_io.go | 11 + go/pkg/fastboot/fastboot.go | 357 ++++++++++ go/pkg/updater/assets/test/lorem_ipsum.txt | 10 + go/pkg/updater/assets/test/lorem_ipsum.txt.xz | Bin 0 -> 1232 bytes go/pkg/updater/config.go | 30 + go/pkg/updater/reader_monitor.go | 22 + go/pkg/updater/reader_seeker_monitor.go | 29 + go/pkg/updater/updater.go | 618 ++++++++++++++++++ go/pkg/updater/xz.go | 90 +++ go/pkg/updater/xz_go.go | 15 + go/pkg/updater/xz_libxz.go | 15 + go/pkg/updater/xz_test.go | 21 + go/pkg/updater/xz_util.go | 39 ++ sfx-config.txt | 2 +- 23 files changed, 2003 insertions(+), 39 deletions(-) create mode 100644 go/Makefile create mode 100644 go/cmd/updater/main.go create mode 100644 go/cmd/updater/ui.go create mode 100644 go/pkg/fastboot/context_io.go create mode 100644 go/pkg/fastboot/fastboot.go create mode 100644 go/pkg/updater/assets/test/lorem_ipsum.txt create mode 100644 go/pkg/updater/assets/test/lorem_ipsum.txt.xz create mode 100644 go/pkg/updater/config.go create mode 100644 go/pkg/updater/reader_monitor.go create mode 100644 go/pkg/updater/reader_seeker_monitor.go create mode 100644 go/pkg/updater/updater.go create mode 100644 go/pkg/updater/xz.go create mode 100644 go/pkg/updater/xz_go.go create mode 100644 go/pkg/updater/xz_libxz.go create mode 100644 go/pkg/updater/xz_test.go create mode 100644 go/pkg/updater/xz_util.go diff --git a/.gitignore b/.gitignore index 790d9ab..350b37b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,13 @@ *.exe* *.img *.sfx -buildroot/ -engineos/* -!engineos/.gitkeep -media/ -unpacked-img/ -updater/ +*.dll +*.so +/go/updater +/go/find_update +/buildroot/ +/engineos/* +!/engineos/.gitkeep +/media/ +/unpacked-img/ +/updater/ diff --git a/devices.txt b/devices.txt index 06aef2d..36e02e2 100644 --- a/devices.txt +++ b/devices.txt @@ -1,11 +1,11 @@ -numark mixstreampro NH08 MIXSTREAMPRO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPRO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+4.0.0+Updater.exe -numark mixstreamprogo --- MIXSTREAMPROGO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPROGO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+Go+4.0.0+Updater.exe -numark mixstreamproplus --- MIXSTREAMPROPLUS https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPROPLUS-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+Plus+4.0.0+Updater.exe -denon prime4 JC11 PRIME4 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME4-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+4+4.0.0+Updater.exe -denon prime4plus --- PRIME4PLUS https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME4PLUS-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+4+Plus+4.0.0+Updater.exe -denon primego JP11 PRIMEGO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIMEGO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+GO+4.0.0+Updater.exe -denon prime2 JC16 PRIME2 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME2-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+2+4.0.0+Updater.exe -denon sc6000prime JP13 SC6000 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000+Prime+4.0.0+Updater.exe -denon sc6000mprime JP14 SC6000M https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000M-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000M+Prime+4.0.0+Updater.exe -denon sc5000prime JP07 SC5000 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000+Prime+4.0.0+Updater.exe -denon sc5000mprime JP08 SC5000M https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000M-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000M+Prime+4.0.0+Updater.exe +numark mixstreampro NH08 MIXSTREAMPRO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPRO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+4.0.0+Updater.exe Numark Mixstream Pro +numark mixstreamprogo --- MIXSTREAMPROGO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPROGO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+Go+4.0.0+Updater.exe Numark Mixstream Pro Go +numark mixstreamproplus --- MIXSTREAMPROPLUS https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/MIXSTREAMPROPLUS-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Mixstream+Pro+Plus+4.0.0+Updater.exe Numark Mixstream Pro Plus +denon prime4 JC11 PRIME4 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME4-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+4+4.0.0+Updater.exe Denon DJ PRIME 4 +denon prime4plus --- PRIME4PLUS https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME4PLUS-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+4+Plus+4.0.0+Updater.exe Denon DJ PRIME 4+ +denon primego JP11 PRIMEGO https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIMEGO-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+GO+4.0.0+Updater.exe Denon DJ PRIME GO +denon prime2 JC16 PRIME2 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/PRIME2-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/Prime+2+4.0.0+Updater.exe Denon DJ PRIME 2 +denon sc6000prime JP13 SC6000 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000+Prime+4.0.0+Updater.exe Denon DJ SC6000 PRIME +denon sc6000mprime JP14 SC6000M https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000M-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC6000M+Prime+4.0.0+Updater.exe Denon DJ SC6000M PRIME +denon sc5000prime JP07 SC5000 https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000+Prime+4.0.0+Updater.exe Denon DJ SC5000 PRIME +denon sc5000mprime JP08 SC5000M https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000M-4.0.0-Update.img https://enginedj.inmusiccdn.com/Engine/4.0.0/Release/af2cdcfdbe977f25/SC5000M+Prime+4.0.0+Updater.exe Denon DJ SC5000M PRIME diff --git a/functions.sh b/functions.sh index e4f814a..3635423 100644 --- a/functions.sh +++ b/functions.sh @@ -70,8 +70,9 @@ device_update_download_filename= device_application_name= device_updater_win_download_url= device_updater_win_download_filename= +device_name= -while read -r current_vendor current_device current_device_id current_device_application_name current_img_download_url current_updater_download_url _; do +while read -r current_vendor current_device current_device_id current_device_application_name current_img_download_url current_updater_download_url current_device_name; do if [ "$current_vendor" = "$vendor" ] && [ "$current_device" = "$device" ]; then device_id="$current_device_id" device_update_download_url="$current_img_download_url" @@ -80,6 +81,7 @@ while read -r current_vendor current_device current_device_id current_device_app device_updater_win_download_url="${current_updater_download_url}" fi device_application_name="$current_device_application_name" + device_name="$current_device_name" break fi done &2 -fi +make -C go all-windows-amd64 + +tempdir=$(mktemp -d) +trap 'rm -rf ${tempdir}' EXIT for file in "${files[@]}"; do - cp -v "${file}" "${output_dir}/win"/update.img dtb_name="$(basename "${file}" .dtb)" + dtb_dir="$(dirname "$dtb_name")" + + # generate config file + cat >"$tempdir"/config.toml <"$exe_name" diff --git a/go/Makefile b/go/Makefile new file mode 100644 index 0000000..bce6e06 --- /dev/null +++ b/go/Makefile @@ -0,0 +1,100 @@ +TRIPLET=x86_64-linux-gnu +CC=$(TRIPLET)-gcc +CXX=$(TRIPLET)-g++ +PREFIX=/usr +WINDOWS_AMD64_TRIPLET=x86_64-w64-mingw32 +WINDOWS_AMD64_PREFIX=/usr/$(WINDOWS_AMD64_TRIPLET) +WINDOWS_386_TRIPLET=i686-w64-mingw32 +WINDOWS_386_PREFIX=/usr/$(WINDOWS_386_TRIPLET) +GOTAGS=libxz +LINUX_AMD64_TRIPLET=x86_64-linux-gnu +GOOS= +GOARCH= +GOBUILDFLAGS= +CGO_ENABLED=1 +DLLEXT=.so +BINEXT= +LINUX_DLL_BLACKLIST="^(libc.so)([\.\d+]+)\$$" +WINDOWS_DLL_BLACKLIST="^(advapi32.dll|comctl32.dll|comdlg32.dll|gdi32.dll|kernel32.dll|gdi32.dll|gdiplus.dll|glu32.dll|kernel32.dll|msvcrt.dll|ole32.dll|opengl32.dll|shell32.dll|user32.dll|ws2_32.dll)\$$" +DLL_BLACKLIST=$(WINDOWS_DLL_BLACKLIST) +LIB_PREFIX=$(PREFIX)/lib +BINS=find_update updater +GOGUIBUILDFLAGS= +GOBUILDLDFLAGS="-s -w" +GOBUILDFLAGS= + +export CC CXX GOOS GOARCH CGO_ENABLED + +all: $(addsuffix $(BINEXT),$(BINS)) + +# +# WINDOWS AMD64 BUILD CONVENIENCE TASKS +# + +.PHONY: all-windows-amd64 + +all-windows-amd64: + $(MAKE) \ + GOOS=windows \ + GOARCH=amd64 \ + TRIPLET=$(WINDOWS_AMD64_TRIPLET) \ + PREFIX=$(WINDOWS_AMD64_PREFIX) \ + LIB_PREFIX=$(WINDOWS_AMD64_PREFIX)/bin \ + GOGUIBUILDFLAGS='"-H windowsgui"' \ + BINEXT=.exe \ + DLLEXT=.dll \ + all \ + $(addprefix copy-libs-,$(addsuffix .exe,$(BINS))) + +.PHONY: clean-windows-amd64 + +clean-windows-amd64: + $(MAKE) \ + GOOS=windows \ + GOARCH=amd64 \ + TRIPLET=$(WINDOWS_AMD64_TRIPLET) \ + PREFIX=$(WINDOWS_AMD64_PREFIX) \ + LIB_PREFIX=$(WINDOWS_AMD64_PREFIX)/bin \ + GOGUIBUILDFLAGS="-H windowsgui" \ + BINEXT=.exe \ + DLLEXT=.dll \ + clean + +# +# MAIN BINARY TARGETS +# + +find_update$(BINEXT): go.mod go.sum $(wildcard ./pkg/**/*.go ./cmd/find_update/**/*.go) + go build -tags $(GOTAGS) -ldflags $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/find_update + +updater$(BINEXT): go.mod go.sum $(wildcard ./pkg/**/*.go ./cmd/updater/**/*.go) + go build -tags $(GOTAGS) -ldflags $(GOGUIBUILDFLAGS)\ $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/updater + +# +# PACKAGING TASKS +# + +.PHONY: copy-libs-% + +copy-libs-%: % + $(MAKE) -s TARGET=$< copy-libs + +.PHONY: copy-libs + +# Extract list of DLL files that are used by imports. +# +# Windows output only. +copy-libs: + $(TRIPLET)-objdump -p $(TARGET) | \ + grep -F "DLL Name:" | \ + sed -e "s/\t*DLL Name: //g" | \ + grep -Evi $(DLL_BLACKLIST) | \ + tr "\n" " " | \ + xargs -r -t $(MAKE) + +%$(DLLEXT): $(LIB_PREFIX)/%$(DLLEXT) + cp $< $@ + $(MAKE) -s TARGET=$@ copy-libs + +clean: + $(RM) $(wildcard updater$(BINEXT) *$(DLLEXT)) diff --git a/go/cmd/updater/main.go b/go/cmd/updater/main.go new file mode 100644 index 0000000..1f66079 --- /dev/null +++ b/go/cmd/updater/main.go @@ -0,0 +1,32 @@ +package main + +import ( + "flag" + "fmt" + "log" + "log/slog" +) + +var ( + flagDebugLevel = flag.Int("debug", int(slog.LevelError), fmt.Sprintf("logging level (ranges from %d for debug to %d for error)", int(slog.LevelDebug), int(slog.LevelError))) + flagLibusbDebugLevel = flag.Int("libusb_debug", 0, fmt.Sprintf("libusb debug level (%d..%d)", 0, 3)) + flagSkipRebootAfterFlash = flag.Bool("skip_reboot", false, "Whether to skip reboot after flashing") + flagDryRun = flag.Bool("dry", false, "Enable dry run (device still needs to be plugged in but will not actually flash)") +) + +func main() { + err := run() + if err != nil { + log.Fatal(err) + } +} + +func run() error { + flag.Parse() + + if err := initUI(); err != nil { + panic(err) + } + + return nil +} diff --git a/go/cmd/updater/ui.go b/go/cmd/updater/ui.go new file mode 100644 index 0000000..fb03990 --- /dev/null +++ b/go/cmd/updater/ui.go @@ -0,0 +1,448 @@ +package main + +import ( + "context" + "errors" + "image" + "image/color" + "log" + "log/slog" + "os" + "reflect" + "strings" + "time" + + "gioui.org/app" + "gioui.org/font" + "gioui.org/font/gofont" + "gioui.org/layout" + "gioui.org/op" + "gioui.org/op/clip" + "gioui.org/op/paint" + "gioui.org/text" + "gioui.org/unit" + "gioui.org/widget" + "gioui.org/widget/material" + "github.com/google/gousb" + "github.com/icedream/denon-prime4/go/pkg/fastboot" + "github.com/icedream/denon-prime4/go/pkg/updater" + "github.com/knadh/koanf/parsers/toml" + "github.com/knadh/koanf/providers/file" + "github.com/knadh/koanf/v2" + "github.com/roblillack/spot/ui" +) + +func doLayout(graphicsCtx layout.Context, state *State) { + theme := material.NewTheme() + // Flip to the dark side + palettedTheme := theme.WithPalette(material.Palette{ + Bg: color.NRGBA{0x1a, 0x1a, 0x1a, 0xff}, + Fg: color.NRGBA{0xff, 0xff, 0xff, 0xff}, + + ContrastBg: color.NRGBA{0x11, 0xaa, 0x33, 0xff}, + ContrastFg: color.NRGBA{0xff, 0xff, 0xff, 0xff}, + }) + theme = &palettedTheme + + // Font + fontCollection := gofont.Collection() + theme.Shaper = text.NewShaper(text.NoSystemFonts(), text.WithCollection(fontCollection)) + + // set the background color + macro := op.Record(graphicsCtx.Ops) + rect := image.Rectangle{ + Max: image.Point{ + X: graphicsCtx.Constraints.Max.X, + Y: graphicsCtx.Constraints.Max.Y, + }, + } + paint.FillShape(graphicsCtx.Ops, theme.Palette.Bg, clip.Rect(rect).Op()) + background := macro.Stop() + + background.Add(graphicsCtx.Ops) + + flex := layout.Flex{ + // Vertical alignment, from top to bottom + Axis: layout.Vertical, + // Empty space is left at the start, i.e. at the top + Spacing: layout.SpaceAround, + } + flex.Layout( + graphicsCtx, + // logo + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + margins := layout.Inset{ + Top: unit.Dp(10), + Bottom: unit.Dp(10), + Left: unit.Dp(10), + Right: unit.Dp(10), + } + return margins.Layout( + gtx, + // The height of the spacer is 25 Device independent pixels + layout.Spacer{Height: unit.Dp(25)}.Layout, + ) + }, + ), + // title + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + margins := layout.Inset{ + Top: unit.Dp(10), + Bottom: unit.Dp(10), + Left: unit.Dp(10), + Right: unit.Dp(10), + } + return margins.Layout( + gtx, + func(gtx layout.Context) layout.Dimensions { + title := material.H2(theme, "Firmware updater") + title.Alignment = text.Middle + return title.Layout(gtx) + }, + ) + }, + ), + // list of supported devices for this updater + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + margins := layout.Inset{ + Top: unit.Dp(10), + Bottom: unit.Dp(10), + Left: unit.Dp(10), + Right: unit.Dp(10), + } + return margins.Layout( + gtx, + func(gtx layout.Context) layout.Dimensions { + txt := "" + for _, device := range state.Devices { + txt += device.Name + "\n" + } + + deviceTitle := material.Body1(theme, txt) + deviceTitle.Alignment = text.Middle + return deviceTitle.Layout(gtx) + }, + ) + }, + ), + // progress panel + layout.Flexed( + 0.2, + func(gtx layout.Context) layout.Dimensions { + margins := layout.Inset{ + Top: unit.Dp(10), + Bottom: unit.Dp(10), + Left: unit.Dp(10), + Right: unit.Dp(10), + } + return margins.Layout( + gtx, + func(gtx layout.Context) layout.Dimensions { + children := []layout.FlexChild{} + + if state.isFlashRunning { + children = append(children, + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + statusBody := material.Body2(theme, "") + if state.flashProgress != nil { + statusBody.Text = state.flashProgress.Text + } + statusBody.Alignment = text.Middle + return statusBody.Layout(gtx) + }, + ), + ) + } else { + children = append(children, + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + statusBody := material.Body2(theme, state.finalProgressText) + statusBody.Alignment = text.Middle + if state.isFlashFailed { + statusBody.Color = color.NRGBA{0xaa, 0x22, 0x22, 0xff} + } else if state.isFlashDone { + statusBody.Color = color.NRGBA{0x11, 0xaa, 0x33, 0xff} + } + return statusBody.Layout(gtx) + }, + ), + ) + } + + if state.flashProgress != nil && !state.flashProgress.Indetermined { + children = append(children, + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + progressBar := material.ProgressBar(theme, 0) + progressBar.Height = theme.FingerSize + progressBar.Radius = 2 + if state.flashProgress != nil && + !state.flashProgress.Indetermined { + progressBar.Progress = float32(state.flashProgress.Percentage) + } + return progressBar.Layout(gtx) + }, + ), + ) + } else { + children = append(children, + layout.Rigid( + func(gtx layout.Context) layout.Dimensions { + // disable the button if a flash is already running + if state.isFlashRunning { + gtx = gtx.Disabled() + } + + btn := material.Button(theme, &state.startButton, "START UPDATE") + btn.Font.Weight = font.Bold + if state.flashProgress != nil { + btn.Text = "UPDATING..." + btn.Background = color.NRGBA{0x00, 0x00, 0x00, 0x7f} + } else { + btn.Background = color.NRGBA{0x11, 0xaa, 0x33, 0xff} + btn.Color = color.NRGBA{0xff, 0xff, 0xff, 0xff} + } + return btn.Layout(gtx) + }, + ), + ) + } + + // render current progress instead + return layout.Flex{ + Axis: layout.Vertical, + Spacing: layout.SpaceStart, + }.Layout(gtx, children...) + }, + ) + }, + ), + ) +} + +type State struct { + window *app.Window + + ops op.Ops + startButton widget.Clickable + flashProgress *updater.Progress + finalProgressText string + isFlashDone bool + isFlashFailed bool + isFlashRunning bool + + Devices []updater.DeviceConfig +} + +type Err struct { + Err error + ErrType reflect.Type +} + +func errChain(err error) []Err { + errs := []Err{} + for unwrappedErr := err; unwrappedErr != nil; unwrappedErr = errors.Unwrap(unwrappedErr) { + errs = append(errs, Err{Err: unwrappedErr, ErrType: reflect.TypeOf(unwrappedErr)}) + } + return errs +} + +const ( + corruptedMessage = "Please redownload this software." + maybeCorruptedMessage = "Make sure your copy of this software has not been corrupted." + closeAppsMessage = "Make sure to close any application that may interact with the device, reconnect it and retry the update." + runAdminMessage = "Please run this app with higher privileges (e.g. as administrator)." + retryMessage = "Please reconnect the device and retry the update." +) + +func buildMessage(messages ...string) string { + return strings.Join(messages, " ") +} + +func triggerUpdate(u *updater.Updater, state *State) { + window := state.window + + state.isFlashDone = false + state.isFlashFailed = false + state.finalProgressText = "" + state.flashProgress = nil + state.isFlashRunning = true + defer func() { + state.isFlashRunning = false + }() + + progressC := make(chan updater.Progress, 1) + go func() { + ticker := time.NewTicker(8 * time.Millisecond) + defer func() { + ticker.Stop() + window.Invalidate() + }() + for { + select { + case progress, ok := <-progressC: + if !ok { + return + } + state.flashProgress = &progress + case <-ticker.C: + window.Invalidate() + } + } + }() + + err := u.Run(progressC) + state.isFlashDone = true + state.flashProgress = nil + if err != nil { + log.Println("Update failed:", err) + state.isFlashFailed = true + message := err.Error() + var usbTransferStatus gousb.TransferStatus + switch { + case errors.Is(err, gousb.ErrorAccess): + message = buildMessage("Permission denied.", runAdminMessage) + case errors.Is(err, gousb.ErrorBusy): + message = buildMessage("Device is busy.", closeAppsMessage) + case errors.Is(err, gousb.ErrorInterrupted): + message = buildMessage("Communication with the device was interrupted.", retryMessage) + case errors.Is(err, gousb.ErrorNoDevice): + message = buildMessage("Device is no longer present.", retryMessage) + case errors.Is(err, gousb.ErrorNotFound): + message = buildMessage("Device was not found.", retryMessage) + case errors.Is(err, gousb.ErrorNotSupported): + message = buildMessage("An operation necessary for the update process is not supported.") + case errors.Is(err, gousb.ErrorTimeout), + errors.Is(err, gousb.TransferTimedOut), + errors.Is(err, context.Canceled): + message = buildMessage("Communication with the device was canceled or has timed out.", retryMessage) + case errors.As(err, &usbTransferStatus): + switch usbTransferStatus { + case gousb.TransferError: + message = "USB transfer failed." + case gousb.TransferTimedOut: + message = "USB transfer timed out." + case gousb.TransferStall: + message = "Communication with the device was halted." + case gousb.TransferNoDevice: + message = "Communication with the device was lost." + default: + message = "USB transfer entered an unexpected state." + } + message = buildMessage(message, retryMessage) + case errors.Is(err, fastboot.ErrUnexpectedResponse): + message = buildMessage("An unexpected response was sent by the device.", retryMessage) + case errors.Is(err, updater.ErrNoImagesInDeviceTree): + message = buildMessage("Firmware update does not contain any flashable images.", maybeCorruptedMessage) + case errors.Is(err, updater.ErrBadVersion): + message = buildMessage("Firmware update does not contain valid version information.", maybeCorruptedMessage) + case errors.Is(err, updater.ErrChecksumMismatch): + message = buildMessage("The firmware update seems to have been corrupted.", corruptedMessage) + case errors.Is(err, updater.ErrFooterMagicMismatch): + message = buildMessage("Part of the firmware update seems to have been corrupted.", corruptedMessage) + case errors.Is(err, updater.ErrMissingVersion): + message = buildMessage("Firmware update does not contain version information.", corruptedMessage) + case errors.Is(err, updater.ErrNoMatchingDevices): + message = "No matching devices were found. Please plug in one of the devices listed above and reboot it into bootloader mode. Check the manual for instructions." + case errors.Is(err, updater.ErrUnsupportedConfiguration): + message = "Unsupported configuration." + default: + slog.Warn("Unknown error type", + "err", err, + "errType", reflect.TypeOf(err), + "errChain", errChain(err)) + } + state.finalProgressText = "Update failed.\n\n" + message + } else { + state.finalProgressText = "Update succeeded." + } + window.Invalidate() +} + +func runUI(u *updater.Updater, window *app.Window) error { + var state State + state.window = window + state.Devices = u.Config().Devices + for { + switch event := window.Event().(type) { + case app.DestroyEvent: + return event.Err + case app.FrameEvent: + // This graphics context is used for managing the rendering state. + graphicsCtx := app.NewContext(&state.ops, event) + + if state.startButton.Clicked(graphicsCtx) && !state.isFlashRunning { + go triggerUpdate(u, &state) + } + + doLayout(graphicsCtx, &state) + + // Pass the drawing operations to the GPU. + event.Frame(graphicsCtx.Ops) + } + } +} + +func initUI() error { + ui.Init() + + // Load config + k := koanf.New(".") + parser := toml.Parser() + if err := k.Load(file.Provider("config.toml"), parser); err != nil { + log.Fatalf("error loading config: %v", err) + } + var config updater.Config + if err := k.Unmarshal("", &config); err != nil { + return err + } + + programLevel := new(slog.LevelVar) // Info by default + logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{ + Level: programLevel, + })) + slog.SetDefault(logger) + + // set log level if requested + if flagDebugLevel != nil { + programLevel.Set(slog.Level(*flagDebugLevel)) + } + + // set libusb debug level if requested + if flagLibusbDebugLevel != nil { + config.LibusbDebugLevel = *flagLibusbDebugLevel + } + + if flagSkipRebootAfterFlash != nil { + config.SkipRebootAfterFlash = *flagSkipRebootAfterFlash + } + + updater, err := updater.NewUpdater(config, logger) + if err != nil { + logger.Error("Failed to initialize updater", + "err", err) + } + if flagDryRun != nil { + updater.DryRun = *flagDryRun + } + + go func() { + window := new(app.Window) + window.Option( + app.Title("Updater"), + app.Size(unit.Dp(550), unit.Dp(400)), + app.MinSize(unit.Dp(500), unit.Dp(330)), + ) + err := runUI(updater, window) + if err != nil { + log.Fatal(err) + } + os.Exit(0) + }() + app.Main() + return nil +} diff --git a/go/go.mod b/go/go.mod index 5ee5130..38e2845 100644 --- a/go/go.mod +++ b/go/go.mod @@ -1,10 +1,45 @@ module github.com/icedream/denon-prime4/go -go 1.19 +go 1.22.4 -require github.com/PuerkitoBio/goquery v1.9.1 +require ( + gioui.org v0.7.0 + github.com/PuerkitoBio/goquery v1.9.1 + github.com/dustin/go-humanize v1.0.1 + github.com/google/gousb v1.1.3 + github.com/jamespfennell/xz v0.1.2 + github.com/knadh/koanf/parsers/toml v0.1.0 + github.com/knadh/koanf/providers/file v1.0.0 + github.com/knadh/koanf/v2 v2.1.1 + github.com/roblillack/spot v0.3.2 + github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 + github.com/stretchr/testify v1.8.1 + github.com/u-root/u-root v0.14.0 + github.com/ulikunitz/xz v0.5.11 +) require ( + gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 // indirect + gioui.org/shader v1.0.8 // indirect + github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf // indirect github.com/andybalholm/cascadia v1.3.2 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/fsnotify/fsnotify v1.7.0 // indirect + github.com/go-text/typesetting v0.1.1 // indirect + github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 // indirect + github.com/knadh/koanf/maps v0.1.1 // indirect + github.com/mitchellh/copystructure v1.2.0 // indirect + github.com/mitchellh/reflectwalk v1.0.2 // indirect + github.com/pelletier/go-toml v1.9.5 // indirect + github.com/pierrec/lz4/v4 v4.1.14 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/pwiecz/go-fltk v0.0.0-20240511142305-990b442ae1ed // indirect + github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a // indirect + golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect + golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 // indirect + golang.org/x/image v0.18.0 // indirect golang.org/x/net v0.21.0 // indirect + golang.org/x/sys v0.21.0 // indirect + golang.org/x/text v0.16.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go/go.sum b/go/go.sum index adb1774..44575bb 100644 --- a/go/go.sum +++ b/go/go.sum @@ -1,26 +1,87 @@ -github.com/PuerkitoBio/goquery v1.8.0 h1:PJTF7AmFCFKk1N6V6jmKfrNH9tV5pNE6lZMkG0gta/U= -github.com/PuerkitoBio/goquery v1.8.0/go.mod h1:ypIiRMtY7COPGk+I/YbZLbxsxn9g5ejnI2HSMtkjZvI= -github.com/PuerkitoBio/goquery v1.8.1 h1:uQxhNlArOIdbrH1tr0UXwdVFgDcZDrZVdcpygAcwmWM= -github.com/PuerkitoBio/goquery v1.8.1/go.mod h1:Q8ICL1kNUJ2sXGoAhPGUdYDJvgQgHzJsnnd3H7Ho5jQ= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d h1:ARo7NCVvN2NdhLlJE9xAbKweuI9L6UgfTbYb0YwPacY= +eliasnaur.com/font v0.0.0-20230308162249-dd43949cb42d/go.mod h1:OYVuxibdk9OSLX8vAqydtRPP87PyTFcT9uH3MlEGBQA= +gioui.org v0.7.0 h1:5I+7Uu2yjTu7W5p7HWQrgsDPO3vex+8T1DsvCLGBfuI= +gioui.org v0.7.0/go.mod h1:19wZxaNP+eHN4H2YdZwEfbkAAgoYB5rcIbDHo4BqUl4= +gioui.org/cpu v0.0.0-20210808092351-bfe733dd3334/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2 h1:AGDDxsJE1RpcXTAxPG2B4jrwVUJGFDjINIPi1jtO6pc= +gioui.org/cpu v0.0.0-20210817075930-8d6a761490d2/go.mod h1:A8M0Cn5o+vY5LTMlnRoK3O5kG+rH0kWfJjeKd9QpBmQ= +gioui.org/shader v1.0.8 h1:6ks0o/A+b0ne7RzEqRZK5f4Gboz2CfG+mVliciy6+qA= +gioui.org/shader v1.0.8/go.mod h1:mWdiME581d/kV7/iEhLmUgUK5iZ09XR5XpduXzbePVM= github.com/PuerkitoBio/goquery v1.9.1 h1:mTL6XjbJTZdpfL+Gwl5U2h1l9yEkJjhmlTeV9VPW7UI= github.com/PuerkitoBio/goquery v1.9.1/go.mod h1:cW1n6TmIMDoORQU5IU/P1T3tGFunOeXEpGP2WHRwkbY= -github.com/andybalholm/cascadia v1.3.1 h1:nhxRkql1kdYCc8Snf7D5/D3spOX+dBgjA6u8x004T2c= -github.com/andybalholm/cascadia v1.3.1/go.mod h1:R4bJ1UQfqADjvDa4P6HZHLh/3OxWWEqc0Sk8XGwHqvA= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf h1:FPsprx82rdrX2jiKyS17BH6IrTmUBYqZa/CXT4uvb+I= +github.com/TheTitanrain/w32 v0.0.0-20180517000239-4f5cfb03fabf/go.mod h1:peYoMncQljjNS6tZwI9WVyQB3qZS6u79/N3mBOcnd3I= github.com/andybalholm/cascadia v1.3.2 h1:3Xi6Dw5lHF15JtdcmAHD3i1+T8plmv7BQ/nsViSLyss= github.com/andybalholm/cascadia v1.3.2/go.mod h1:7gtRlve5FxPPgIgX36uWBX58OdBsSS6lUvCFb+h7KvU= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= +github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= +github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA= +github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM= +github.com/go-text/typesetting v0.1.1 h1:bGAesCuo85nXnEN5LmFMVGAGpGkCPtHrZLi//qD7EJo= +github.com/go-text/typesetting v0.1.1/go.mod h1:d22AnmeKq/on0HNv73UFriMKc4Ez6EqZAofLhAzpSzI= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04 h1:zBx+p/W2aQYtNuyZNcTfinWvXBQwYtDfme051PR/lAY= +github.com/go-text/typesetting-utils v0.0.0-20231211103740-d9332ae51f04/go.mod h1:DDxDdQEnB70R8owOx3LVpEFvpMK9eeH1o2r0yZhFI9o= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1 h1:TQcrn6Wq+sKGkpyPvppOz99zsMBaUOKXq6HSv655U1c= +github.com/go-viper/mapstructure/v2 v2.0.0-alpha.1/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/google/gousb v1.1.3 h1:xt6M5TDsGSZ+rlomz5Si5Hmd/Fvbmo2YCJHN+yGaK4o= +github.com/google/gousb v1.1.3/go.mod h1:GGWUkK0gAXDzxhwrzetW592aOmkkqSGcj5KLEgmCVUg= +github.com/jamespfennell/xz v0.1.2 h1:iCw5kScLfGCceOKgQaGuj5RilAAlV4iiwauYntak2oU= +github.com/jamespfennell/xz v0.1.2/go.mod h1:DhpWvZY1xDkK/6BREFl3c3R/fZh7IBdYq2m7xh4uLl0= +github.com/knadh/koanf/maps v0.1.1 h1:G5TjmUh2D7G2YWf5SQQqSiHRJEjaicvU0KpypqB3NIs= +github.com/knadh/koanf/maps v0.1.1/go.mod h1:npD/QZY3V6ghQDdcQzl1W4ICNVTkohC8E73eI2xW4yI= +github.com/knadh/koanf/parsers/toml v0.1.0 h1:S2hLqS4TgWZYj4/7mI5m1CQQcWurxUz6ODgOub/6LCI= +github.com/knadh/koanf/parsers/toml v0.1.0/go.mod h1:yUprhq6eo3GbyVXFFMdbfZSo928ksS+uo0FFqNMnO18= +github.com/knadh/koanf/providers/file v1.0.0 h1:DtPvSQBeF+N0QLPMz0yf2bx0nFSxUcncpqQvzCxfCyk= +github.com/knadh/koanf/providers/file v1.0.0/go.mod h1:/faSBcv2mxPVjFrXck95qeoyoZ5myJ6uxN8OOVNJJCI= +github.com/knadh/koanf/v2 v2.1.1 h1:/R8eXqasSTsmDCsAyYj+81Wteg8AqrV9CP6gvsTsOmM= +github.com/knadh/koanf/v2 v2.1.1/go.mod h1:4mnTRbZCK+ALuBXHZMjDfG9y714L7TykVnZkXbMU3Es= +github.com/mitchellh/copystructure v1.2.0 h1:vpKXTN4ewci03Vljg/q9QvCGUDttBOGBIa15WveJJGw= +github.com/mitchellh/copystructure v1.2.0/go.mod h1:qLl+cE2AmVv+CoeAwDPye/v+N2HKCj9FbZEVFJRxO9s= +github.com/mitchellh/reflectwalk v1.0.2 h1:G2LzWKi524PWgd3mLHV8Y5k7s6XUvT0Gef6zxSIeXaQ= +github.com/mitchellh/reflectwalk v1.0.2/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= +github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= +github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= +github.com/pierrec/lz4/v4 v4.1.14 h1:+fL8AQEZtz/ijeNnpduH0bROTu0O3NZAlPjQxGn8LwE= +github.com/pierrec/lz4/v4 v4.1.14/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/pwiecz/go-fltk v0.0.0-20240511142305-990b442ae1ed h1:7/j/4x3KAwuJB9sWdfhi+1UFbv42TyJ4LyUI/GHS+p8= +github.com/pwiecz/go-fltk v0.0.0-20240511142305-990b442ae1ed/go.mod h1:uMK5daOr9p+ba2BPs5QadbfaqqrHR5TGj13yWGsAsmw= +github.com/roblillack/spot v0.3.2 h1:yqxCnkMk57Dc+o33dxh1c8ZXGx3kyMEV7GYkAP5z5OU= +github.com/roblillack/spot v0.3.2/go.mod h1:nTQK9qWQuL977BcNBN5deI/AouFbCuBv/o4PyyXlP9I= +github.com/sqweek/dialog v0.0.0-20240226140203-065105509627 h1:2JL2wmHXWIAxDofCK+AdkFi1KEg3dgkefCsm7isADzQ= +github.com/sqweek/dialog v0.0.0-20240226140203-065105509627/go.mod h1:/qNPSY91qTz/8TgHEMioAUc6q7+3SOybeKczHMXFcXw= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/u-root/u-root v0.14.0 h1:Ka4T10EEML7dQ5XDvO9c3MBN8z4nuSnGjcd1jmU2ivg= +github.com/u-root/u-root v0.14.0/go.mod h1:hAyZorapJe4qzbLWlAkmSVCJGbfoU9Pu4jpJ1WMluqE= +github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a h1:BH1SOPEvehD2kVrndDnGJiUF0TrBpNs+iyYocu6h0og= +github.com/u-root/uio v0.0.0-20240209044354-b3d14b93376a/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA= +github.com/ulikunitz/xz v0.5.11 h1:kpFauv27b6ynzBNT/Xy+1k+fK4WswhN/6PN5WhFAGw8= +github.com/ulikunitz/xz v0.5.11/go.mod h1:nbz6k7qbPmH4IRqmfOplQw/tblSgqTqBwxkY0oWt/14= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ= +golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc= +golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91 h1:ryT6Nf0R83ZgD8WnFFdfI8wCeyqgdXWN4+CkFVNPAT0= +golang.org/x/exp/shiny v0.0.0-20220827204233-334a2380cb91/go.mod h1:VjAR7z0ngyATZTELrBSkxOOHhhlnVUxDye4mcjx5h/8= +golang.org/x/image v0.18.0 h1:jGzIakQa/ZXI1I0Fxvaa9W7yP25TqT6cHIHn+6CqvSQ= +golang.org/x/image v0.18.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8 h1:/6y1LfuqNuQdHAm0jjtPtgRcxIxjVZgm5OTu8/QhZvk= -golang.org/x/net v0.0.0-20210916014120-12bc252f5db8/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= @@ -29,24 +90,31 @@ golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.21.0 h1:rF+pYz3DAGSQAxAu1CbC7catZg4ebC4UIeIhKxBZvws= +golang.org/x/sys v0.21.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/text v0.16.0 h1:a94ExnEXNtEwYLGJSIUxnWoxoRz/ZcCsV63ROupILh4= +golang.org/x/text v0.16.0/go.mod h1:GhwF1Be+LQoKShO3cGOHzqOgRrGaYc9AvblQOmPVHnI= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/go/pkg/fastboot/context_io.go b/go/pkg/fastboot/context_io.go new file mode 100644 index 0000000..44b049b --- /dev/null +++ b/go/pkg/fastboot/context_io.go @@ -0,0 +1,11 @@ +package fastboot + +import "context" + +type ContextReader interface { + ReadContext(context.Context, []byte) (int, error) +} + +type ContextWriter interface { + WriteContext(context.Context, []byte) (int, error) +} diff --git a/go/pkg/fastboot/fastboot.go b/go/pkg/fastboot/fastboot.go new file mode 100644 index 0000000..054eb05 --- /dev/null +++ b/go/pkg/fastboot/fastboot.go @@ -0,0 +1,357 @@ +package fastboot + +import ( + "bytes" + "context" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "io" + "log/slog" + "math" +) + +type responseType string + +const ( + okay responseType = "OKAY" + fail responseType = "FAIL" + data responseType = "DATA" + info responseType = "INFO" + text responseType = "TEXT" +) + +// FastBootError represents an error message returned by the FastBoot client +// device. +type FastBootError struct { + Message string +} + +// Error implements error. +func (f *FastBootError) Error() string { + return fmt.Sprintf("fastboot request failed: %s", f.Message) +} + +var _ error = (*FastBootError)(nil) + +type FastBootChannel struct { + infoC <-chan string + textC <-chan string + resultC <-chan []byte + readyForDataC <-chan uint32 + errorC <-chan error + + logger *slog.Logger + w ContextWriter +} + +var ( + ErrUnexpectedResponse = errors.New("unexpected response") + ErrMaxLengthExceeded = errors.New("max length exceeded") +) + +type UnexpectedDataSizeError struct { + Purpose string + ActualLength, ExpectedLength uint64 +} + +// Error implements error. +func (e *UnexpectedDataSizeError) Error() string { + return fmt.Sprintf("unexpected data size: expected %s size of %d but got %d instead", + e.Purpose, + e.ExpectedLength, + e.ActualLength) +} + +type TooShortPayloadError struct { + Purpose string + ActualLength, ExpectedLength uint64 +} + +// Error implements error. +func (e *TooShortPayloadError) Error() string { + return fmt.Sprintf("too short payload: expected %s length of %d but got %d instead", + e.Purpose, + e.ExpectedLength, + e.ActualLength) +} + +var _ error = (*TooShortPayloadError)(nil) + +func NewFastBootChannel( + ctx context.Context, + logger *slog.Logger, + r ContextReader, + w ContextWriter, +) *FastBootChannel { + infoC := make(chan string, 1) + textC := make(chan string, 1) + resultC := make(chan []byte, 1) + readyForDataC := make(chan uint32, 1) + errorC := make(chan error, 1) + + fb := &FastBootChannel{ + infoC: infoC, + textC: textC, + resultC: resultC, + readyForDataC: readyForDataC, + errorC: errorC, + w: w, + logger: logger, + } + + go func() { + defer close(infoC) + defer close(textC) + defer close(resultC) + defer close(errorC) + defer close(readyForDataC) + + var textBuf *bytes.Buffer + + msgBuf := make([]byte, 512) + for { + n, err := r.ReadContext(ctx, msgBuf) + if err != nil { + errorC <- fmt.Errorf("read error: %w", err) + return + } + if n < 4 { + errorC <- &TooShortPayloadError{ + Purpose: "message type", + ExpectedLength: 4, + ActualLength: uint64(n), + } + return + } + logger.Debug("H<-C\n" + hex.Dump(msgBuf[0:n])) + responseType := responseType(msgBuf[0:4]) + msgBuf = msgBuf[4:n] + switch responseType { + case okay: + resultC <- msgBuf + + case fail: + // rest of message provides text to present to the user + errorC <- &FastBootError{ + Message: string(msgBuf), + } + + case data: // ready for data, gives us uint32 size + if len(msgBuf) < 8 { + errorC <- &TooShortPayloadError{ + Purpose: "allocated data length", + ExpectedLength: 8, + ActualLength: uint64(len(msgBuf)), + } + return + } + dataLenBytes := make([]byte, 4) + if _, err := hex.Decode(dataLenBytes, msgBuf[0:8]); err != nil { + errorC <- err + } + readyForDataC <- binary.BigEndian.Uint32(dataLenBytes) + + case info: // informative message + infoC <- string(msgBuf) + + case text: // arbitrary data, null-terminated + for len(msgBuf) > 0 { + copyLength := len(msgBuf) + + // copy everything up to null terminator if one exists + nullIndex := bytes.IndexByte(msgBuf, 0) + if nullIndex >= 0 { + copyLength = nullIndex + } + textBuf.Write(msgBuf[0:copyLength]) + + // leave rest in msgBuf + msgBuf = msgBuf[copyLength:] + + // if there was a terminator, flush this payload + if nullIndex >= 0 { + textC <- textBuf.String() + textBuf.Reset() + + // skip null terminator on next read + msgBuf = msgBuf[1:] + } + } + + default: + errorC <- ErrUnexpectedResponse + return + } + } + }() + + return fb +} + +func (fb *FastBootChannel) InfoC() <-chan string { + return fb.infoC +} + +func (fb *FastBootChannel) TextC() <-chan string { + return fb.textC +} + +// Command executes an arbitrary formatted command. +// +// An error will be returned if a FAIL or any response type that is not OKAY, +// TEXT or INFO is transmitted back by the client. +func (fb *FastBootChannel) Command(ctx context.Context, cmd string, param ...interface{}) ([]byte, error) { + msg := []byte(fmt.Sprintf(cmd, param...)) + fb.logger.Debug("H->C\n" + hex.Dump(msg)) + if _, err := fb.w.WriteContext(ctx, msg); err != nil { + return nil, fmt.Errorf("write failed: %w", err) + } + select { + case data := <-fb.resultC: + return data, nil + case err := <-fb.errorC: + return nil, err + } +} + +func (fb *FastBootChannel) dataCommand( + ctx context.Context, + r io.Reader, + size uint32, + cmd string, + param ...interface{}, +) ([]byte, error) { + msg := []byte(fmt.Sprintf(cmd, param...)) + fb.logger.Debug("H->C\n" + hex.Dump(msg)) + if _, err := fb.w.WriteContext(ctx, msg); err != nil { + return nil, fmt.Errorf("write failed: %w", err) + } + // we expect DATA on success or FAIL on failure + select { + case data := <-fb.resultC: // okay + return data, ErrUnexpectedResponse + case err := <-fb.errorC: // error/fail + return nil, err + case returnedSize := <-fb.readyForDataC: // data + if returnedSize != size { + return nil, &UnexpectedDataSizeError{ + Purpose: "allocated data buffer", + ExpectedLength: uint64(size), + ActualLength: uint64(returnedSize), + } + } + } + + // now we stream our data to the client + buf := make([]byte, 16*1024) + for { + n, err := r.Read(buf) + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, err + } + fb.logger.Debug("H->C\n" + hex.Dump(buf[0:n])) + _, err = fb.w.WriteContext(ctx, buf[0:n]) + if err != nil { + return nil, err + } + } + + // and now we await the final response + select { + case result := <-fb.resultC: + return result, nil + case err := <-fb.errorC: + return nil, err + } +} + +// GetVar requests a config/version variable's contents from the bootloader. If +// the variable is unknown, either a *FastBootError is returned or the contents +// will be empty, depending on implementation on the hardware side. +func (fb *FastBootChannel) GetVar(ctx context.Context, varName string) (string, error) { + data, err := fb.Command(ctx, "getvar:%s", varName) + var content string + if data != nil { + content = string(data) + } + return content, err +} + +// Download writes data to the client device's memory to be later used by Boot, +// Flash, etc. +// +// The size to be transmitted is calculated from the offset at which r ends. +// +// The command will fail if there is not enough space in RAM or if the data size +// exceeds math.MaxUint32 in bytes. +func (fb *FastBootChannel) DownloadFromReadSeeker(ctx context.Context, r io.ReadSeeker) error { + len, err := r.Seek(0, io.SeekStart) + if err != nil { + return err + } + + if len > math.MaxUint32 { + return ErrMaxLengthExceeded + } + + return fb.DownloadFromReader(ctx, r, uint32(len)) +} + +// DownloadFromReader writes data to the client device's memory to be later used +// by Boot, Flash, etc. +// +// The true size must to be passed as the size parameter, the client device's +// implementation needs the value for proper allocation on its side. +// +// The command will fail if there is not enough space in RAM. +func (fb *FastBootChannel) DownloadFromReader(ctx context.Context, r io.Reader, size uint32) error { + b := make([]byte, 4) + binary.BigEndian.PutUint32(b, size) + + _, err := fb.dataCommand(ctx, r, size, "download:%08x", size) + return err +} + +// Flash tells the device to write the previously downloaded image to the named +// partition (if possible). +func (fb *FastBootChannel) Flash(ctx context.Context, partition string) error { + _, err := fb.Command(ctx, "flash:%s", partition) + return err +} + +// Erase tells the device to erase the indicated partition (clear to 0xFFs). +func (fb *FastBootChannel) Erase(ctx context.Context, partition string) error { + _, err := fb.Command(ctx, "erase:%s", partition) + return err +} + +// Boot tells the device to boot into the previously downloaded boot.img. +func (fb *FastBootChannel) Boot(ctx context.Context) error { + _, err := fb.Command(ctx, "boot") + return err +} + +// Continue tells the device to continue booting as normal (if possible). +func (fb *FastBootChannel) Continue(ctx context.Context) error { + _, err := fb.Command(ctx, "continue") + return err +} + +// Reboot tells the device to reboot. +func (fb *FastBootChannel) Reboot(ctx context.Context) error { + _, err := fb.Command(ctx, "reboot") + return err +} + +// Reboot tells the device to reboot back into the bootloader. Useful for +// upgrade processes that require upgrading the bootloader and then upgrading +// other partitions using the new bootloader. +func (fb *FastBootChannel) RebootBootloader(ctx context.Context) error { + _, err := fb.Command(ctx, "reboot-bootloader") + return err +} diff --git a/go/pkg/updater/assets/test/lorem_ipsum.txt b/go/pkg/updater/assets/test/lorem_ipsum.txt new file mode 100644 index 0000000..58c3a8f --- /dev/null +++ b/go/pkg/updater/assets/test/lorem_ipsum.txt @@ -0,0 +1,10 @@ +Tempor non laborum sit velit proident tempor consequat officia. Anim ex Lorem sunt adipisicing ad non incididunt esse anim velit nostrud exercitation duis. Dolore nostrud consectetur Lorem commodo occaecat anim magna dolore. Labore laborum et deserunt mollit ipsum exercitation quis consequat ut pariatur esse esse ut tempor consectetur. +Do anim ad nostrud. In elit velit consequat. Laboris fugiat proident labore sint ea reprehenderit ad consectetur aliqua sunt est est mollit. Exercitation ex magna laboris occaecat cillum ea officia aute qui. Officia ut id cupidatat minim enim ullamco. Laborum deserunt Lorem amet culpa do labore ea dolore proident dolore dolor. Officia elit reprehenderit aute nisi cupidatat ut amet eu minim sit duis nisi amet cupidatat sit. Exercitation id culpa ea anim deserunt nisi nulla aute. +Ipsum eiusmod dolore sit magna. Ex irure qui excepteur quis nisi nostrud labore reprehenderit. Reprehenderit dolore aute officia velit tempor consequat ea do non. Ad aliquip ut eu. Ad ea fugiat commodo. Et dolor dolore id sint magna. Proident sunt magna dolore pariatur consequat sint minim est qui magna. +Consequat excepteur culpa cupidatat anim magna mollit culpa in pariatur ea magna laborum ullamco excepteur. Proident ut sint minim sunt laboris. Deserunt eiusmod consectetur deserunt sunt proident. Enim laborum nulla est adipisicing. +Consectetur ut enim irure sunt aliqua cillum adipisicing esse consectetur minim magna enim excepteur cillum. Adipisicing proident laborum amet et ex excepteur veniam sunt velit voluptate eu tempor. Labore nulla enim excepteur sunt irure fugiat est minim ex aliqua culpa. Labore in voluptate et ad labore nostrud consectetur eiusmod magna ipsum est velit laboris labore. +Mollit aliqua nulla mollit ullamco et ullamco Lorem aliquip labore minim pariatur excepteur veniam dolor. Est fugiat enim occaecat incididunt eiusmod deserunt sint Lorem ullamco voluptate qui. Consectetur est occaecat labore id sint nisi aliquip. Consectetur adipisicing ex cillum est deserunt culpa excepteur elit. Sunt consectetur pariatur voluptate. +Nostrud sit ullamco ullamco tempor voluptate. Eiusmod reprehenderit aliquip culpa fugiat tempor incididunt quis culpa minim Lorem. Minim duis qui eiusmod minim mollit aute velit sit eiusmod est eu proident culpa. Dolor quis in voluptate nulla do dolore ut veniam esse voluptate reprehenderit labore officia. +Enim excepteur voluptate aliqua ad. Aute pariatur commodo ullamco. Mollit fugiat proident labore labore deserunt adipisicing qui minim pariatur voluptate. Culpa incididunt cupidatat magna amet duis elit. Aliqua ad aliqua laboris duis ex pariatur fugiat exercitation qui esse quis consectetur. Amet dolore duis ut cupidatat laborum eu. +Consectetur ea deserunt pariatur laborum et ea consectetur aliqua. Id tempor velit et. Nostrud irure non quis amet exercitation exercitation cillum tempor non nostrud consectetur sint ut occaecat. Et do amet id do velit velit aliqua irure laborum cupidatat sunt aute cillum ipsum ipsum. Excepteur adipisicing ipsum enim proident. Commodo pariatur occaecat duis voluptate occaecat magna dolor officia ipsum velit ex aute est. Proident voluptate id officia velit fugiat eu sunt tempor officia id. Nulla aliquip officia velit ullamco sunt anim consectetur Lorem. +Excepteur Lorem ipsum ullamco consectetur enim. Consequat nisi pariatur fugiat voluptate velit do cillum fugiat mollit. Excepteur ipsum laborum ipsum aliquip nisi dolor. Ut laborum tempor duis reprehenderit quis velit amet cupidatat commodo anim amet ullamco laboris minim tempor. In Lorem occaecat id duis et dolore. \ No newline at end of file diff --git a/go/pkg/updater/assets/test/lorem_ipsum.txt.xz b/go/pkg/updater/assets/test/lorem_ipsum.txt.xz new file mode 100644 index 0000000000000000000000000000000000000000..7eb4032c5e960ebacaced75d19995a7b78500c0c GIT binary patch literal 1232 zcmV;>1TXvjH+ooF000E$*0e?hz>W!&93cT5000000000ui%ulq4if~1T>vT>NvAXT z)W}_I8}a#hDyc7wdCu6vN7M$M;}Hq z{^bQlri00v1D!#tX0W2d{%vBv&VCZmsXXEw#6j0Npw0;$2ZH-K<(^bm*gcH&fS^W7 zx4~3+Z$&E|F`VsShrR4P0=Ny$GsPJOHzDUZX#pGQB>!ivv{F*j;IbBn8I^nf#o+ja zwKB1{pp!ix!=@YoD~gr9S>e{nlZb$~|wZcurt zNT6x%GDTrnuEPE|>$y~-O>S&dqOb5l3iO~1*j>1KS6(3J7WdP_ zzmb_VmPkEP#IY`=XdjE*0kKq!em_=aTOSyVY+$9nf>!86B<*Oo)Wcj_lWhU?tM7LhQ*|IIuwFHj*kZr+=kd_EMlt% z;0lFEC%2QxaRI3N{M#mf7I0$fafe8vY%98E2qdFUryI zWt}q#=~Y!2L+O;P*V>}uvKa{G${s#!PB*+*tG97dJ)Pm>ceSD^lhdyMRA-c?SlYN$ z%QFDUA$X~p>Gg7w#IPm&T4gu0x%#h0H0D(1d#a)rMfiJ*!dhh>+G&F06qcLh=R3#; zW1~vW%PP53QfF`jg#>HBd>YN3=>=}t$Srbgx?JM4GghL5rse804t{N1OL#%}x|?JX zH$~n!BGnYpC5u~bD@>gup{5UofB*v2$QyGRk|>?GPw6>4Y>40g6hn?Hn=kBG0x9_( z0!sDjKBkFakP!H%z-LRB`eFBIv&f*gAr=CU*I}_A9k-*(K7&{%m*iA|yDbi9h=w|A z$nYF#CXBC0-j~j*4}$w&P{ctL4As*db)M?~#NNQ(rwm)vLEe~tih1;wq-4^Z680=ok&n#EKHNtixGMBK)pc?E=oVmxYvCO8JuOH^ uZ+aH&keELh0002eIw?6@EcK%R0jdd<8~^~rsN7Mp#Ao{g000001X)@}CslL+ literal 0 HcmV?d00001 diff --git a/go/pkg/updater/config.go b/go/pkg/updater/config.go new file mode 100644 index 0000000..7b07d75 --- /dev/null +++ b/go/pkg/updater/config.go @@ -0,0 +1,30 @@ +package updater + +import "time" + +type DeviceConfig struct { + Name string + ProductID, VendorID uint16 + ImagePath string + + USBConfig int + USBInterface int + USBAlternate int + + USBInputEndpoint int + USBReadSize int + USBReadBufferSize int + + USBOutputEndpoint int + USBWriteSize int + USBWriteBufferSize int + + USBOpTimeout time.Duration +} + +type Config struct { + Devices []DeviceConfig + + LibusbDebugLevel int + SkipRebootAfterFlash bool +} diff --git a/go/pkg/updater/reader_monitor.go b/go/pkg/updater/reader_monitor.go new file mode 100644 index 0000000..b927ed1 --- /dev/null +++ b/go/pkg/updater/reader_monitor.go @@ -0,0 +1,22 @@ +package updater + +import "io" + +type ReaderMonitor struct { + io.Reader + f func(offset int64) +} + +// Read implements io.Reader. +func (r *ReaderMonitor) Read(p []byte) (int, error) { + n, err := r.Reader.Read(p) + if err != nil { + return n, err + } + r.f(int64(n)) + return n, err +} + +func NewReaderMonitor(r io.Reader, f func(offset int64)) io.Reader { + return &ReaderMonitor{r, f} +} diff --git a/go/pkg/updater/reader_seeker_monitor.go b/go/pkg/updater/reader_seeker_monitor.go new file mode 100644 index 0000000..815388e --- /dev/null +++ b/go/pkg/updater/reader_seeker_monitor.go @@ -0,0 +1,29 @@ +package updater + +import "io" + +type ReaderSeekerMonitor struct { + io.ReadSeeker + f func(offset int64, whence int) +} + +// Read implements io.ReadSeeker. +func (r *ReaderSeekerMonitor) Read(p []byte) (int, error) { + n, err := r.ReadSeeker.Read(p) + if err != nil { + return n, err + } + r.f(int64(n), io.SeekCurrent) + return n, err +} + +// Seek implements io.ReadSeeker. +func (r *ReaderSeekerMonitor) Seek(offset int64, whence int) (int64, error) { + n, err := r.ReadSeeker.Seek(offset, whence) + r.f(offset, whence) + return n, err +} + +func NewReadSeekerMonitor(r io.ReadSeeker, f func(offset int64, whence int)) io.ReadSeeker { + return &ReaderSeekerMonitor{r, f} +} diff --git a/go/pkg/updater/updater.go b/go/pkg/updater/updater.go new file mode 100644 index 0000000..97857ba --- /dev/null +++ b/go/pkg/updater/updater.go @@ -0,0 +1,618 @@ +package updater + +import ( + "bytes" + "context" + "crypto/sha1" + "encoding/binary" + "encoding/hex" + "errors" + "fmt" + "hash" + "io" + "log/slog" + "os" + "os/signal" + "syscall" + "time" + + "github.com/dustin/go-humanize" + "github.com/google/gousb" + "github.com/icedream/denon-prime4/go/pkg/fastboot" + "github.com/sqweek/dialog" + "github.com/u-root/u-root/pkg/dt" +) + +var ( + ErrNoMatchingDevices = errors.New("no matching devices") + ErrNoImagesInDeviceTree = errors.New("no images in device tree") + ErrMissingVersion = errors.New("missing version") + ErrBadVersion = errors.New("bad version") + ErrUnsupportedConfiguration = errors.New("unsupported configuration") + ErrChecksumMismatch = errors.New("checksum mismatch") +) + +// TODO - Max packet size must be 64 bytes for full-speed, 512 bytes for high-speed and 1024 bytes for Super Speed USB. + +type Progress struct { + Text string + Percentage float64 + Indetermined bool + Cancellable bool +} + +var ErrInvalidLength = errors.New("invalid length") + +type DeviceID struct { + VendorID, ProductID uint16 +} + +func (id DeviceID) String() string { + return fmt.Sprintf("%04x:%04x", id.VendorID, id.ProductID) +} + +func bytesAsDeviceList(b []byte) ([]DeviceID, error) { + if len(b)%4 != 0 { + return nil, ErrInvalidLength + } + items := make([]DeviceID, len(b)/4) + i := 0 + for offset := 0; offset < len(b); offset += 4 { + items[i].VendorID = binary.BigEndian.Uint16(b[offset : offset+2]) + items[i].ProductID = binary.BigEndian.Uint16(b[offset+2 : offset+4]) + i++ + } + return items, nil +} + +type Updater struct { + config Config + logger *slog.Logger + + DryRun bool +} + +func NewUpdater(config Config, logger *slog.Logger) (*Updater, error) { + if logger == nil { + logger = slog.Default() + } + + if len(config.Devices) < 1 { + return nil, ErrUnsupportedConfiguration + } + + return &Updater{ + config: config, + logger: logger, + }, nil +} + +func (u Updater) Config() Config { + return u.config +} + +func (u Updater) runDevice(progressC chan Progress, deviceConfig DeviceConfig) error { + progressC <- Progress{ + Text: "Preparing update...", + Indetermined: true, + } + + imageFile, err := os.Open(deviceConfig.ImagePath) + if err != nil { + return err + } + defer imageFile.Close() + fdt, err := dt.New(dt.WithReaderAt(imageFile)) + if err != nil { + return err + } + + // extract list of compatible devices + devices := fdt.Root().Property("inmusic,devices") + devicesBytes, err := devices.AsBytes() + if err != nil { + return err + } + devicesList, err := bytesAsDeviceList(devicesBytes) + if err != nil { + return err + } + + // extract version string + version := fdt.Root().Property("inmusic,version") + if version == nil { + return ErrMissingVersion + } + versionStr, err := version.AsString() + if err != nil { + return ErrBadVersion + } + + images := fdt.Root().Walk("images") + if images == nil { + return ErrNoImagesInDeviceTree + } + var totalDataSizeFloat float64 + imageNames, err := images.ListChildNodes() + if err != nil { + return err + } + for _, imageName := range imageNames { + image := images.Walk(imageName) + if image == nil { + continue + } + + // image + data := image.Property("data") + if data == nil { + return err + } + dataBytes, err := data.AsBytes() + if err != nil { + return err + } + compression := image.Property("compression") + var compressionStr string + dataSize := float64(len(dataBytes)) + dataReader := bytes.NewReader(dataBytes) + if compression != nil { + compressionStr, err = compression.AsString() + if err != nil { + return err + } + switch compressionStr { + case "xz": + // determine uncompressed size + uncompressedSize, err := getXZUncompressedLength(bytes.NewReader(dataBytes)) + if err != nil { + return err + } + dataSize = float64(uncompressedSize) + default: + u.logger.Error("Unsupported compression", + "compression", compressionStr, + "imageName", imageName) + return errors.New("unsupported compression: " + compressionStr) + } + } + totalDataSizeFloat += dataSize + + // verify image hash + hashProp := image.Walk("hash") + if hashProp == nil { + continue + } + hashAlgo := hashProp.Property("algo") + if hashAlgo != nil { + hashAlgoStr, err := hashAlgo.AsString() + if err != nil { + return err + } + hashValue := hashProp.Property("value") + if hashValue == nil { + return err + } + hashBytes, err := hashValue.AsBytes() + if err != nil { + return err + } + var hasher hash.Hash + switch hashAlgoStr { + case "sha1": + hasher = sha1.New() + default: + u.logger.Error("Checksum algorithm not supported", + "imageName", imageName, + "hashAlgo", hashAlgoStr) + return errors.New("checksum algorithm not supported yet: " + hashAlgoStr) + } + u.logger.Info("Verifying image checksum", + "imageName", imageName, + "hashAlgo", hashAlgoStr, + "wantedHash", hex.EncodeToString(hashBytes)) + if _, err := io.Copy(hasher, dataReader); err != nil { + u.logger.Error("Failed to generate checksum", + "imageName", imageName, + "err", err) + // TODO - ErrChecksumGenerationFailure + return fmt.Errorf("checksum generation failure: %w", err) + } + actualHash := hasher.Sum(nil) + if !bytes.Equal(actualHash, hashBytes) { + u.logger.Error("Checksum mismatch", + "imageName", imageName, + "hashAlgo", hashAlgoStr, + "wantedHash", hex.EncodeToString(hashBytes), + "actualHash", hex.EncodeToString(actualHash)) + return ErrChecksumMismatch + } + u.logger.Info("Image checksum OK", + "imageName", imageName) + } else { + return errors.New("missing image hash") + } + } + + u.logger.Info("Calculated total data length", + "totalSize", int64(totalDataSizeFloat)) + + usbCtx := gousb.NewContext() + defer usbCtx.Close() + + usbCtx.Debug(u.config.LibusbDebugLevel) + + appCtx, cancel := context.WithCancel(context.Background()) + defer cancel() + + appCtx, cancelNotify := signal.NotifyContext(appCtx, os.Interrupt, syscall.SIGTERM) + defer cancelNotify() + + devicesMatched := 0 + + for _, deviceID := range devicesList { + withDevice := func(f func(fb *fastboot.FastBootChannel) error) error { + device, err := usbCtx.OpenDeviceWithVIDPID( + gousb.ID(deviceID.VendorID), + gousb.ID(deviceID.ProductID)) + if err != nil { + if errors.Is(err, gousb.ErrorAccess) { + dialog.Message( + "Permission error. Make sure you are running the application with correct permissions (you may want to run this with admin privileges).\n"+ + "\n"+ + "%s", + err.Error()).Title("Error").Error() + } + return err + } + if device == nil { + return ErrNoMatchingDevices + } + defer device.Close() + devicesMatched++ + + u.logger.Debug("Enabling autodetach") + device.SetAutoDetach(true) + + u.logger.Debug("Setting configuration...", + "configNum", deviceConfig.USBConfig) + cfg, err := device.Config(deviceConfig.USBConfig) + if err != nil { + return fmt.Errorf("dev.Config(%d): %w", deviceConfig.USBConfig, err) + } + u.logger.Debug("Claiming interface...", + "interfaceNum", deviceConfig.USBInterface, + "altNum", deviceConfig.USBAlternate) + intf, err := cfg.Interface(deviceConfig.USBInterface, deviceConfig.USBAlternate) + if err != nil { + return fmt.Errorf("cfg.Interface(%d, %d): %w", deviceConfig.USBInterface, deviceConfig.USBAlternate, err) + } + defer intf.Close() + + u.logger.Debug("Using input endpoint", + "inputEndpoint", deviceConfig.USBInputEndpoint) + inEP, err := intf.InEndpoint(deviceConfig.USBInputEndpoint) + if err != nil { + return fmt.Errorf("dev.InEndpoint(): %w", err) + } + u.logger.Debug("Found input endpoint", + "inEP", inEP) + var rdr fastboot.ContextReader = inEP + if deviceConfig.USBReadBufferSize > 1 { + u.logger.Debug("Creating input buffer...") + s, err := inEP.NewStream(deviceConfig.USBReadSize, deviceConfig.USBReadBufferSize) + if err != nil { + return fmt.Errorf("inEP.NewStream(): %w", err) + } + defer s.Close() + rdr = s + } + + u.logger.Debug("Using output endpoint", + "outputEndpoint", deviceConfig.USBOutputEndpoint) + outEP, err := intf.OutEndpoint(deviceConfig.USBOutputEndpoint) + if err != nil { + return fmt.Errorf("dev.OutEndpoint(): %w", err) + } + u.logger.Debug("Found input endpoint", + "outEP", outEP) + var wrr fastboot.ContextWriter = outEP + if deviceConfig.USBWriteBufferSize > 1 { + u.logger.Debug("Creating output buffer...") + s, err := outEP.NewStream(deviceConfig.USBWriteSize, deviceConfig.USBWriteBufferSize) + if err != nil { + return fmt.Errorf("outEP.NewStream(): %w", err) + } + defer s.Close() + wrr = s + } + + fbCtx, cancelfb := context.WithCancel(appCtx) + defer cancelfb() + + fb := fastboot.NewFastBootChannel(fbCtx, + u.logger.WithGroup("fastboot"), + rdr, + wrr) + + bootloaderLog := u.logger.WithGroup("bootloader") + go func() { + for info := range fb.InfoC() { + bootloaderLog.Info(info) + } + }() + + go func() { + for text := range fb.TextC() { + u.logger.Info(text) + } + }() + + return f(fb) + } + + withTimeout := func(f func(opCtx context.Context)) { + opCtx := appCtx + if deviceConfig.USBOpTimeout > 0 { + u.logger.Debug("Setting up deadline", + "timeout", deviceConfig.USBOpTimeout) + var cancelTimeout func() + opCtx, cancelTimeout = context.WithTimeout(appCtx, deviceConfig.USBOpTimeout) + defer cancelTimeout() + } + f(opCtx) + } + + // unlock device for flashing + if err := withDevice(func(fb *fastboot.FastBootChannel) error { + var err error + withTimeout(func(opCtx context.Context) { + _, err = fb.Command(opCtx, "oem:%s", "inmusic-unlock-magic-7de5fbc22b8c524e") + if err != nil { + return + } + }) + return err + }); err != nil { + return err + } + + // log some basic fastboot variables + fields := make([]any, 0) + for _, varName := range []string{ + "version", + "version-bootloader", + "version-baseband", + "product", + "serialno", + "secure", + "is-userspace", + } { + if err := withDevice(func(fb *fastboot.FastBootChannel) error { + var data string + var err error + withTimeout(func(opCtx context.Context) { + data, err = fb.GetVar(opCtx, varName) + }) + if err != nil { + u.logger.Warn("Bootloader does not support variable", + "varName", varName) + return nil + } + fields = append(fields, varName, data) + return nil + }); err != nil { + return err + } + } + u.logger.Info("Read bootloader variables", fields...) + + // download image to device + var totalDownloadedSizeFloat float64 + statusText := fmt.Sprintf("Updating to version %s...", versionStr) + for _, imageName := range imageNames { + u.logger.Info("Parsing image data", + "imageName", imageName) + + image := images.Walk(imageName) + if image == nil { + continue + } + + // parse partition + partition := image.Property("partition") + if partition == nil { + return errors.New("missing partition") + } + partitionStr, err := partition.AsString() + if err != nil { + return err + } + + // parse data and data size + data := image.Property("data") + if data == nil { + return errors.New("missing data") + } + dataBytes, err := data.AsBytes() + if err != nil { + return err + } + dataSize := int64(len(dataBytes)) + compressedDataSize := dataSize + dataReader := bytes.NewReader(dataBytes) + + var finalReader io.Reader = dataReader + compression := image.Property("compression") + var compressionStr string + if compression != nil { + compressionStr, err = compression.AsString() + if err != nil { + return err + } + switch compressionStr { + case "xz": + // determine uncompressed size + uncompressedSize, err := getXZUncompressedLength(bytes.NewReader(dataBytes)) + if err != nil { + return err + } + dataSize = uncompressedSize + + // decompress on the fly + uncompressedDataReader, err := newXZReader(finalReader) + if err != nil { + return err + } + finalReader = uncompressedDataReader + default: + u.logger.Error("Unsupported compression", + "compression", compressionStr, + "imageName", imageName) + return errors.New("unsupported compression: " + compressionStr) + } + } + + u.logger.Info("Now writing image", + "imageName", imageName, + "partition", partitionStr, + "compressedDataSize", compressedDataSize, + "dataSize", dataSize, + "compression", compressionStr) + + // monitor our progress on the decoded data + var previousDataPos int64 + // var countedPos int64 + // finalReader = NewReadSeekerMonitor(finalReader, func(offset int64, whence int) { + // var newPos int64 + // switch whence { + // case io.SeekCurrent: + // newPos = previousPos + offset + // case io.SeekEnd: + // newPos = int64(len(dataBytes)) + // if offset < 0 { + // newPos += offset + // } + // case io.SeekStart: + // newPos = offset + // } + // defer func() { previousPos = newPos }() + // if countedPos >= newPos { + // return + // } + // diff := newPos - previousPos + // countedPos = newPos + // downloadedSize += float64(diff) + // progressC <- Progress{ + // Text: statusText + fmt.Sprintf("\n(%s, transferred %s/%s)", + // imageName, + // humanize.Bytes(uint64(newPos)), + // humanize.Bytes(uint64(len(dataBytes)))), + // Percentage: downloadedSize / totalSize, + // } + // }) + finalReader = NewReaderMonitor(finalReader, func(offset int64) { + // calculate pos difference and then store new pos + newDataPos := previousDataPos + offset + dataPosDiff := newDataPos - previousDataPos + previousDataPos = newDataPos + + // add difference to TOTAL size for total progress + totalDownloadedSizeFloat += float64(dataPosDiff) + + progressC <- Progress{ + Text: statusText + fmt.Sprintf("\n(%s, transferred %s/%s)", + imageName, + humanize.Bytes(uint64(newDataPos)), + humanize.Bytes(uint64(dataSize))), + Percentage: totalDownloadedSizeFloat / totalDataSizeFloat, + } + }) + + // buf := make([]byte, 4096) + if err := withDevice(func(fb *fastboot.FastBootChannel) error { + u.logger.Info("Download started", + "compressedDataSize", compressedDataSize, + "dataSize", dataSize, + "imageName", imageName, + "dryRun", u.DryRun) + if u.DryRun { + io.Copy(io.Discard, finalReader) + } else if err := fb.DownloadFromReader(appCtx, finalReader, uint32(dataSize)); err != nil { + u.logger.Error("Download failed", + "err", err) + return fmt.Errorf("download failed: %w", err) + } + u.logger.Info("Download OK") + return nil + }); err != nil { + return err + } + + progressC <- Progress{ + Text: statusText + fmt.Sprintf("\n(%s, flashing)", + imageName), + Percentage: totalDownloadedSizeFloat / totalDataSizeFloat, + } + if err := withDevice(func(fb *fastboot.FastBootChannel) error { + u.logger.Info("Flash started", + "imageName", imageName, + "dryRun", u.DryRun) + if u.DryRun { + time.Sleep(2 * time.Second) + } else { + if err := fb.Flash(appCtx, partitionStr); err != nil { + u.logger.Error("Flash failed", + "err", err) + return fmt.Errorf("flash failed: %w", err) + } + } + u.logger.Info("Flash OK") + return nil + }); err != nil { + return err + } + time.Sleep(1 * time.Second) + } + + progressC <- Progress{ + Text: "Finishing update...", + Indetermined: true, + } + if !u.config.SkipRebootAfterFlash { + if err := withDevice(func(fb *fastboot.FastBootChannel) error { + u.logger.Info("Requesting reboot", + "dryRun", u.DryRun) + if !u.DryRun { + if err := fb.Reboot(appCtx); err != nil { + u.logger.Error("Reboot failed", "err", err) + return fmt.Errorf("reboot failed: %w", err) + } + } + u.logger.Info("Reboot OK") + return nil + }); err != nil { + return err + } + } + time.Sleep(1 * time.Second) + } + + if devicesMatched == 0 { + return ErrNoMatchingDevices + } + + return nil +} + +func (u Updater) Run(progressC chan Progress) error { + defer close(progressC) + + config := u.config + + if len(config.Devices) < 1 { + return errors.New("configurations with not exactly 1 device not supported yet") + } + + return u.runDevice(progressC, config.Devices[0]) +} diff --git a/go/pkg/updater/xz.go b/go/pkg/updater/xz.go new file mode 100644 index 0000000..2b2ca41 --- /dev/null +++ b/go/pkg/updater/xz.go @@ -0,0 +1,90 @@ +package updater + +import ( + "encoding/binary" + "errors" + "fmt" + "hash/crc32" + "io" + + "github.com/ulikunitz/xz/lzma" +) + +// footerLen defines the length of the footer. +const footerLen = 12 + +// Minimum and maximum for the size of the index (backward size). +const ( + minIndexSize = 4 + maxIndexSize = (1 << 32) * 4 +) + +var ErrFooterMagicMismatch = errors.New("footer magic mismatch") + +func getXZUncompressedLength(r io.ReadSeeker) (int64, error) { + // read footer and after all safety checks extract backward size from it + if _, err := r.Seek(-footerLen, io.SeekEnd); err != nil { + return 0, fmt.Errorf("failed to seek to footer: %w", err) + } + footerBytes := make([]byte, footerLen) + if n, err := r.Read(footerBytes); err != nil { + return 0, fmt.Errorf("failed to read footer bytes: %w", err) + } else if n != footerLen { + return 0, fmt.Errorf("failed to read footer bytes: %w", ErrInvalidLength) + } + if string(footerBytes[10:12]) != "YZ" { + return 0, fmt.Errorf("failed to read footer bytes: %w", ErrFooterMagicMismatch) + } + checksum := binary.LittleEndian.Uint32(footerBytes[0:4]) + calculatedChecksum := crc32.ChecksumIEEE(footerBytes[4:10]) + if checksum != calculatedChecksum { + return 0, fmt.Errorf("failed to read footer bytes: %w", ErrChecksumMismatch) + } + backwardSize := int64(binary.LittleEndian.Uint32(footerBytes[4:8])+1) * 4 + // streamFlags := footerBytes[8:10] + + // get xz index offset from backwardsize and seek to it + if _, err := r.Seek(-(backwardSize + footerLen), io.SeekEnd); err != nil { + return 0, fmt.Errorf("failed to seek to index: %w", err) + } + br := lzma.ByteReader(r) + + // verify this is actually the index using the index marker + indexMarker, err := br.ReadByte() + if err != nil { + return 0, fmt.Errorf("failed to read index marker: %w", err) + } + if indexMarker != 0 { + return 0, fmt.Errorf("invalid index marker") + } + + // parse number of records + numberOfRecords, _, err := readUvarint(br) + if err != nil { + return 0, fmt.Errorf("failed to read number of records from index: %w", err) + } + if numberOfRecords < 0 { + return 0, fmt.Errorf("failed to read number of records from index: %w", errors.New("number of records negative")) + } + + // calculate total uncompressed size from all records + var totalUncompressedRecordSize int64 + for i := uint64(0); i < numberOfRecords; i++ { + // skip unpadded size + _, _, err := readUvarint(br) + if err != nil { + return 0, fmt.Errorf("failed to read index record %d: %w", i, err) + } + + // read uncompressed size for this record and add it + uncompressedRecordSize, _, err := readUvarint(br) + if err != nil { + return 0, fmt.Errorf("failed to read uncompressed size for index record %d: %w", i, err) + } + if uncompressedRecordSize < 0 { + return 0, fmt.Errorf("failed to read uncompressed size for index record %d: %w", i, errors.New("uncompressed size negative")) + } + totalUncompressedRecordSize += int64(uncompressedRecordSize) + } + return totalUncompressedRecordSize, nil +} diff --git a/go/pkg/updater/xz_go.go b/go/pkg/updater/xz_go.go new file mode 100644 index 0000000..1cfffd2 --- /dev/null +++ b/go/pkg/updater/xz_go.go @@ -0,0 +1,15 @@ +//go:build !libxz +// +build !libxz + +package updater + +import ( + "io" + + "github.com/ulikunitz/xz" +) + +func newXZReader(r io.Reader) (io.ReadCloser, error) { + xzReader, err := xz.NewReader(r) + return io.NopCloser(xzReader), err +} diff --git a/go/pkg/updater/xz_libxz.go b/go/pkg/updater/xz_libxz.go new file mode 100644 index 0000000..b81aaff --- /dev/null +++ b/go/pkg/updater/xz_libxz.go @@ -0,0 +1,15 @@ +//go:build libxz +// +build libxz + +package updater + +import ( + "io" + + "github.com/jamespfennell/xz" +) + +func newXZReader(r io.Reader) (io.ReadCloser, error) { + dr := xz.NewReader(r) + return io.NopCloser(dr), nil +} diff --git a/go/pkg/updater/xz_test.go b/go/pkg/updater/xz_test.go new file mode 100644 index 0000000..014c4a5 --- /dev/null +++ b/go/pkg/updater/xz_test.go @@ -0,0 +1,21 @@ +package updater + +import ( + "bytes" + _ "embed" + "testing" + + "github.com/stretchr/testify/require" +) + +//go:embed assets/test/lorem_ipsum.txt.xz +var compressedAsset []byte + +//go:embed assets/test/lorem_ipsum.txt +var uncompressedAsset []byte + +func TestGetXZUncompressedLength(t *testing.T) { + uncompressedLength, err := getXZUncompressedLength(bytes.NewReader(compressedAsset)) + require.NoError(t, err) + require.Equal(t, int64(len(uncompressedAsset)), uncompressedLength) +} diff --git a/go/pkg/updater/xz_util.go b/go/pkg/updater/xz_util.go new file mode 100644 index 0000000..3aa095e --- /dev/null +++ b/go/pkg/updater/xz_util.go @@ -0,0 +1,39 @@ +package updater + +import ( + "errors" + "io" +) + +// errOverflow indicates an overflow of the 64-bit unsigned integer. +// +// Adapted from https://github.com/ulikunitz/xz/blob/master/bits.go#L53. +var errOverflowU64 = errors.New("uvarint overflows 64-bit unsigned integer") + +// readUvarint reads a uvarint from the given byte reader. +// +// Adapted from https://github.com/ulikunitz/xz/blob/master/bits.go#L56. +func readUvarint(r io.ByteReader) (x uint64, n int, err error) { + const maxUvarintLen = 10 + + var s uint + i := 0 + for { + b, err := r.ReadByte() + if err != nil { + return x, i, err + } + i++ + if i > maxUvarintLen { + return x, i, errOverflowU64 + } + if b < 0x80 { + if i == maxUvarintLen && b > 1 { + return x, i, errOverflowU64 + } + return x | uint64(b)< Date: Mon, 8 Jul 2024 01:07:14 +0200 Subject: [PATCH 2/5] Increase data copy buffer size from 16k to 128k. --- go/pkg/fastboot/fastboot.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go/pkg/fastboot/fastboot.go b/go/pkg/fastboot/fastboot.go index 054eb05..4457443 100644 --- a/go/pkg/fastboot/fastboot.go +++ b/go/pkg/fastboot/fastboot.go @@ -245,7 +245,7 @@ func (fb *FastBootChannel) dataCommand( } // now we stream our data to the client - buf := make([]byte, 16*1024) + buf := make([]byte, 128*1024 /*128k*/) for { n, err := r.Read(buf) if err != nil { From f3a8bc1d5df98825921cf1d3262b8bec83d90335 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 4 Aug 2024 19:32:50 +0200 Subject: [PATCH 3/5] Remove updater unpacking script. --- README.md | 2 +- dist.sh | 1 - unpack-updater.sh | 28 ---------------------------- 3 files changed, 1 insertion(+), 30 deletions(-) delete mode 100755 unpack-updater.sh diff --git a/README.md b/README.md index 1e1b638..909aaa6 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To reproduce the modified firmware with OpenSSH: 2. Run `./clone-buildroot.sh` to download the matching buildroot environment via Git to the `buildroot/2021.02.10` directory. 3. Run `./compile-buildroot.sh` to build the required toolchain and packages in buildroot. Note that this will ask for sudo access to modify the unpacked firmware images via loopback mount. 4. Run `./pack.sh` to finally pack the modified image files back into a new firmware package. It will have a `.dtb` extension but you can rename this to `.img` and flash it directly to your hardware. -5. Optionally run `./unpack-updater.sh` to download Denon's original Windows tool for flashing firmware via USB cable, then run `./generate-updater-win.sh` to download 7-zip's SFX module to generate a self-extracting executable based on that tool but with your own image instead. +5. Optionally, run `./generate-updater-win.sh` to download 7-zip's SFX module to generate a self-extracting executable based on that tool but with your own image instead. ## Customizations diff --git a/dist.sh b/dist.sh index 6d91497..cb48723 100755 --- a/dist.sh +++ b/dist.sh @@ -8,5 +8,4 @@ rm -rf unpacked-img ./unpack.sh "$@" ./compile-buildroot.sh "$@" ./pack.sh "$@" -./unpack-updater.sh "$@" ./generate-updater-win.sh "$@" diff --git a/unpack-updater.sh b/unpack-updater.sh deleted file mode 100755 index 374914c..0000000 --- a/unpack-updater.sh +++ /dev/null @@ -1,28 +0,0 @@ -#!/bin/sh -e - -. ./functions.sh - -if ! command -v 7z >/dev/null; then - log_fatal "You need 7-zip installed (7z command seems to be missing)." -fi - -files=("${device_updater_win_download_filename}") - -download_updater_win() { - log "*** Downloading ${device_updater_win_download_filename}" - curl '-#Lo' "${device_updater_win_download_filename}" "${device_updater_win_download_url}" - files+=("${device_updater_win_download_filename}") -} - -for file in "${files[@]}"; do - if [ ! -f "$file" ]; then - #log_fatal "Need $file. Put it into the current working directory ($(pwd))." - download_updater_win - fi - - output_dir="updater/$device_id/win" - - log "*** Unpacking $file to $output_dir" - mkdir -p "$output_dir" - 7z x -y -o"$output_dir" '-x!*.img' "$file" -done From 2bfe595d87afb75d5369150d4bb4031f550a0ab0 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 4 Aug 2024 19:37:03 +0200 Subject: [PATCH 4/5] Make go tool path configurable in makefile. --- go/Makefile | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/go/Makefile b/go/Makefile index bce6e06..a8ab944 100644 --- a/go/Makefile +++ b/go/Makefile @@ -8,6 +8,7 @@ WINDOWS_386_TRIPLET=i686-w64-mingw32 WINDOWS_386_PREFIX=/usr/$(WINDOWS_386_TRIPLET) GOTAGS=libxz LINUX_AMD64_TRIPLET=x86_64-linux-gnu +GO=go GOOS= GOARCH= GOBUILDFLAGS= @@ -65,10 +66,10 @@ clean-windows-amd64: # find_update$(BINEXT): go.mod go.sum $(wildcard ./pkg/**/*.go ./cmd/find_update/**/*.go) - go build -tags $(GOTAGS) -ldflags $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/find_update + $(GO) build -tags $(GOTAGS) -ldflags $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/find_update updater$(BINEXT): go.mod go.sum $(wildcard ./pkg/**/*.go ./cmd/updater/**/*.go) - go build -tags $(GOTAGS) -ldflags $(GOGUIBUILDFLAGS)\ $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/updater + $(GO) build -tags $(GOTAGS) -ldflags $(GOGUIBUILDFLAGS)\ $(GOBUILDLDFLAGS) $(GOBUILDFLAGS) -o $@ -v ./cmd/updater # # PACKAGING TASKS From e674ac9419c9bb5e6adeee1f00b3e170a16f13d2 Mon Sep 17 00:00:00 2001 From: Carl Kittelberger Date: Sun, 4 Aug 2024 19:38:33 +0200 Subject: [PATCH 5/5] List Go as requirement for generating the updater. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 909aaa6..4b2aae5 100644 --- a/README.md +++ b/README.md @@ -20,7 +20,7 @@ To reproduce the modified firmware with OpenSSH: 2. Run `./clone-buildroot.sh` to download the matching buildroot environment via Git to the `buildroot/2021.02.10` directory. 3. Run `./compile-buildroot.sh` to build the required toolchain and packages in buildroot. Note that this will ask for sudo access to modify the unpacked firmware images via loopback mount. 4. Run `./pack.sh` to finally pack the modified image files back into a new firmware package. It will have a `.dtb` extension but you can rename this to `.img` and flash it directly to your hardware. -5. Optionally, run `./generate-updater-win.sh` to download 7-zip's SFX module to generate a self-extracting executable based on that tool but with your own image instead. +5. Optionally, install [Go](https://go.dev/) 1.22 or newer and run `./generate-updater-win.sh` to download 7-zip's SFX module to generate a self-extracting executable based on that tool but with your own image instead. ## Customizations