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 }