-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Package fota implements Flash Over The Air utilities. Usage of fota: -card string path to SDcard -map string path to JSON formatted mapping -md5 string URL to MD5SUMS file -q suppress logging Change-Id: I84f4168047f2f9488f0ed42efb1f8143410ac341
- Loading branch information
1 parent
17179cb
commit c28a204
Showing
3 changed files
with
421 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,85 @@ | ||
/* | ||
* Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License | ||
*/ | ||
|
||
package main | ||
|
||
import ( | ||
"encoding/json" | ||
"flag" | ||
"log" | ||
"os" | ||
|
||
"git.tizen.org/tools/muxpi/fota" | ||
) | ||
|
||
var ( | ||
sdcard string | ||
mapping string | ||
md5sums string | ||
quiet bool | ||
) | ||
|
||
func setFlags() { | ||
flag.StringVar(&sdcard, "card", "", "path to SDcard") | ||
// TODO: if map is not present it can generate an example. | ||
flag.StringVar(&mapping, "map", "", "path to JSON formatted mapping") | ||
flag.StringVar(&md5sums, "md5", "", "URL to MD5SUMS file") | ||
flag.BoolVar(&quiet, "q", false, "suppress logging") | ||
} | ||
|
||
func checkErr(ctx string, err error) { | ||
if err != nil { | ||
log.Fatal(ctx, err) | ||
} | ||
} | ||
|
||
func verbose(str string) { | ||
if !quiet { | ||
log.Println(str) | ||
} | ||
} | ||
|
||
func main() { | ||
setFlags() | ||
flag.Parse() | ||
|
||
if mapping == "" { | ||
log.Fatal("missing mapping argument") | ||
} | ||
if sdcard == "" { | ||
log.Fatal("missing sdcard argument") | ||
} | ||
|
||
f, err := os.Open(mapping) | ||
checkErr("failed to open the mapping: ", err) | ||
defer f.Close() | ||
|
||
partMapping := make(map[string]string) | ||
decoder := json.NewDecoder(f) | ||
checkErr("failed to decode the mapping: ", decoder.Decode(&partMapping)) | ||
|
||
flasher, err := fota.NewFOTA(flag.Args(), md5sums, sdcard, partMapping) | ||
checkErr("failed to initialize FOTA: ", err) | ||
defer flasher.Close() | ||
if !quiet { | ||
flasher.SetVerbose() | ||
} | ||
verbose("FOTA initialized") | ||
|
||
checkErr("SDcard not found: ", fota.WaitForSDcard(sdcard, 10)) | ||
verbose("SDcard detected") | ||
checkErr("failed to flash images: ", flasher.DownloadAndFlash()) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,233 @@ | ||
/* | ||
* Copyright (c) 2017 Samsung Electronics Co., Ltd All Rights Reserved | ||
* | ||
* Licensed under the Apache License, Version 2.0 (the "License"); | ||
* you may not use this file except in compliance with the License. | ||
* You may obtain a copy of the License at | ||
* | ||
* http://www.apache.org/licenses/LICENSE-2.0 | ||
* | ||
* Unless required by applicable law or agreed to in writing, software | ||
* distributed under the License is distributed on an "AS IS" BASIS, | ||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
* See the License for the specific language governing permissions and | ||
* limitations under the License | ||
*/ | ||
|
||
// Package fota implements Flash Over The Air utilities. | ||
package fota | ||
|
||
import ( | ||
"archive/tar" | ||
"bufio" | ||
"compress/gzip" | ||
"crypto/md5" | ||
"fmt" | ||
"io" | ||
"log" | ||
"net/http" | ||
"net/url" | ||
"os" | ||
"path" | ||
"time" | ||
|
||
"git.tizen.org/tools/muxpi/stm" | ||
) | ||
|
||
// FOTA provides methods to help in the process of flashing an image to sdcard. | ||
type FOTA struct { | ||
// URLs are the source of archives. | ||
URLs []string | ||
// md5sums is URL which should store checksums of files referenced by URLs. | ||
md5sums string | ||
// checksums maps filenames onto hash values. | ||
checksums map[string]string | ||
// SDcard is a path to block device images will be flashed to. | ||
// Example: "/dev/sda". | ||
SDcard string | ||
// PartMapping ".img" file -> partition number. | ||
// Example: {"boot.img":1} | ||
PartMapping map[string]string | ||
// verbose allows logging to default log.Logger instance or prevents it if false. | ||
verbose bool | ||
} | ||
|
||
// NewFOTA returns new instance of FOTA. It also opens connection to STM. | ||
func NewFOTA(URLs []string, md5sums string, sdcard string, partMapping map[string]string) (*FOTA, error) { | ||
return &FOTA{ | ||
URLs: URLs, | ||
md5sums: md5sums, | ||
checksums: make(map[string]string), | ||
SDcard: sdcard, | ||
PartMapping: partMapping, | ||
}, stm.Open() | ||
} | ||
|
||
// Close releases FOTA resources. | ||
func (fota *FOTA) Close() error { | ||
return stm.Close() | ||
} | ||
|
||
// SetVerbose increases logging of FOTA actions. | ||
func (fota *FOTA) SetVerbose() { | ||
fota.verbose = true | ||
} | ||
|
||
// computeHash passes content of the reader to the PipeReader | ||
// and returns a MD5 checksum at the end. | ||
// Returned PipeReader must be closed by the caller. | ||
func (fota *FOTA) computeHash(reader io.Reader) (chan []byte, *io.PipeReader) { | ||
hash := md5.New() | ||
pipeReader, pipeWriter := io.Pipe() | ||
writer := io.MultiWriter(hash, pipeWriter) | ||
ret := make(chan []byte) | ||
go func() { | ||
defer close(ret) | ||
_, err := io.Copy(writer, reader) | ||
if err != nil { | ||
pipeWriter.CloseWithError(err) | ||
return | ||
} | ||
pipeWriter.Close() | ||
ret <- hash.Sum(nil) | ||
}() | ||
return ret, pipeReader | ||
} | ||
|
||
// flash copies content of reader to partition specified by path. | ||
func (fota *FOTA) flash(reader io.Reader, path string) (int64, error) { | ||
f, err := os.OpenFile(path, os.O_WRONLY, 0660) | ||
if err != nil { | ||
return 0, err | ||
} | ||
defer f.Sync() | ||
defer f.Close() | ||
return io.Copy(f, reader) | ||
} | ||
|
||
// uncompressAndFlash uncompresses the stream, unpacks resulting archive | ||
// and calls flash for each ".img" file mentioned in the PartMapping. | ||
// It closes reader after it finishes. | ||
func (fota *FOTA) uncompressAndFlash(reader io.ReadCloser) error { | ||
defer reader.Close() | ||
var start time.Time | ||
gzipReader, err := gzip.NewReader(reader) | ||
if err != nil { | ||
return err | ||
} | ||
tarReader := tar.NewReader(gzipReader) | ||
for { | ||
if fota.verbose { | ||
start = time.Now() | ||
} | ||
header, err := tarReader.Next() | ||
if err == io.EOF { | ||
break | ||
} | ||
if err != nil { | ||
return err | ||
} | ||
part, present := fota.PartMapping[header.Name] | ||
if !present { | ||
// Image not in mapping, skipping. | ||
fota.log("Image is not present in the mapping. Skipping...", header.Name) | ||
continue | ||
} | ||
path := fota.SDcard + part | ||
fota.log("Flashing", header.Name, "to", path) | ||
written, err := fota.flash(tarReader, path) | ||
if err != nil { | ||
return err | ||
} | ||
fota.log("Flashed", header.Name, "to", path) | ||
if fota.verbose { | ||
duration := time.Since(start) | ||
log.Printf("Average speed: %.2f kB/s\n", float64(written)/(duration.Seconds()*1000)) | ||
} | ||
} | ||
return nil | ||
} | ||
|
||
func (fota *FOTA) log(str ...interface{}) { | ||
if fota.verbose { | ||
log.Println(str...) | ||
} | ||
} | ||
|
||
func (fota *FOTA) getMD5() error { | ||
r, err := http.Get(fota.md5sums) | ||
if err != nil { | ||
return err | ||
} | ||
defer r.Body.Close() | ||
if r.StatusCode != http.StatusOK { | ||
return fmt.Errorf("unexpected HTTP status: %s", r.Status) | ||
} | ||
s := bufio.NewScanner(r.Body) | ||
for s.Scan() { | ||
str := s.Text() | ||
var hash, filename string | ||
_, err := fmt.Sscanf(str, "%s %s", &hash, &filename) | ||
if err != nil { | ||
return err | ||
} | ||
fota.checksums[filename] = hash | ||
} | ||
return nil | ||
} | ||
|
||
// DownloadAndFlash downloads images from URLs, uncompresses them, | ||
// unpacks the archives and writes resulting ".img" files to proper partitions. | ||
// | ||
// Files not mentioned in PartMapping are ignored. | ||
// | ||
// MD5 checksum is calculated and error returned when mismatch occurs. | ||
// If MD5SUMS URL is not specified or downloaded file is not mentioned in it, | ||
// calculation results are ignored. | ||
func (fota *FOTA) DownloadAndFlash() (err error) { | ||
if fota.md5sums != "" { | ||
err = fota.getMD5() | ||
if err != nil { | ||
return | ||
} | ||
} | ||
|
||
for _, u := range fota.URLs { | ||
err = fota.downloadAndFlash(u) | ||
if err != nil { | ||
return | ||
} | ||
} | ||
return | ||
} | ||
|
||
func (fota *FOTA) downloadAndFlash(u string) error { | ||
parsedURL, err := url.Parse(u) | ||
if err != nil { | ||
return fmt.Errorf("parse URL (%v) failed: %s", u, err) | ||
} | ||
filename := path.Base(parsedURL.Path) | ||
response, err := http.Get(u) | ||
if err != nil { | ||
return fmt.Errorf("GET failed: %s", err) | ||
} | ||
defer response.Body.Close() | ||
if response.StatusCode != http.StatusOK { | ||
return fmt.Errorf("unexpected HTTP status code: %s", response.Status) | ||
} | ||
fota.log("Downloading images from:", u) | ||
hashChan, reader := fota.computeHash(response.Body) | ||
err = fota.uncompressAndFlash(reader) | ||
if err != nil { | ||
return fmt.Errorf("unpack or flash failed: %s", err) | ||
} | ||
hash := <-hashChan | ||
refHash, ok := fota.checksums[filename] | ||
if !ok { | ||
return nil | ||
} | ||
if h := fmt.Sprintf("%x", hash); h != refHash { | ||
return fmt.Errorf("MD5 checksum mismatch: %s != %s", h, refHash) | ||
} | ||
return nil | ||
} |
Oops, something went wrong.