github.com/kubiko/snapd@v0.0.0-20201013125620-d4f3094d9ddf/client/apps.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2016 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 client
    21  
    22  import (
    23  	"bufio"
    24  	"bytes"
    25  	"context"
    26  	"encoding/json"
    27  	"errors"
    28  	"fmt"
    29  	"net/url"
    30  	"strconv"
    31  	"strings"
    32  	"time"
    33  )
    34  
    35  // AppActivator is a thing that activates the app that is a service in the
    36  // system.
    37  type AppActivator struct {
    38  	Name string
    39  	// Type describes the type of the unit, either timer or socket
    40  	Type    string
    41  	Active  bool
    42  	Enabled bool
    43  }
    44  
    45  // AppInfo describes a single snap application.
    46  type AppInfo struct {
    47  	Snap        string         `json:"snap,omitempty"`
    48  	Name        string         `json:"name"`
    49  	DesktopFile string         `json:"desktop-file,omitempty"`
    50  	Daemon      string         `json:"daemon,omitempty"`
    51  	Enabled     bool           `json:"enabled,omitempty"`
    52  	Active      bool           `json:"active,omitempty"`
    53  	CommonID    string         `json:"common-id,omitempty"`
    54  	Activators  []AppActivator `json:"activators,omitempty"`
    55  }
    56  
    57  // IsService returns true if the application is a background daemon.
    58  func (a *AppInfo) IsService() bool {
    59  	if a == nil {
    60  		return false
    61  	}
    62  	if a.Daemon == "" {
    63  		return false
    64  	}
    65  
    66  	return true
    67  }
    68  
    69  // AppOptions represent the options of the Apps call.
    70  type AppOptions struct {
    71  	// If Service is true, only return apps that are services
    72  	// (app.IsService() is true); otherwise, return all.
    73  	Service bool
    74  }
    75  
    76  // Apps returns information about all matching apps. Each name can be
    77  // either a snap or a snap.app. If names is empty, list all (that
    78  // satisfy opts).
    79  func (client *Client) Apps(names []string, opts AppOptions) ([]*AppInfo, error) {
    80  	q := make(url.Values)
    81  	if len(names) > 0 {
    82  		q.Add("names", strings.Join(names, ","))
    83  	}
    84  	if opts.Service {
    85  		q.Add("select", "service")
    86  	}
    87  
    88  	var appInfos []*AppInfo
    89  	_, err := client.doSync("GET", "/v2/apps", q, nil, nil, &appInfos)
    90  
    91  	return appInfos, err
    92  }
    93  
    94  // LogOptions represent the options of the Logs call.
    95  type LogOptions struct {
    96  	N      int  // The maximum number of log lines to retrieve initially. If <0, no limit.
    97  	Follow bool // Whether to continue returning new lines as they appear
    98  }
    99  
   100  // A Log holds the information of a single syslog entry
   101  type Log struct {
   102  	Timestamp time.Time `json:"timestamp"` // Timestamp of the event, in RFC3339 format to µs precision.
   103  	Message   string    `json:"message"`   // The log message itself
   104  	SID       string    `json:"sid"`       // The syslog identifier
   105  	PID       string    `json:"pid"`       // The process identifier
   106  }
   107  
   108  func (l Log) String() string {
   109  	return fmt.Sprintf("%s %s[%s]: %s", l.Timestamp.Format(time.RFC3339), l.SID, l.PID, l.Message)
   110  }
   111  
   112  // Logs asks for the logs of a series of services, by name.
   113  func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error) {
   114  	query := url.Values{}
   115  	if len(names) > 0 {
   116  		query.Set("names", strings.Join(names, ","))
   117  	}
   118  	query.Set("n", strconv.Itoa(opts.N))
   119  	if opts.Follow {
   120  		query.Set("follow", strconv.FormatBool(opts.Follow))
   121  	}
   122  
   123  	rsp, err := client.raw(context.Background(), "GET", "/v2/logs", query, nil, nil)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  
   128  	if rsp.StatusCode != 200 {
   129  		var r response
   130  		defer rsp.Body.Close()
   131  		if err := decodeInto(rsp.Body, &r); err != nil {
   132  			return nil, err
   133  		}
   134  		return nil, r.err(client, rsp.StatusCode)
   135  	}
   136  
   137  	ch := make(chan Log, 20)
   138  	go func() {
   139  		// logs come in application/json-seq, described in RFC7464: it's
   140  		// a series of <RS><arbitrary, valid JSON><LF>. Decoders are
   141  		// expected to skip invalid or truncated or empty records.
   142  		scanner := bufio.NewScanner(rsp.Body)
   143  		for scanner.Scan() {
   144  			buf := scanner.Bytes() // the scanner prunes the ending LF
   145  			if len(buf) < 1 {
   146  				// truncated record? skip
   147  				continue
   148  			}
   149  			idx := bytes.IndexByte(buf, 0x1E) // find the initial RS
   150  			if idx < 0 {
   151  				// no RS? skip
   152  				continue
   153  			}
   154  			buf = buf[idx+1:] // drop the initial RS
   155  			var log Log
   156  			if err := json.Unmarshal(buf, &log); err != nil {
   157  				// truncated/corrupted/binary record? skip
   158  				continue
   159  			}
   160  			ch <- log
   161  		}
   162  		close(ch)
   163  		rsp.Body.Close()
   164  	}()
   165  
   166  	return ch, nil
   167  }
   168  
   169  // ErrNoNames is returned by Start, Stop, or Restart, when the given
   170  // list of things on which to operate is empty.
   171  var ErrNoNames = errors.New(`"names" must not be empty`)
   172  
   173  type appInstruction struct {
   174  	Action string   `json:"action"`
   175  	Names  []string `json:"names"`
   176  	StartOptions
   177  	StopOptions
   178  	RestartOptions
   179  }
   180  
   181  // StartOptions represent the different options of the Start call.
   182  type StartOptions struct {
   183  	// Enable, as well as starting, the listed services. A
   184  	// disabled service does not start on boot.
   185  	Enable bool `json:"enable,omitempty"`
   186  }
   187  
   188  // Start services.
   189  //
   190  // It takes a list of names that can be snaps, of which all their
   191  // services are started, or snap.service which are individual
   192  // services to start; it shouldn't be empty.
   193  func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) {
   194  	if len(names) == 0 {
   195  		return "", ErrNoNames
   196  	}
   197  
   198  	buf, err := json.Marshal(appInstruction{
   199  		Action:       "start",
   200  		Names:        names,
   201  		StartOptions: opts,
   202  	})
   203  	if err != nil {
   204  		return "", err
   205  	}
   206  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   207  }
   208  
   209  // StopOptions represent the different options of the Stop call.
   210  type StopOptions struct {
   211  	// Disable, as well as stopping, the listed services. A
   212  	// service that is not disabled starts on boot.
   213  	Disable bool `json:"disable,omitempty"`
   214  }
   215  
   216  // Stop services.
   217  //
   218  // It takes a list of names that can be snaps, of which all their
   219  // services are stopped, or snap.service which are individual
   220  // services to stop; it shouldn't be empty.
   221  func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) {
   222  	if len(names) == 0 {
   223  		return "", ErrNoNames
   224  	}
   225  
   226  	buf, err := json.Marshal(appInstruction{
   227  		Action:      "stop",
   228  		Names:       names,
   229  		StopOptions: opts,
   230  	})
   231  	if err != nil {
   232  		return "", err
   233  	}
   234  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   235  }
   236  
   237  // RestartOptions represent the different options of the Restart call.
   238  type RestartOptions struct {
   239  	// Reload the services, if possible (i.e. if the App has a
   240  	// ReloadCommand, invoque it), instead of restarting.
   241  	Reload bool `json:"reload,omitempty"`
   242  }
   243  
   244  // Restart services.
   245  //
   246  // It takes a list of names that can be snaps, of which all their
   247  // services are restarted, or snap.service which are individual
   248  // services to restart; it shouldn't be empty. If the service is not
   249  // running, starts it.
   250  func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) {
   251  	if len(names) == 0 {
   252  		return "", ErrNoNames
   253  	}
   254  
   255  	buf, err := json.Marshal(appInstruction{
   256  		Action:         "restart",
   257  		Names:          names,
   258  		RestartOptions: opts,
   259  	})
   260  	if err != nil {
   261  		return "", err
   262  	}
   263  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   264  }