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 }