Implement image updates (#580)

This commit is contained in:
andig 2021-01-09 11:47:38 +01:00 • committed by GitHub
parent a78f92e7cc
commit 33e4058b93
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
20 changed files with 1174 additions and 748 deletions

View file

@ -16,7 +16,25 @@ export const Base = Template.bind({});
Base.args = {
installed: "0.36",
available: "0.40",
// soc: false,
// socTitle: "Mein Auto",
// connected: true,
};
export const ReleaseNotes = Template.bind({});
ReleaseNotes.args = {
installed: "0.36",
available: "0.40",
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
};
export const Updater = Template.bind({});
Updater.args = {
installed: "0.36",
available: "0.40",
releaseNotes: "Lorem ipsum dolor sit amet consectetur",
hasUpdater: true,
};
export const Upgrade = Template.bind({});
Upgrade.args = {
installed: "0.36",
available: "0.40",
hasUpdater: true,
};

View file

@ -11,15 +11,26 @@
<fa-icon
icon="chevron-down"
class="expand-icon"
:class="{ 'expand-icon-rotated': releaseNotesExpanded }"
:class="{ 'expand-icon-rotated': releaseNotesShown }"
>
</fa-icon>
</a>
</b>
<b class="px-3">
<button
type="button"
class="btn btn-primary"
data-toggle="modal"
data-target="#updateModal"
v-if="hasUpdater"
@click="toggleUpdater"
>
Update
</button>
<a
:href="'https://github.com/andig/evcc/releases/tag/' + available"
class="text-body"
v-else
>
Download <fa-icon icon="chevron-down"></fa-icon>
</a>
@ -35,16 +46,71 @@
</button>
</div>
</div>
<transition name="fade">
<div class="row p-3 bg-light" v-if="releaseNotesExpanded">
<div class="row p-3 bg-light" v-if="releaseNotesShown">
<div class="col-12" v-html="releaseNotes"></div>
</div>
</transition>
<transition name="display">
<div
id="updateModal"
class="dialog"
tabindex="-1"
role="dialog"
v-if="updaterShown"
>
<div class="modal-dialog modal-dialog-centered" role="document">
<div class="modal-content">
<div class="modal-header">
<h4 class="modal-title font-weight-bold">
Aktualisierung durchführen
</h4>
<button type="button" class="close" @click="toggleUpdater">
<span aria-hidden="true">&times;</span>
</button>
</div>
<div class="modal-body">
<p class="font-weight-bold">
Nach Aktualisierung wird evcc neu gestartet.
</p>
<div
class="progress"
style="margin-top: 16px; margin-bottom: 16px"
v-if="updateStarted"
>
<div
class="progress-bar"
role="progressbar"
:style="{ width: uploadProgress + '%' }"
></div>
</div>
<p>{{ updateStatus }}{{ uploadMessage }}</p>
</div>
<div class="modal-footer">
<button
type="button"
class="btn btn-secondary"
:class="{ disabled: updateStarted }"
@click="toggleUpdater"
>
Abbrechen
</button>
<button type="button" class="btn btn-danger" @click="update">
Installieren
</button>
</div>
</div>
</div>
</div>
</transition>
</div>
</transition>
</template>
<script>
import axios from "axios";
import "../icons";
export default {
@ -53,11 +119,17 @@ export default {
installed: String,
available: String,
releaseNotes: String,
hasUpdater: Boolean,
uploadMessage: String,
uploadProgress: Number,
},
data: function () {
return {
dismissed: false,
releaseNotesExpanded: false,
releaseNotesShown: false,
updaterShown: false,
updateStarted: false,
updateStatus: "",
};
},
methods: {
@ -66,7 +138,24 @@ export default {
},
toggleReleaseNotes: function (e) {
e.preventDefault();
this.releaseNotesExpanded = !this.releaseNotesExpanded;
this.releaseNotesShown = !this.releaseNotesShown;
},
toggleUpdater: function (e) {
e.preventDefault();
if (!this.updateStarted) {
this.updaterShown = !this.updaterShown;
}
},
update: function () {
axios
.post("update")
.then(() => {
this.updateStatus = "Aktualisierung gestartet: ";
this.updateStarted = true;
})
.catch((res) => {
this.updateStatus = "Aktualisierung nicht möglich: " + res;
});
},
},
computed: {
@ -83,7 +172,7 @@ export default {
watch: {
available: function () {
this.dismissed = false;
this.releaseNotesExpanded = false;
this.releaseNotesShown = false;
},
},
};
@ -94,12 +183,25 @@ export default {
.fade-leave-active {
transition: opacity 0.25s ease-in;
}
.fade-enter,
.fade-leave-to {
opacity: 0;
}
.dialog {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
width: 100%;
height: 100%;
overflow: hidden;
outline: 0;
}
.display-enter-active {
display: block !important;
}
.expand-icon {
transition: transform 0.25s ease-in;
transform: rotate(0);

View file

@ -36,6 +36,9 @@
:installed="installedVersion"
:available="store.state.availableVersion"
:releaseNotes="store.state.releaseNotes"
:hasUpdater="store.state.hasUpdater"
:uploadMessage="store.state.uploadMessage"
:uploadProgress="store.state.uploadProgress"
></Version>
<router-view></router-view>

View file

@ -1,6 +1,8 @@
package cmd
import (
"fmt"
"github.com/spf13/viper"
"gopkg.in/yaml.v3"
)
@ -132,6 +134,9 @@ func demoConfig() (conf config) {
viper.Set(k, v)
}
// demo port
viper.Set("uri", fmt.Sprintf("0.0.0.0:%d", defaultPort))
if err := viper.UnmarshalExact(&conf); err != nil {
log.FATAL.Fatalf("failed loading demo config: %v", err)
}

5
cmd/demoport.go Normal file
View file

@ -0,0 +1,5 @@
// +build !gokrazy
package cmd
const defaultPort = 7070

5
cmd/demoport_gokrazy.go Normal file
View file

@ -0,0 +1,5 @@
// +build gokrazy
package cmd
const defaultPort = 80

View file

@ -13,7 +13,6 @@ import (
"github.com/andig/evcc/server/updater"
"github.com/andig/evcc/util"
"github.com/andig/evcc/util/pipe"
"github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp"
"github.com/spf13/cobra"
@ -162,7 +161,7 @@ func run(cmd *cobra.Command, args []string) {
configureJavascript(conf.Javascript)
// start broadcasting values
tee := &Tee{}
tee := &util.Tee{}
// value cache
cache := util.NewCache()
@ -191,13 +190,12 @@ func run(cmd *cobra.Command, args []string) {
// metrics
if viper.GetBool("metrics") {
httpd.Handle("/metrics", promhttp.Handler())
httpd.Router().Handle("/metrics", promhttp.Handler())
}
// pprof
if viper.GetBool("profile") {
router := httpd.Handler.(*mux.Router)
router.PathPrefix("/debug/").Handler(http.DefaultServeMux)
httpd.Router().PathPrefix("/debug/").Handler(http.DefaultServeMux)
}
// start HEMS server
@ -214,7 +212,7 @@ func run(cmd *cobra.Command, args []string) {
go tee.Run(valueChan)
// version check
go updater.Run(log, valueChan)
go updater.Run(log, httpd, tee, valueChan)
// capture log messages for UI
util.CaptureLogs(valueChan)

View file

@ -1,29 +0,0 @@
package cmd
import "github.com/andig/evcc/util"
// Tee distributed parameters to subscribers
type Tee struct {
recv []chan<- util.Param
}
// Attach creates a new receiver channel and attaches it to the tee
func (t *Tee) Attach() <-chan util.Param {
out := make(chan util.Param)
t.Add(out)
return out
}
// Add attaches a receiver channel to the tee
func (t *Tee) Add(out chan<- util.Param) {
t.recv = append(t.recv, out)
}
// Run starts parameter distribution
func (t *Tee) Run(in <-chan util.Param) {
for msg := range in {
for _, recv := range t.recv {
recv <- msg
}
}
}

4
go.mod
View file

@ -16,6 +16,7 @@ require (
github.com/go-ping/ping v0.0.0-20201022122018-3977ed72668a
github.com/go-telegram-bot-api/telegram-bot-api v4.6.4+incompatible
github.com/godbus/dbus/v5 v5.0.3
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24
github.com/golang/mock v1.4.4
github.com/golang/protobuf v1.4.3 // indirect
github.com/google/go-github/v32 v32.1.0
@ -52,12 +53,11 @@ require (
github.com/spf13/jwalterweatherman v1.1.0
github.com/spf13/pflag v1.0.5
github.com/spf13/viper v1.7.1
github.com/tcnksm/go-latest v0.0.0-20170313132115-e3007ae9052e
github.com/thoas/go-funk v0.7.0
github.com/tv42/httpunix v0.0.0-20191220191345-2ba4b9c3382c
github.com/volkszaehler/mbmd v0.0.0-20201205173745-5106cb0b334e
golang.org/x/net v0.0.0-20200904194848-62affa334b73
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 // indirect
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 // indirect
gopkg.in/ini.v1 v1.62.0
gopkg.in/sourcemap.v1 v1.0.5 // indirect
gopkg.in/yaml.v3 v3.0.0-20200615113413-eeeca48fe776

7
go.sum
View file

@ -90,6 +90,8 @@ github.com/goburrow/serial v0.1.0/go.mod h1:sAiqG0nRVswsm1C97xsttiYCzSLBmUZ/VSlV
github.com/godbus/dbus/v5 v5.0.3 h1:ZqHaoEF7TBzh4jzPmqVhE/5A1z9of6orkAe5uHoAeME=
github.com/godbus/dbus/v5 v5.0.3/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24 h1:tGm83sgE6OiTIyHylO7ZBsBZcMocwLdvtBIOJbpyUIU=
github.com/gokrazy/updater v0.0.0-20210106211705-4d92b338dd24/go.mod h1:PYOvzGOL4nlBmuxu7IyKQTFLaxr61+WPRNRzVtuYOHw=
github.com/golang/mock v1.4.3 h1:GV+pQPG/EUUbkh47niozDcADz6go/dUwhVzdUQHIVRw=
github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw=
github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc=
@ -405,6 +407,7 @@ golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208 h1:qwRHBd0NqMbJxfbotnDhm2ByMI1Shq4Y6oRJo21SGJA=
golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -435,8 +438,8 @@ golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1 h1:sIky/MyNRSHTrdxfsiUSS4WIA
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f h1:+Nyd8tzPX9R7BWHguqsrbFdRx3WQ/1ib8I44HXV5yTA=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014 h1:joucsQqXmyBVxViHCPFjG3hx8JzIFSaym3l3MM/Jsdg=
golang.org/x/sys v0.0.0-20201231184435-2d18734c6014/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363 h1:wHn06sgWHMO1VsQ8F+KzDJx/JzqfsNLnc+oEi07qD7s=
golang.org/x/sys v0.0.0-20210108172913-0df2131ae363/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=

View file

@ -80,7 +80,7 @@ func New(conf map[string]interface{}, site core.SiteAPI, cache *util.Cache, http
s.hostURI = s.callbackURI()
s.handlers(httpd.Router)
s.handlers(httpd.Router())
return s, err
}

File diff suppressed because it is too large Load diff

View file

@ -319,7 +319,6 @@ func SocketHandler(hub *SocketHub) http.HandlerFunc {
// HTTPd wraps an http.Server and adds the root router
type HTTPd struct {
*http.Server
*mux.Router
}
// NewHTTPd creates HTTP server with configured routes for loadpoint
@ -394,9 +393,12 @@ func NewHTTPd(url string, site core.SiteAPI, hub *SocketHub, cache *util.Cache)
IdleTimeout: 120 * time.Second,
ErrorLog: log.ERROR,
},
Router: router,
}
srv.SetKeepAlivesEnabled(true)
return srv
}
func (s *HTTPd) Router() *mux.Router {
return s.Handler.(*mux.Router)
}

107
server/updater/github.go Normal file
View file

@ -0,0 +1,107 @@
package updater
import (
"bytes"
"context"
"fmt"
"io"
"net/http"
"strings"
"time"
"github.com/andig/evcc/server"
"github.com/google/go-github/v32/github"
"github.com/hashicorp/go-version"
)
const timeout = 30 * time.Second
type Repo struct {
owner, repository string
*github.Client
}
func NewRepo(owner, repository string) *Repo {
r := &Repo{
owner: owner,
repository: repository,
Client: github.NewClient(nil),
}
return r
}
func (r *Repo) GetLatestRelease() (*github.RepositoryRelease, error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
release, _, err := r.Repositories.GetLatestRelease(ctx, r.owner, r.repository)
return release, err
}
// ReleaseNotes returns github release notes for the (from,to] semver interval
func (r *Repo) ReleaseNotes(from string) (rendered string, err error) {
ctx, cancel := context.WithTimeout(context.Background(), timeout)
defer cancel()
var fromVersion *version.Version
if fromVersion, err = version.NewVersion(from); err != nil {
return
}
releases, _, err := r.Repositories.ListReleases(ctx, r.owner, r.repository, nil)
if err != nil {
return
}
history := bytes.NewBuffer([]byte{})
for _, rel := range releases {
tag := *rel.TagName
var ver *version.Version
if ver, err = version.NewVersion(tag); err != nil {
return
}
if ver.LessThanOrEqual(fromVersion) {
continue
}
if notes := strings.TrimSpace(rel.GetBody()); notes != "" {
var md string
if md, _, err = r.Markdown(context.Background(), notes, &github.MarkdownOptions{
Mode: "gfm",
Context: fmt.Sprintf("%s/%s", server.Owner, server.Repository),
}); err != nil {
return
}
history.WriteString(fmt.Sprintf("<h1>%s</h1>\n", tag))
history.WriteString(md)
}
}
return history.String(), nil
}
func (r *Repo) FindReleaseAsset(name string) (int64, int, error) {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
rel, _, err := r.Repositories.GetLatestRelease(ctx, r.owner, r.repository)
if err != nil {
return 0, 0, err
}
for _, a := range rel.Assets {
if *a.Name == name {
return *a.ID, *a.Size, nil
}
}
return 0, 0, fmt.Errorf("asset not found: %s", name)
}
func (r *Repo) StreamAsset(id int64) (io.ReadCloser, error) {
rc, _, err := r.Repositories.DownloadReleaseAsset(context.Background(), r.owner, r.repository, id, http.DefaultClient)
return rc, err
}

99
server/updater/gokrazy.go Normal file
View file

@ -0,0 +1,99 @@
// +build gokrazy
package updater
import (
"compress/gzip"
"errors"
"fmt"
"io"
"net/http"
"sync/atomic"
"github.com/gokrazy/updater"
)
// update constants
const (
MB = 1024 * 1024
RootOffset = 8192*512 + 100*MB
RootSize = 500 * MB
RootFS = "evcc_%s.rootfs.gz"
Password = "FIat2WTD8i3tJikkvIJI"
Port = 8080
)
// unzipReader transparently unpacks zip files
func unzipReader(file io.ReadCloser) (io.ReadCloser, error) {
if unzipped, err := gzip.NewReader(file); err == nil {
return unzipped, err
}
return file, nil
}
type countingWriter struct {
count int
C chan int
}
func (cw *countingWriter) Write(p []byte) (n int, err error) {
cw.count += len(p)
cw.C <- cw.count
return len(p), nil
}
var mutex int32
// Update request handler
func (u *watch) execute(assetID int64, size int) error {
if !atomic.CompareAndSwapInt32(&mutex, 0, 1) {
return errors.New("upgrade already running")
}
defer atomic.StoreInt32(&mutex, 0)
rootFS, err := u.repo.StreamAsset(assetID)
if err != nil {
return err
}
if rootFS, err = unzipReader(rootFS); err != nil {
rootFS.Close()
return err
}
defer rootFS.Close()
cw := &countingWriter{C: make(chan int)}
go func() {
for v := range cw.C {
u.Send("uploadProgress", 100*v/size)
}
u.Send("uploadProgress", 100)
}()
uri := fmt.Sprintf("http://gokrazy:%s@localhost:%d/", Password, Port)
target, err := updater.NewTarget(uri, http.DefaultClient)
if err != nil {
return err
}
u.Send("uploadMessage", "uploading")
if err := target.StreamTo("root", io.TeeReader(rootFS, cw)); err != nil {
return fmt.Errorf("updating root file system: %w", err)
}
close(cw.C) // upload finished
u.Send("uploadMessage", "switching to non-active partition")
if err := target.Switch(); err != nil {
return fmt.Errorf("switching to non-active partition: %w", err)
}
u.Send("uploadMessage", "rebooting")
if err := target.Reboot(); err != nil {
return fmt.Errorf("reboot: %w", err)
}
return nil
}

25
server/updater/run.go Normal file
View file

@ -0,0 +1,25 @@
// +build !gokrazy
package updater
import (
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/google/go-github/v32/github"
)
// Run regularly checks version
func Run(log *util.Logger, httpd webServer, tee util.TeeAttacher, outChan chan<- util.Param) {
u := &watch{
log: log,
outChan: outChan,
repo: NewRepo(server.Owner, server.Repository),
}
c := make(chan *github.RepositoryRelease, 1)
go u.watchReleases(server.Version, c) // endless
for rel := range c {
u.Send("availableVersion", *rel.TagName)
}
}

View file

@ -0,0 +1,60 @@
// +build gokrazy
package updater
import (
"fmt"
"net/http"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/google/go-github/v32/github"
)
var (
latest *github.RepositoryRelease
)
// Run regularly checks version
func Run(log *util.Logger, httpd webServer, tee util.TeeAttacher, outChan chan<- util.Param) {
u := &watch{
log: log,
outChan: outChan,
repo: NewRepo(server.Owner, server.Repository),
}
httpd.Router().PathPrefix("/api/update").HandlerFunc(u.updateHandler)
c := make(chan *github.RepositoryRelease, 1)
go u.watchReleases(server.Version, c) // endless
for rel := range c {
u.Send("availableVersion", rel)
latest = rel
}
}
func (u *watch) updateHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost || latest == nil {
w.WriteHeader(http.StatusBadRequest)
return
}
name := fmt.Sprintf(RootFS, *latest.TagName)
assetID, size, err := u.repo.FindReleaseAsset(name)
if err != nil {
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "rootfs image not found: %v", err)
return
}
if err := u.execute(assetID, size); err != nil {
u.log.ERROR.Printf("could not find release image: %v", err)
w.WriteHeader(http.StatusBadRequest)
fmt.Fprintf(w, "update failed: %v", err)
return
}
w.WriteHeader(http.StatusOK)
}

View file

@ -1,128 +0,0 @@
package updater
import (
"bytes"
"context"
"fmt"
"strings"
"time"
"github.com/andig/evcc/server"
"github.com/andig/evcc/util"
"github.com/google/go-github/v32/github"
"github.com/hashicorp/go-version"
latest "github.com/tcnksm/go-latest"
)
var instance *updater
type updater struct {
log *util.Logger
cache chan<- util.Param
}
func tag2semver(tag string) string {
if strings.Count(tag, ".") < 2 {
tag += ".0"
}
return tag
}
func semver2tag(ver string) string {
return strings.TrimSuffix(ver, ".0")
}
// Run regularly checks version
func Run(log *util.Logger, cache chan<- util.Param) {
instance = &updater{
log: log,
cache: cache,
}
for range time.NewTicker(time.Hour).C {
instance.checkVersion()
}
}
// checkVersion validates if updates are available
func (u *updater) checkVersion() {
githubTag := &latest.GithubTag{
Owner: server.Owner,
Repository: server.Repository,
}
installed := tag2semver(server.Version)
if res, err := latest.Check(githubTag, installed); err == nil {
tag := semver2tag(res.Current)
if res.Outdated {
u.log.INFO.Printf("new version available - please upgrade to %s", tag)
}
u.cache <- util.Param{
Key: "availableVersion",
Val: tag,
}
u.fetchReleaseNotes()
} else {
u.log.ERROR.Println("version check failed:", err)
}
}
// fetchReleaseNotes retrieves release notes up to semver and sends to client
func (u *updater) fetchReleaseNotes() {
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if notes, err := releaseNotes(ctx, tag2semver(server.Version)); err == nil {
u.cache <- util.Param{
Key: "releaseNotes",
Val: notes,
}
} else {
u.log.WARN.Printf("couldn't download release notes: %v", err)
}
}
// releaseNotes returns github release notes for the (from,to] semver interval
func releaseNotes(ctx context.Context, from string) (rendered string, err error) {
var fromVersion *version.Version
if fromVersion, err = version.NewVersion(from); err != nil {
return
}
client := github.NewClient(nil)
releases, _, err := client.Repositories.ListReleases(ctx, server.Owner, server.Repository, nil)
if err != nil {
return
}
history := bytes.NewBuffer([]byte{})
for _, rel := range releases {
tag := *rel.TagName
var ver *version.Version
if ver, err = version.NewVersion(tag2semver(tag)); err != nil {
return
}
if ver.LessThanOrEqual(fromVersion) {
continue
}
if notes := strings.TrimSpace(rel.GetBody()); notes != "" {
var md string
if md, _, err = client.Markdown(context.Background(), notes, &github.MarkdownOptions{
Mode: "gfm",
Context: fmt.Sprintf("%s/%s", server.Owner, server.Repository),
}); err != nil {
return
}
history.WriteString(fmt.Sprintf("<h1>%s</h1>\n", tag))
history.WriteString(md)
}
}
return history.String(), nil
}

85
server/updater/watch.go Normal file
View file

@ -0,0 +1,85 @@
package updater
import (
"errors"
"time"
"github.com/andig/evcc/util"
"github.com/google/go-github/v32/github"
"github.com/gorilla/mux"
"github.com/hashicorp/go-version"
)
type webServer interface {
Router() *mux.Router
}
type watch struct {
log *util.Logger
outChan chan<- util.Param
repo *Repo
}
func (u *watch) Send(key string, val interface{}) {
u.outChan <- util.Param{
Key: key,
Val: val,
}
}
func (u *watch) watchReleases(installed string, out chan *github.RepositoryRelease) {
for range time.NewTicker(6 * time.Hour).C {
rel, err := u.findReleaseUpdate(installed)
if err != nil {
u.log.ERROR.Printf("version check failed: %v", err)
continue
}
if rel != nil {
u.log.INFO.Printf("new version available: %s", *rel.TagName)
out <- rel
}
}
}
// findReleaseUpdate validates if updates are available
func (u *watch) findReleaseUpdate(installed string) (*github.RepositoryRelease, error) {
rel, err := u.repo.GetLatestRelease()
if err != nil {
return nil, err
}
if rel.TagName == nil {
return nil, errors.New("untagged release")
}
v1, err := version.NewVersion(installed)
if err != nil {
return nil, err
}
v2, err := version.NewVersion(*rel.TagName)
if err != nil {
return nil, err
}
if v1.LessThan(v2) {
go u.fetchReleaseNotes(installed)
return rel, nil
}
// no update
return nil, nil
}
// fetchReleaseNotes retrieves release notes up to semver and sends to client
func (u *watch) fetchReleaseNotes(installed string) {
if notes, err := u.repo.ReleaseNotes(installed); err == nil {
u.outChan <- util.Param{
Key: "releaseNotes",
Val: notes,
}
} else {
u.log.WARN.Printf("couldn't download release notes: %v", err)
}
}

32
util/tee.go Normal file
View file

@ -0,0 +1,32 @@
package util
// TeeAttacher allows to attach a listener to a tee
type TeeAttacher interface {
Attach() <-chan Param
}
// Tee distributed parameters to subscribers
type Tee struct {
recv []chan<- Param
}
// Attach creates a new receiver channel and attaches it to the tee
func (t *Tee) Attach() <-chan Param {
out := make(chan Param)
t.add(out)
return out
}
// add attaches a receiver channel to the tee
func (t *Tee) add(out chan<- Param) {
t.recv = append(t.recv, out)
}
// Run starts parameter distribution
func (t *Tee) Run(in <-chan Param) {
for msg := range in {
for _, recv := range t.recv {
recv <- msg
}
}
}