mirror of
https://github.com/token2/snapd.git
synced 2026-03-13 11:15:47 -07:00
Some languages may use multibyte characters for duration. In such case, the width of the progress bar would be incorrectly calculated, making the displayed label too short. At runtime this would randomly cause a panic with the translation used a multibyte characters: panic: runtime error: slice bounds out of range [80:79] [recovered] panic: runtime error: slice bounds out of range [80:79] goroutine 1 [running]: main.main.func1() github.com/snapcore/snapd/cmd/snap/main.go:477 +0x95 panic(0x561d5f7fac80, 0xc000144280) runtime/panic.go:679 +0x1b6 github.com/snapcore/snapd/progress.(*ANSIMeter).Set(0xc000214eb0, 0x41a891a000000000) github.com/snapcore/snapd/progress/ansimeter.go:152 +0xa1b main.waitMixin.wait(0xc0002e42a0, 0x561d5f3e0000, 0xc0001e4488, 0x2, 0x0, 0x0, 0x0) github.com/snapcore/snapd/cmd/snap/wait.go:130 +0x700 main.(*cmdInstall).installOne(0xc000359040, 0x7ffcf44c33e4, 0xb, 0x0, 0x0, 0xc00036cba0, 0x561d5f805060, 0x561d5f8693a0) github.com/snapcore/snapd/cmd/snap/cmd_snap_op.go:491 +0x316 main.(*cmdInstall).Execute(0xc000359040, 0xc0003787a0, 0x0, 0x2, 0xc000359040, 0x1) github.com/snapcore/snapd/cmd/snap/cmd_snap_op.go:596 +0x3d7 github.com/snapcore/snapd/vendor/github.com/jessevdk/go-flags.(*Parser).ParseArgs(0xc000316bd0, 0xc000032190, 0x2, 0x2, 0xc000330330, 0xc0002a5cd0, 0x561d5ee49fbd, 0x561d5f7d08a0, 0xc000330330) github.com/snapcore/snapd/vendor/github.com/jessevdk/go-flags/parser.go:333 +0x8e7 github.com/snapcore/snapd/vendor/github.com/jessevdk/go-flags.(*Parser).Parse(...) github.com/snapcore/snapd/vendor/github.com/jessevdk/go-flags/parser.go:190 main.run(0xc0002a5de0, 0xe) github.com/snapcore/snapd/cmd/snap/main.go:515 +0xa7 main.main() github.com/snapcore/snapd/cmd/snap/main.go:482 +0x371 Since we are assuming that terminal can display up to the column number of runes, fix the length calculation to use slices of runes for all components (keep in mind that len(string) >= len([]rune)). Fixes: https://bugs.launchpad.net/ubuntu/+source/snapd/+bug/1876583 Signed-off-by: Maciej Borzecki <maciej.zenon.borzecki@canonical.com>
214 lines
4.9 KiB
Go
214 lines
4.9 KiB
Go
// -*- Mode: Go; indent-tabs-mode: t -*-
|
|
|
|
/*
|
|
* Copyright (C) 2017 Canonical Ltd
|
|
*
|
|
* This program is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License version 3 as
|
|
* published by the Free Software Foundation.
|
|
*
|
|
* This program is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*
|
|
*/
|
|
|
|
package progress
|
|
|
|
import (
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"time"
|
|
"unicode"
|
|
|
|
"golang.org/x/crypto/ssh/terminal"
|
|
|
|
"github.com/snapcore/snapd/strutil/quantity"
|
|
)
|
|
|
|
var stdout io.Writer = os.Stdout
|
|
|
|
// ANSIMeter is a progress.Meter that uses ANSI escape codes to make
|
|
// better use of the available horizontal space.
|
|
type ANSIMeter struct {
|
|
label []rune
|
|
total float64
|
|
written float64
|
|
spin int
|
|
t0 time.Time
|
|
}
|
|
|
|
// these are the bits of the ANSI escapes (beyond \r) that we use
|
|
// (names of the terminfo capabilities, see terminfo(5))
|
|
var (
|
|
// clear to end of line
|
|
clrEOL = "\033[K"
|
|
// make cursor invisible
|
|
cursorInvisible = "\033[?25l"
|
|
// make cursor visible
|
|
cursorVisible = "\033[?25h"
|
|
// turn on reverse video
|
|
enterReverseMode = "\033[7m"
|
|
// go back to normal video
|
|
exitAttributeMode = "\033[0m"
|
|
)
|
|
|
|
var termWidth = func() int {
|
|
col, _, _ := terminal.GetSize(0)
|
|
if col <= 0 {
|
|
// give up
|
|
col = 80
|
|
}
|
|
return col
|
|
}
|
|
|
|
func (p *ANSIMeter) Start(label string, total float64) {
|
|
p.label = []rune(label)
|
|
p.total = total
|
|
p.t0 = time.Now().UTC()
|
|
fmt.Fprint(stdout, cursorInvisible)
|
|
}
|
|
|
|
func norm(col int, msg []rune) []rune {
|
|
if col <= 0 {
|
|
return []rune{}
|
|
}
|
|
out := make([]rune, col)
|
|
copy(out, msg)
|
|
d := col - len(msg)
|
|
if d < 0 {
|
|
out[col-1] = '…'
|
|
} else {
|
|
for i := len(msg); i < col; i++ {
|
|
out[i] = ' '
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func (p *ANSIMeter) SetTotal(total float64) {
|
|
p.total = total
|
|
}
|
|
|
|
func (p *ANSIMeter) percent() string {
|
|
if p.total == 0. {
|
|
return "---%"
|
|
}
|
|
q := p.written * 100 / p.total
|
|
if q > 999.4 || q < 0. {
|
|
return "???%"
|
|
}
|
|
return fmt.Sprintf("%3.0f%%", q)
|
|
}
|
|
|
|
var formatDuration = quantity.FormatDuration
|
|
|
|
func (p *ANSIMeter) Set(current float64) {
|
|
if current < 0 {
|
|
current = 0
|
|
}
|
|
if current > p.total {
|
|
current = p.total
|
|
}
|
|
|
|
p.written = current
|
|
col := termWidth()
|
|
// time left: 5
|
|
// gutter: 1
|
|
// speed: 8
|
|
// gutter: 1
|
|
// percent: 4
|
|
// gutter: 1
|
|
// =====
|
|
// 20
|
|
// and we want to leave at least 10 for the label, so:
|
|
// * if width <= 15, don't show any of this (progress bar is good enough)
|
|
// * if 15 < width <= 20, only show time left (time left + gutter = 6)
|
|
// * if 20 < width <= 29, also show percentage (percent + gutter = 5
|
|
// * if 29 < width , also show speed (speed+gutter = 9)
|
|
var percent, speed, timeleft string
|
|
if col > 15 {
|
|
since := time.Now().UTC().Sub(p.t0).Seconds()
|
|
per := since / p.written
|
|
left := (p.total - p.written) * per
|
|
// XXX: duration unit string is controlled by translations, and
|
|
// may carry a multibyte unit suffix
|
|
timeleft = " " + formatDuration(left)
|
|
if col > 20 {
|
|
percent = " " + p.percent()
|
|
if col > 29 {
|
|
speed = " " + quantity.FormatBPS(p.written, since, -1)
|
|
}
|
|
}
|
|
}
|
|
|
|
rpercent := []rune(percent)
|
|
rspeed := []rune(speed)
|
|
rtimeleft := []rune(timeleft)
|
|
msg := make([]rune, 0, col)
|
|
// XXX: assuming terminal can display `col` number of runes
|
|
msg = append(msg, norm(col-len(rpercent)-len(rspeed)-len(rtimeleft), p.label)...)
|
|
msg = append(msg, rpercent...)
|
|
msg = append(msg, rspeed...)
|
|
msg = append(msg, rtimeleft...)
|
|
i := int(current * float64(col) / p.total)
|
|
fmt.Fprint(stdout, "\r", enterReverseMode, string(msg[:i]), exitAttributeMode, string(msg[i:]))
|
|
}
|
|
|
|
var spinner = []string{"/", "-", "\\", "|"}
|
|
|
|
func (p *ANSIMeter) Spin(msgstr string) {
|
|
msg := []rune(msgstr)
|
|
col := termWidth()
|
|
if col-2 >= len(msg) {
|
|
fmt.Fprint(stdout, "\r", string(norm(col-2, msg)), " ", spinner[p.spin])
|
|
p.spin++
|
|
if p.spin >= len(spinner) {
|
|
p.spin = 0
|
|
}
|
|
} else {
|
|
fmt.Fprint(stdout, "\r", string(norm(col, msg)))
|
|
}
|
|
}
|
|
|
|
func (*ANSIMeter) Finished() {
|
|
fmt.Fprint(stdout, "\r", exitAttributeMode, cursorVisible, clrEOL)
|
|
}
|
|
|
|
func (*ANSIMeter) Notify(msgstr string) {
|
|
col := termWidth()
|
|
fmt.Fprint(stdout, "\r", exitAttributeMode, clrEOL)
|
|
|
|
msg := []rune(msgstr)
|
|
var i int
|
|
for len(msg) > col {
|
|
for i = col; i >= 0; i-- {
|
|
if unicode.IsSpace(msg[i]) {
|
|
break
|
|
}
|
|
}
|
|
if i < 1 {
|
|
// didn't find anything; print the whole thing and try again
|
|
fmt.Fprintln(stdout, string(msg[:col]))
|
|
msg = msg[col:]
|
|
} else {
|
|
// found a space; print up to but not including it, and skip it
|
|
fmt.Fprintln(stdout, string(msg[:i]))
|
|
msg = msg[i+1:]
|
|
}
|
|
}
|
|
fmt.Fprintln(stdout, string(msg))
|
|
}
|
|
|
|
func (p *ANSIMeter) Write(bs []byte) (n int, err error) {
|
|
n = len(bs)
|
|
p.Set(p.written + float64(n))
|
|
|
|
return
|
|
}
|