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 }