github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/cmd/snap/wait.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2016-2017 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package main
    21  
    22  import (
    23  	"errors"
    24  	"fmt"
    25  	"os"
    26  	"os/signal"
    27  	"time"
    28  
    29  	"github.com/snapcore/snapd/client"
    30  	"github.com/snapcore/snapd/i18n"
    31  	"github.com/snapcore/snapd/progress"
    32  )
    33  
    34  var (
    35  	maxGoneTime = 5 * time.Second
    36  	pollTime    = 100 * time.Millisecond
    37  )
    38  
    39  type waitMixin struct {
    40  	clientMixin
    41  	NoWait    bool `long:"no-wait"`
    42  	skipAbort bool
    43  }
    44  
    45  var waitDescs = mixinDescs{
    46  	// TRANSLATORS: This should not start with a lowercase letter.
    47  	"no-wait": i18n.G("Do not wait for the operation to finish but just print the change id."),
    48  }
    49  
    50  var noWait = errors.New("no wait for op")
    51  
    52  func (wmx waitMixin) wait(id string) (*client.Change, error) {
    53  	if wmx.NoWait {
    54  		fmt.Fprintf(Stdout, "%s\n", id)
    55  		return nil, noWait
    56  	}
    57  	cli := wmx.client
    58  	// Intercept sigint
    59  	c := make(chan os.Signal, 2)
    60  	signal.Notify(c, os.Interrupt)
    61  	go func() {
    62  		sig := <-c
    63  		// sig is nil if c was closed
    64  		if sig == nil || wmx.skipAbort {
    65  			return
    66  		}
    67  		_, err := wmx.client.Abort(id)
    68  		if err != nil {
    69  			fmt.Fprintf(Stderr, err.Error()+"\n")
    70  		}
    71  	}()
    72  
    73  	pb := progress.MakeProgressBar()
    74  	defer func() {
    75  		pb.Finished()
    76  		// next two not strictly needed for CLI, but without
    77  		// them the tests will leak goroutines.
    78  		signal.Stop(c)
    79  		close(c)
    80  	}()
    81  
    82  	tMax := time.Time{}
    83  
    84  	var lastID string
    85  	lastLog := map[string]string{}
    86  	for {
    87  		var rebootingErr error
    88  		chg, err := cli.Change(id)
    89  		if err != nil {
    90  			// a client.Error means we were able to communicate with
    91  			// the server (got an answer)
    92  			if e, ok := err.(*client.Error); ok {
    93  				return nil, e
    94  			}
    95  
    96  			// an non-client error here means the server most
    97  			// likely went away
    98  			// XXX: it actually can be a bunch of other things; fix client to expose it better
    99  			now := time.Now()
   100  			if tMax.IsZero() {
   101  				tMax = now.Add(maxGoneTime)
   102  			}
   103  			if now.After(tMax) {
   104  				return nil, err
   105  			}
   106  			pb.Spin(i18n.G("Waiting for server to restart"))
   107  			time.Sleep(pollTime)
   108  			continue
   109  		}
   110  		if maintErr, ok := cli.Maintenance().(*client.Error); ok && maintErr.Kind == client.ErrorKindSystemRestart {
   111  			rebootingErr = maintErr
   112  		}
   113  		if !tMax.IsZero() {
   114  			pb.Finished()
   115  			tMax = time.Time{}
   116  		}
   117  
   118  		for _, t := range chg.Tasks {
   119  			switch {
   120  			case t.Status != "Doing":
   121  				continue
   122  			case t.Progress.Total == 1:
   123  				pb.Spin(t.Summary)
   124  				nowLog := lastLogStr(t.Log)
   125  				if lastLog[t.ID] != nowLog {
   126  					pb.Notify(nowLog)
   127  					lastLog[t.ID] = nowLog
   128  				}
   129  			case t.ID == lastID:
   130  				pb.Set(float64(t.Progress.Done))
   131  			default:
   132  				pb.Start(t.Summary, float64(t.Progress.Total))
   133  				lastID = t.ID
   134  			}
   135  			break
   136  		}
   137  
   138  		if chg.Ready {
   139  			if chg.Status == "Done" {
   140  				return chg, nil
   141  			}
   142  
   143  			if chg.Err != "" {
   144  				return chg, errors.New(chg.Err)
   145  			}
   146  
   147  			return nil, fmt.Errorf(i18n.G("change finished in status %q with no error message"), chg.Status)
   148  		}
   149  
   150  		if rebootingErr != nil {
   151  			return nil, rebootingErr
   152  		}
   153  
   154  		// note this very purposely is not a ticker; we want
   155  		// to sleep 100ms between calls, not call once every
   156  		// 100ms.
   157  		time.Sleep(pollTime)
   158  	}
   159  }
   160  
   161  func lastLogStr(logs []string) string {
   162  	if len(logs) == 0 {
   163  		return ""
   164  	}
   165  	return logs[len(logs)-1]
   166  }