github.com/Lephar/snapd@v0.0.0-20210825215435-c7fba9cef4d2/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  // String will format the log entry with the timestamp in the local timezone
   112  func (l Log) String() string {
   113  	return l.fmtLog(time.Local)
   114  }
   115  
   116  // StringInUTC will format the log entry with the timestamp in UTC
   117  func (l Log) StringInUTC() string {
   118  	return l.fmtLog(time.UTC)
   119  }
   120  
   121  func (l Log) fmtLog(timezone *time.Location) string {
   122  	if timezone == nil {
   123  		timezone = time.Local
   124  	}
   125  
   126  	return fmt.Sprintf("%s %s[%s]: %s", l.Timestamp.In(timezone).Format(time.RFC3339), l.SID, l.PID, l.Message)
   127  }
   128  
   129  // Logs asks for the logs of a series of services, by name.
   130  func (client *Client) Logs(names []string, opts LogOptions) (<-chan Log, error) {
   131  	query := url.Values{}
   132  	if len(names) > 0 {
   133  		query.Set("names", strings.Join(names, ","))
   134  	}
   135  	query.Set("n", strconv.Itoa(opts.N))
   136  	if opts.Follow {
   137  		query.Set("follow", strconv.FormatBool(opts.Follow))
   138  	}
   139  
   140  	rsp, err := client.raw(context.Background(), "GET", "/v2/logs", query, nil, nil)
   141  	if err != nil {
   142  		return nil, err
   143  	}
   144  
   145  	if rsp.StatusCode != 200 {
   146  		var r response
   147  		defer rsp.Body.Close()
   148  		if err := decodeInto(rsp.Body, &r); err != nil {
   149  			return nil, err
   150  		}
   151  		return nil, r.err(client, rsp.StatusCode)
   152  	}
   153  
   154  	ch := make(chan Log, 20)
   155  	go func() {
   156  		// logs come in application/json-seq, described in RFC7464: it's
   157  		// a series of <RS><arbitrary, valid JSON><LF>. Decoders are
   158  		// expected to skip invalid or truncated or empty records.
   159  		scanner := bufio.NewScanner(rsp.Body)
   160  		for scanner.Scan() {
   161  			buf := scanner.Bytes() // the scanner prunes the ending LF
   162  			if len(buf) < 1 {
   163  				// truncated record? skip
   164  				continue
   165  			}
   166  			idx := bytes.IndexByte(buf, 0x1E) // find the initial RS
   167  			if idx < 0 {
   168  				// no RS? skip
   169  				continue
   170  			}
   171  			buf = buf[idx+1:] // drop the initial RS
   172  			var log Log
   173  			if err := json.Unmarshal(buf, &log); err != nil {
   174  				// truncated/corrupted/binary record? skip
   175  				continue
   176  			}
   177  			ch <- log
   178  		}
   179  		close(ch)
   180  		rsp.Body.Close()
   181  	}()
   182  
   183  	return ch, nil
   184  }
   185  
   186  // ErrNoNames is returned by Start, Stop, or Restart, when the given
   187  // list of things on which to operate is empty.
   188  var ErrNoNames = errors.New(`"names" must not be empty`)
   189  
   190  type appInstruction struct {
   191  	Action string   `json:"action"`
   192  	Names  []string `json:"names"`
   193  	StartOptions
   194  	StopOptions
   195  	RestartOptions
   196  }
   197  
   198  // StartOptions represent the different options of the Start call.
   199  type StartOptions struct {
   200  	// Enable, as well as starting, the listed services. A
   201  	// disabled service does not start on boot.
   202  	Enable bool `json:"enable,omitempty"`
   203  }
   204  
   205  // Start services.
   206  //
   207  // It takes a list of names that can be snaps, of which all their
   208  // services are started, or snap.service which are individual
   209  // services to start; it shouldn't be empty.
   210  func (client *Client) Start(names []string, opts StartOptions) (changeID string, err error) {
   211  	if len(names) == 0 {
   212  		return "", ErrNoNames
   213  	}
   214  
   215  	buf, err := json.Marshal(appInstruction{
   216  		Action:       "start",
   217  		Names:        names,
   218  		StartOptions: opts,
   219  	})
   220  	if err != nil {
   221  		return "", err
   222  	}
   223  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   224  }
   225  
   226  // StopOptions represent the different options of the Stop call.
   227  type StopOptions struct {
   228  	// Disable, as well as stopping, the listed services. A
   229  	// service that is not disabled starts on boot.
   230  	Disable bool `json:"disable,omitempty"`
   231  }
   232  
   233  // Stop services.
   234  //
   235  // It takes a list of names that can be snaps, of which all their
   236  // services are stopped, or snap.service which are individual
   237  // services to stop; it shouldn't be empty.
   238  func (client *Client) Stop(names []string, opts StopOptions) (changeID string, err error) {
   239  	if len(names) == 0 {
   240  		return "", ErrNoNames
   241  	}
   242  
   243  	buf, err := json.Marshal(appInstruction{
   244  		Action:      "stop",
   245  		Names:       names,
   246  		StopOptions: opts,
   247  	})
   248  	if err != nil {
   249  		return "", err
   250  	}
   251  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   252  }
   253  
   254  // RestartOptions represent the different options of the Restart call.
   255  type RestartOptions struct {
   256  	// Reload the services, if possible (i.e. if the App has a
   257  	// ReloadCommand, invoque it), instead of restarting.
   258  	Reload bool `json:"reload,omitempty"`
   259  }
   260  
   261  // Restart services.
   262  //
   263  // It takes a list of names that can be snaps, of which all their
   264  // services are restarted, or snap.service which are individual
   265  // services to restart; it shouldn't be empty. If the service is not
   266  // running, starts it.
   267  func (client *Client) Restart(names []string, opts RestartOptions) (changeID string, err error) {
   268  	if len(names) == 0 {
   269  		return "", ErrNoNames
   270  	}
   271  
   272  	buf, err := json.Marshal(appInstruction{
   273  		Action:         "restart",
   274  		Names:          names,
   275  		RestartOptions: opts,
   276  	})
   277  	if err != nil {
   278  		return "", err
   279  	}
   280  	return client.doAsync("POST", "/v2/apps", nil, nil, bytes.NewReader(buf))
   281  }