chore: use extracted talos-systems/go-kmsg library

This change uses extracted go-kmsg library (see
https://github.com/talos-systems/go-kmsg/pull/1).

Signed-off-by: Andrey Smirnov <smirnov.andrey@gmail.com>
This commit is contained in:
Andrey Smirnov 2021-05-04 18:34:33 +03:00 committed by talos-bot
parent 79d804c5b4
commit f2caed0df5
14 changed files with 10 additions and 676 deletions

3
go.mod
View File

@ -70,6 +70,7 @@ require (
github.com/talos-systems/crypto v0.2.1-0.20210427105118-4f80b976b640
github.com/talos-systems/go-blockdevice v0.2.1-0.20210407132431-1d830a25f64f
github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0
github.com/talos-systems/go-kmsg v0.1.0
github.com/talos-systems/go-loadbalancer v0.1.0
github.com/talos-systems/go-procfs v0.0.0-20210108152626-8cbc42d3dc24
github.com/talos-systems/go-retry v0.2.1-0.20210119124456-b9dc1a990133
@ -87,7 +88,7 @@ require (
go.etcd.io/etcd/pkg/v3 v3.5.0-alpha.0
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6
golang.org/x/term v0.0.0-20210220032956-6a3ed077a48d
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20210427135350-f9ad6d392236

5
go.sum
View File

@ -1029,6 +1029,8 @@ github.com/talos-systems/go-blockdevice v0.2.1-0.20210407132431-1d830a25f64f h1:
github.com/talos-systems/go-blockdevice v0.2.1-0.20210407132431-1d830a25f64f/go.mod h1:qnn/zDc09I1DA2BUDDCOSA2D0P8pIDjN8pGiRoRaQig=
github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0 h1:DI+BjK+fcrLBc70Fi50dZocQcaHosqsuWHrGHKp2NzE=
github.com/talos-systems/go-cmd v0.0.0-20210216164758-68eb0067e0f0/go.mod h1:kf+rZzTEmlDiYQ6ulslvRONnKLQH8x83TowltGMhO+k=
github.com/talos-systems/go-kmsg v0.1.0 h1:juoZn+XioduYvtie6nqi/miKGJPLYSBNXRv5jRe6+lE=
github.com/talos-systems/go-kmsg v0.1.0/go.mod h1:dppwQn+/mrdvsziGMbXjzfc4E+75oZhr39UIP6LgL0w=
github.com/talos-systems/go-loadbalancer v0.1.0 h1:MQFONvSjoleU8RrKq1O1Z8CyTCJGd4SLqdAHDlR6o9s=
github.com/talos-systems/go-loadbalancer v0.1.0/go.mod h1:D5Qjfz+29WVjONWECZvOkmaLsBb3f5YeWME0u/5HmIc=
github.com/talos-systems/go-procfs v0.0.0-20210108152626-8cbc42d3dc24 h1:fN8vYvlB9XBQ5aImb1vLgR0ZaDwvfZfBMptqkpi3aEg=
@ -1377,8 +1379,9 @@ golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210216163648-f7da38b97c65/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210309040221-94ec62e08169/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887 h1:dXfMednGJh/SUUFjTLsWJz3P+TQt9qnR11GgeI3vWKs=
golang.org/x/sys v0.0.0-20210426230700-d19ff857e887/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6 h1:cdsMqa2nXzqlgs183pHxtvoVwU7CyzaCTAUOg94af4c=
golang.org/x/sys v0.0.0-20210503173754-0981d6026fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20201210144234-2321bbc49cbf/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=

View File

@ -14,10 +14,10 @@ import (
"syscall"
"time"
"github.com/talos-systems/go-kmsg"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/internal/pkg/kmsg"
"github.com/talos-systems/talos/internal/pkg/mount"
"github.com/talos-systems/talos/internal/pkg/mount/switchroot"
"github.com/talos-systems/talos/pkg/machinery/constants"

View File

@ -18,13 +18,13 @@ import (
"github.com/containerd/containerd/namespaces"
"github.com/containerd/containerd/oci"
"github.com/opencontainers/runtime-spec/specs-go"
"github.com/talos-systems/go-kmsg"
"github.com/talos-systems/go-procfs/procfs"
"golang.org/x/sys/unix"
"github.com/talos-systems/talos/internal/app/machined/pkg/runtime"
containerdrunner "github.com/talos-systems/talos/internal/app/machined/pkg/system/runner/containerd"
"github.com/talos-systems/talos/internal/pkg/containers/image"
"github.com/talos-systems/talos/internal/pkg/kmsg"
machineapi "github.com/talos-systems/talos/pkg/machinery/api/machine"
"github.com/talos-systems/talos/pkg/machinery/config"
"github.com/talos-systems/talos/pkg/machinery/constants"

View File

@ -33,6 +33,7 @@ import (
"github.com/prometheus/procfs"
"github.com/rs/xid"
"github.com/talos-systems/go-blockdevice/blockdevice/partition/gpt"
"github.com/talos-systems/go-kmsg"
clientv3 "go.etcd.io/etcd/client/v3"
"go.etcd.io/etcd/client/v3/concurrency"
"golang.org/x/sys/unix"
@ -54,7 +55,6 @@ import (
"github.com/talos-systems/talos/internal/pkg/containers/cri"
"github.com/talos-systems/talos/internal/pkg/containers/image"
"github.com/talos-systems/talos/internal/pkg/etcd"
"github.com/talos-systems/talos/internal/pkg/kmsg"
"github.com/talos-systems/talos/internal/pkg/kubeconfig"
"github.com/talos-systems/talos/internal/pkg/mount"
"github.com/talos-systems/talos/pkg/archiver"

View File

@ -30,6 +30,7 @@ import (
"github.com/talos-systems/go-blockdevice/blockdevice/partition/gpt"
"github.com/talos-systems/go-blockdevice/blockdevice/util"
"github.com/talos-systems/go-cmd/pkg/cmd"
"github.com/talos-systems/go-kmsg"
"github.com/talos-systems/go-procfs/procfs"
"github.com/talos-systems/go-retry/retry"
clientv3 "go.etcd.io/etcd/client/v3"
@ -52,7 +53,6 @@ import (
"github.com/talos-systems/talos/internal/pkg/cri"
"github.com/talos-systems/talos/internal/pkg/etcd"
"github.com/talos-systems/talos/internal/pkg/kernel/kspp"
"github.com/talos-systems/talos/internal/pkg/kmsg"
"github.com/talos-systems/talos/internal/pkg/mount"
"github.com/talos-systems/talos/internal/pkg/partition"
"github.com/talos-systems/talos/pkg/conditions"

View File

@ -1,47 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
// Package kmsg provides access to kernel log.
//
package kmsg
import (
"fmt"
"io"
"log"
"os"
"golang.org/x/sys/unix"
)
// SetupLogger configures the logger to write to the kernel ring buffer via
// /dev/kmsg.
//
// If logger is nil, default `log` logger is redirectred.
//
// If extraWriter is not nil, logs will be copied to it as well.
func SetupLogger(logger *log.Logger, prefix string, extraWriter io.Writer) error {
kmsg, err := os.OpenFile("/dev/kmsg", os.O_RDWR|unix.O_CLOEXEC|unix.O_NONBLOCK|unix.O_NOCTTY, 0o666)
if err != nil {
return fmt.Errorf("failed to open /dev/kmsg: %w", err)
}
var writer io.Writer = &Writer{KmsgWriter: kmsg}
if extraWriter != nil {
writer = io.MultiWriter(writer, extraWriter)
}
if logger != nil {
logger.SetOutput(writer)
logger.SetPrefix(prefix + " ")
logger.SetFlags(0)
} else {
log.SetOutput(writer)
log.SetPrefix(prefix + " ")
log.SetFlags(0)
}
return nil
}

View File

@ -1,120 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg
import (
"fmt"
"strconv"
"strings"
"time"
)
// Facility is an attribute of kernel log message.
type Facility int
// Kernel log facilities.
//
// From <sys/syslog.h>.
const (
Kern Facility = iota
User
Mail
Daemon
Auth
Syslog
Lpr
News
Uucp
Cron
AuthPriv
Local0
Local1
Local2
Local3
Local4
Local5
Local6
Local7
)
func (f Facility) String() string {
return [...]string{
"kern", "user", "mail", "daemon",
"auth", "syslog", "lpr", "news", "uucp",
"cron", "authpriv",
"local0", "local1", "local2", "local3",
"local4", "local5", "local6", "local7",
}[f]
}
// Priority is an attribute of kernel log message.
type Priority int
// Kernel log priorities.
const (
Emerg Priority = iota
Alert
Crit
Err
Warning
Notice
Info
Debug
)
func (p Priority) String() string {
return [...]string{"emerg", "alert", "crit", "err", "warning", "notice", "info", "debug"}[p]
}
// Message is a parsed kernel log message.
type Message struct {
Facility Facility
Priority Priority
SequenceNumber int64
Clock int64
Timestamp time.Time
Message string
}
// ParseMessage parses internal kernel log format.
//
// Reference: https://www.kernel.org/doc/Documentation/ABI/testing/dev-kmsg
func ParseMessage(input string, bootTime time.Time) (Message, error) {
parts := strings.SplitN(input, ";", 2)
if len(parts) != 2 {
return Message{}, fmt.Errorf("kernel message should contain a prefix")
}
prefix, message := parts[0], parts[1]
metadata := strings.Split(prefix, ",")
if len(metadata) < 3 {
return Message{}, fmt.Errorf("message metdata should have at least 3 parts, got %d", len(metadata))
}
syslogPrefix, err := strconv.ParseInt(metadata[0], 10, 64)
if err != nil {
return Message{}, fmt.Errorf("error parsing priority: %w", err)
}
sequence, err := strconv.ParseInt(metadata[1], 10, 64)
if err != nil {
return Message{}, fmt.Errorf("error parsing sequence: %w", err)
}
clock, err := strconv.ParseInt(metadata[2], 10, 64)
if err != nil {
return Message{}, fmt.Errorf("errors parsing clock from boot: %w", err)
}
return Message{
Priority: Priority(syslogPrefix & 7),
Facility: Facility(syslogPrefix >> 3),
SequenceNumber: sequence,
Clock: clock,
Timestamp: bootTime.Add(time.Duration(clock) * time.Microsecond),
Message: message,
}, nil
}

View File

@ -1,60 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg_test
import (
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/internal/pkg/kmsg"
)
func mustParse(tStr string) time.Time {
t, err := time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", tStr)
if err != nil {
panic(err)
}
return t
}
func TestParseMessage(t *testing.T) {
for _, testCase := range []struct {
input string
expected kmsg.Message
}{
{
input: `7,160,424069,-;pci_root PNP0A03:00: host bridge window [io 0x0000-0x0cf7] (ignored)
SUBSYSTEM=acpi
DEVICE=+acpi:PNP0A03:00`,
expected: kmsg.Message{
Facility: kmsg.Kern,
Priority: kmsg.Debug,
SequenceNumber: 160,
Clock: 424069,
Timestamp: mustParse("0001-01-01 00:00:00.424069 +0000 UTC"),
Message: "pci_root PNP0A03:00: host bridge window [io 0x0000-0x0cf7] (ignored)\n SUBSYSTEM=acpi\n DEVICE=+acpi:PNP0A03:00",
},
},
{
input: `6,339,5140900,-;NET: Registered protocol family 10`,
expected: kmsg.Message{
Facility: kmsg.Kern,
Priority: kmsg.Info,
SequenceNumber: 339,
Clock: 5140900,
Timestamp: mustParse("0001-01-01 00:00:05.1409 +0000 UTC"),
Message: "NET: Registered protocol family 10",
},
},
} {
message, err := kmsg.ParseMessage(testCase.input, time.Time{})
assert.NoError(t, err)
assert.Equal(t, testCase.expected, message)
}
}

View File

@ -1,200 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg
import (
"context"
"fmt"
"io"
"os"
"syscall"
"time"
)
// Packet combines Message and error.
//
// Only one of the fields is set in Reader.Scan.
type Packet struct {
Message Message
Err error
}
// Reader for /dev/kmsg messages.
type Reader interface {
// Scan and issue parsed messages.
//
// Scan stops when context is canceled or when EOF is reached
// in NoFollow mode.
Scan(ctx context.Context) <-chan Packet
// Close releases resources associated with the Reader.
Close() error
}
// Option configures Reader.
type Option func(*options)
type options struct {
follow bool
tail bool
}
// Follow the kmsg to stream live messages.
func Follow() Option {
return func(o *options) {
o.follow = true
}
}
// FromTail starts reading kmsg from the tail (after last message).
func FromTail() Option {
return func(o *options) {
o.tail = true
}
}
// NewReader initializes new /dev/kmsg reader.
func NewReader(options ...Option) (Reader, error) {
r := &reader{}
for _, o := range options {
o(&r.options)
}
var err error
r.bootTime, err = getBootTime()
if err != nil {
return nil, err
}
r.f, err = os.OpenFile("/dev/kmsg", os.O_RDONLY, 0)
if err != nil {
return nil, err
}
if r.options.tail {
_, err = r.f.Seek(0, os.SEEK_END)
if err != nil {
r.f.Close() //nolint:errcheck
return nil, fmt.Errorf("error seeking to the tail of kmsg: %w", err)
}
}
return r, nil
}
type reader struct {
f *os.File
options options
bootTime time.Time
}
func (r *reader) Close() error {
return r.f.Close()
}
func (r *reader) Scan(ctx context.Context) <-chan Packet {
ch := make(chan Packet)
if r.options.follow {
go r.scanFollow(ctx, ch)
} else {
go r.scanNoFollow(ctx, ch)
}
return ch
}
//nolint:gocyclo
func (r *reader) scanNoFollow(ctx context.Context, ch chan<- Packet) {
defer close(ch)
fd := int(r.f.Fd())
if err := syscall.SetNonblock(fd, true); err != nil {
select {
case ch <- Packet{
Err: fmt.Errorf("error switching to nonblock mode: %w", err),
}:
case <-ctx.Done():
}
return
}
buf := make([]byte, 8192)
for {
n, err := syscall.Read(fd, buf)
if err != nil {
if err == io.EOF || err == syscall.EAGAIN {
// end of file, done
return
}
if err == syscall.EPIPE {
// buffer overrun, retry
continue
}
select {
case ch <- Packet{
Err: fmt.Errorf("error reading from kmsg: %w", err),
}:
case <-ctx.Done():
}
return
}
var packet Packet
packet.Message, packet.Err = ParseMessage(string(buf[:n]), r.bootTime)
select {
case ch <- packet:
case <-ctx.Done():
}
}
}
func (r *reader) scanFollow(ctx context.Context, ch chan<- Packet) {
defer close(ch)
buf := make([]byte, 8192)
for {
n, err := r.f.Read(buf)
if err != nil {
if err == io.EOF {
// end of file, done
return
}
if err == syscall.EPIPE {
// buffer overrun, retry
continue
}
select {
case ch <- Packet{
Err: fmt.Errorf("error reading from kmsg: %w", err),
}:
case <-ctx.Done():
}
return
}
var packet Packet
packet.Message, packet.Err = ParseMessage(string(buf[:n]), r.bootTime)
select {
case ch <- packet:
case <-ctx.Done():
}
}
}

View File

@ -1,109 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg_test
import (
"context"
"errors"
"os"
"testing"
"time"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/internal/pkg/kmsg"
)
//nolint:thelper
func skipIfNoKmsg(t *testing.T) {
f, err := os.OpenFile("/dev/kmsg", os.O_RDONLY, 0)
if err != nil {
t.Skip("/dev/kmsg is not available", err.Error())
}
f.Close() //nolint:errcheck
}
func TestReaderNoFollow(t *testing.T) {
skipIfNoKmsg(t)
r, err := kmsg.NewReader()
assert.NoError(t, err)
defer r.Close() //nolint:errcheck
messageCount := 0
for packet := range r.Scan(context.Background()) {
assert.NoError(t, packet.Err)
messageCount++
}
assert.Greater(t, messageCount, 0)
assert.NoError(t, r.Close())
}
func TestReaderFollow(t *testing.T) {
testReaderFollow(t, true)
}
func TestReaderFollowTail(t *testing.T) {
testReaderFollow(t, false, kmsg.FromTail())
}
//nolint:thelper
func testReaderFollow(t *testing.T, expectMessages bool, options ...kmsg.Option) {
skipIfNoKmsg(t)
r, err := kmsg.NewReader(append([]kmsg.Option{kmsg.Follow()}, options...)...)
assert.NoError(t, err)
defer r.Close() //nolint:errcheck
messageCount := 0
ctx, ctxCancel := context.WithCancel(context.Background())
defer ctxCancel()
ch := r.Scan(ctx)
var closed bool
LOOP:
for {
select {
case packet, ok := <-ch:
if !ok {
if !closed {
assert.Fail(t, "channel closed before cancel")
}
break LOOP
}
if closed && errors.Is(packet.Err, os.ErrClosed) {
// ignore 'file already closed' error as it might happen
// from the branch below depending on whether context cancel or
// read() finishes first
continue
}
assert.NoError(t, packet.Err)
messageCount++
case <-time.After(100 * time.Millisecond):
// abort
closed = true
ctxCancel()
assert.NoError(t, r.Close())
}
}
if expectMessages {
assert.Greater(t, messageCount, 0)
}
}

View File

@ -1,23 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg
import (
"fmt"
"syscall"
"time"
)
func getBootTime() (time.Time, error) {
var sysinfo syscall.Sysinfo_t
err := syscall.Sysinfo(&sysinfo)
if err != nil {
return time.Time{}, fmt.Errorf("could not get boot time: %w", err)
}
// sysinfo only has seconds
return time.Now().Add(-1 * (time.Duration(sysinfo.Uptime) * time.Second)), nil
}

View File

@ -1,53 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg
import (
"bytes"
"io"
)
// MaxLineLength to be passed to kmsg, see https://github.com/torvalds/linux/blob/master/kernel/printk/printk.c#L450.
const MaxLineLength = 1024 - 48
// Writer ensures writes by line and limits each line to maxLineLength characters.
//
// This workarounds kmsg limits.
type Writer struct {
KmsgWriter io.Writer
}
// Write implements io.Writer interface.
func (w *Writer) Write(p []byte) (n int, err error) {
// split writes by `\n`, and limit each line to MaxLineLength
for len(p) > 0 {
i := bytes.IndexByte(p, '\n')
if i == -1 {
i = len(p) - 1
}
line := p[:i+1]
if len(line) > MaxLineLength {
line = append(line[:MaxLineLength-4], []byte("...\n")...)
}
var nn int
nn, err = w.KmsgWriter.Write(line)
if nn == len(line) {
n += i + 1
} else {
n += nn
}
if err != nil {
return
}
p = p[i+1:]
}
return
}

View File

@ -1,58 +0,0 @@
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this
// file, You can obtain one at http://mozilla.org/MPL/2.0/.
package kmsg_test
import (
"bytes"
"testing"
"github.com/stretchr/testify/assert"
"github.com/talos-systems/talos/internal/pkg/kmsg"
)
type fakeWriter struct {
lines [][]byte
}
func (w *fakeWriter) Write(p []byte) (n int, err error) {
w.lines = append(w.lines, append([]byte(nil), p...))
return len(p), nil
}
func TestWriter(t *testing.T) {
fakeW := &fakeWriter{}
kmsgW := &kmsg.Writer{KmsgWriter: fakeW}
n, err := kmsgW.Write([]byte("foo"))
assert.Equal(t, 3, n)
assert.NoError(t, err)
n, err = kmsgW.Write([]byte("bar\n"))
assert.Equal(t, 4, n)
assert.NoError(t, err)
n, err = kmsgW.Write([]byte("foo\nbar\n"))
assert.Equal(t, 8, n)
assert.NoError(t, err)
n, err = kmsgW.Write(append(bytes.Repeat([]byte{0xce}, kmsg.MaxLineLength-1), '\n'))
assert.Equal(t, kmsg.MaxLineLength, n)
assert.NoError(t, err)
n, err = kmsgW.Write(append(bytes.Repeat([]byte{0xce}, kmsg.MaxLineLength), '\n', 'a', 'b', '\n'))
assert.Equal(t, kmsg.MaxLineLength+4, n)
assert.NoError(t, err)
assert.Len(t, fakeW.lines, 7)
assert.Equal(t, fakeW.lines[0], []byte("foo"))
assert.Equal(t, fakeW.lines[1], []byte("bar\n"))
assert.Equal(t, fakeW.lines[2], []byte("foo\n"))
assert.Equal(t, fakeW.lines[3], []byte("bar\n"))
assert.Equal(t, fakeW.lines[4], append(bytes.Repeat([]byte{0xce}, kmsg.MaxLineLength-1), '\n'))
assert.Equal(t, fakeW.lines[5], append(bytes.Repeat([]byte{0xce}, kmsg.MaxLineLength-4), '.', '.', '.', '\n'))
assert.Equal(t, fakeW.lines[6], []byte("ab\n"))
}