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