|  | // Copyright 2019 The Fuchsia Authors. All rights reserved. | 
|  | // Use of this source code is governed by a BSD-style license that can be | 
|  | // found in the LICENSE file. | 
|  |  | 
|  | package syslog | 
|  |  | 
|  | import ( | 
|  | "context" | 
|  | "io" | 
|  | "time" | 
|  |  | 
|  | "go.fuchsia.dev/fuchsia/tools/lib/logger" | 
|  | "go.fuchsia.dev/fuchsia/tools/lib/retry" | 
|  | "go.fuchsia.dev/fuchsia/tools/net/sshutil" | 
|  | ) | 
|  |  | 
|  | const ( | 
|  | // The program on fuchsia used to stream system logs through a shell, not to | 
|  | // be confused with the zircon host tool "loglistener" (no underscore) used | 
|  | // to stream zircon-level logs to host. | 
|  | logListener = "/bin/log_listener" | 
|  |  | 
|  | // Time to wait between attempts to reconnect after losing the connection. | 
|  | defaultReconnectInterval = 5 * time.Second | 
|  | ) | 
|  |  | 
|  | // Syslogger streams systems logs from a Fuchsia instance. | 
|  | type Syslogger struct { | 
|  | client sshClient | 
|  | } | 
|  |  | 
|  | type sshClient interface { | 
|  | Run(context.Context, []string, io.Writer, io.Writer) error | 
|  | ReconnectWithBackoff(context.Context, retry.Backoff) error | 
|  | } | 
|  |  | 
|  | // NewSyslogger creates a new Syslogger, given an SSH session with a Fuchsia instance. | 
|  | func NewSyslogger(client *sshutil.Client) *Syslogger { | 
|  | return &Syslogger{ | 
|  | client: client, | 
|  | } | 
|  | } | 
|  |  | 
|  | // Stream writes system logs to a given writer, starting from the very beginning | 
|  | // of the system's uptime. It blocks until the context is canceled or until an | 
|  | // unexpected (not SSH-related) error occurs. | 
|  | func (s *Syslogger) Stream(ctx context.Context, output io.Writer) error { | 
|  | cmd := []string{logListener} | 
|  | for { | 
|  | // Note: Fuchsia's log_listener does not write to stderr. | 
|  | err := s.client.Run(ctx, cmd, output, nil) | 
|  | if ctx.Err() == nil { | 
|  | if err != nil { | 
|  | logger.Debugf(ctx, "error streaming syslog: %v", err) | 
|  | } else { | 
|  | logger.Debugf(ctx, "log_listener exited successfully, will rerun") | 
|  | // Don't stream from the beginning of the system's uptime, since | 
|  | // that would include logs that we've already streamed. | 
|  | cmd = []string{logListener, "--since_now", "yes"} | 
|  | continue | 
|  | } | 
|  | } | 
|  |  | 
|  | // We need not attempt to reconnect if we hit an error unrelated to the | 
|  | // connection (which is probably unrecoverable) or if the context was | 
|  | // canceled (context cancellation is the only mechanism for stopping | 
|  | // this method, so it generally indicates that the caller is exiting | 
|  | // normally). | 
|  | if ctx.Err() != nil || !sshutil.IsConnectionError(err) { | 
|  | logger.Debugf(ctx, "syslog streaming complete") | 
|  | return err | 
|  | } | 
|  |  | 
|  | logger.Errorf(ctx, "syslog: SSH client unresponsive; will attempt to reconnect and continue streaming: %v", err) | 
|  | if err := s.client.ReconnectWithBackoff(ctx, retry.NewConstantBackoff(defaultReconnectInterval)); err != nil { | 
|  | // The context probably got cancelled before we were able to | 
|  | // reconnect. | 
|  | return err | 
|  | } | 
|  | // Start streaming from the beginning of the system's uptime again now that | 
|  | // we're rebooting. | 
|  | cmd = []string{logListener} | 
|  | logger.Infof(ctx, "syslog: refreshed ssh connection") | 
|  | io.WriteString(output, "\n\n<< SYSLOG STREAM INTERRUPTED; RECONNECTING NOW >>\n\n") | 
|  | } | 
|  | } |