github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/usersession/client/client.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2015-2020 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  	"bytes"
    24  	"context"
    25  	"encoding/json"
    26  	"fmt"
    27  	"io"
    28  	"io/ioutil"
    29  	"net"
    30  	"net/http"
    31  	"net/url"
    32  	"path/filepath"
    33  	"strconv"
    34  	"sync"
    35  	"time"
    36  
    37  	"github.com/snapcore/snapd/dirs"
    38  )
    39  
    40  // dialSessionAgent connects to a user's session agent
    41  //
    42  // The host portion of the address is interpreted as the numeric user
    43  // ID of the target user.
    44  func dialSessionAgent(network, address string) (net.Conn, error) {
    45  	host, _, err := net.SplitHostPort(address)
    46  	if err != nil {
    47  		return nil, err
    48  	}
    49  	socket := filepath.Join(dirs.XdgRuntimeDirBase, host, "snapd-session-agent.socket")
    50  	return net.Dial("unix", socket)
    51  }
    52  
    53  type Client struct {
    54  	doer *http.Client
    55  }
    56  
    57  func New() *Client {
    58  	transport := &http.Transport{Dial: dialSessionAgent, DisableKeepAlives: true}
    59  	return &Client{
    60  		doer: &http.Client{Transport: transport},
    61  	}
    62  }
    63  
    64  type Error struct {
    65  	Kind    string      `json:"kind"`
    66  	Value   interface{} `json:"value"`
    67  	Message string      `json:"message"`
    68  }
    69  
    70  func (e *Error) Error() string {
    71  	return e.Message
    72  }
    73  
    74  type response struct {
    75  	// Not from JSON
    76  	uid        int
    77  	err        error
    78  	statusCode int
    79  
    80  	Result json.RawMessage `json:"result"`
    81  	Type   string          `json:"type"`
    82  }
    83  
    84  func (resp *response) checkError() {
    85  	if resp.Type != "error" {
    86  		return
    87  	}
    88  	var resultErr Error
    89  	err := json.Unmarshal(resp.Result, &resultErr)
    90  	if err != nil || resultErr.Message == "" {
    91  		resp.err = fmt.Errorf("server error: %q", http.StatusText(resp.statusCode))
    92  	} else {
    93  		resp.err = &resultErr
    94  	}
    95  }
    96  
    97  func (client *Client) doMany(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body []byte) ([]*response, error) {
    98  	sockets, err := filepath.Glob(filepath.Join(dirs.XdgRuntimeDirGlob, "snapd-session-agent.socket"))
    99  	if err != nil {
   100  		return nil, err
   101  	}
   102  	var (
   103  		wg        sync.WaitGroup
   104  		mu        sync.Mutex
   105  		responses []*response
   106  	)
   107  	for _, socket := range sockets {
   108  		wg.Add(1)
   109  		go func(socket string) {
   110  			defer wg.Done()
   111  			uidStr := filepath.Base(filepath.Dir(socket))
   112  			uid, err := strconv.Atoi(uidStr)
   113  			if err != nil {
   114  				// Ignore directories that do not
   115  				// appear to be valid XDG runtime dirs
   116  				// (i.e. /run/user/NNNN).
   117  				return
   118  			}
   119  			response := response{uid: uid}
   120  			defer func() {
   121  				mu.Lock()
   122  				defer mu.Unlock()
   123  				responses = append(responses, &response)
   124  			}()
   125  
   126  			u := url.URL{
   127  				Scheme:   "http",
   128  				Host:     uidStr,
   129  				Path:     urlpath,
   130  				RawQuery: query.Encode(),
   131  			}
   132  			req, err := http.NewRequest(method, u.String(), bytes.NewBuffer(body))
   133  			if err != nil {
   134  				response.err = fmt.Errorf("internal error: %v", err)
   135  				return
   136  			}
   137  			req = req.WithContext(ctx)
   138  			for key, value := range headers {
   139  				req.Header.Set(key, value)
   140  			}
   141  			httpResp, err := client.doer.Do(req)
   142  			if err != nil {
   143  				response.err = err
   144  				return
   145  			}
   146  			defer httpResp.Body.Close()
   147  			response.statusCode = httpResp.StatusCode
   148  			response.err = decodeInto(httpResp.Body, &response)
   149  			response.checkError()
   150  		}(socket)
   151  	}
   152  	wg.Wait()
   153  	return responses, nil
   154  }
   155  
   156  func decodeInto(reader io.Reader, v interface{}) error {
   157  	dec := json.NewDecoder(reader)
   158  	if err := dec.Decode(v); err != nil {
   159  		r := dec.Buffered()
   160  		buf, err1 := ioutil.ReadAll(r)
   161  		if err1 != nil {
   162  			buf = []byte(fmt.Sprintf("error reading buffered response body: %s", err1))
   163  		}
   164  		return fmt.Errorf("cannot decode %q: %s", buf, err)
   165  	}
   166  	return nil
   167  }
   168  
   169  type SessionInfo struct {
   170  	Version string `json:"version"`
   171  }
   172  
   173  func (client *Client) SessionInfo(ctx context.Context) (info map[int]SessionInfo, err error) {
   174  	responses, err := client.doMany(ctx, "GET", "/v1/session-info", nil, nil, nil)
   175  	if err != nil {
   176  		return nil, err
   177  	}
   178  
   179  	info = make(map[int]SessionInfo)
   180  	for _, resp := range responses {
   181  		if resp.err != nil {
   182  			if err == nil {
   183  				err = resp.err
   184  			}
   185  			continue
   186  		}
   187  		var si SessionInfo
   188  		if decodeErr := json.Unmarshal(resp.Result, &si); decodeErr != nil {
   189  			if err == nil {
   190  				err = decodeErr
   191  			}
   192  			continue
   193  		}
   194  		info[resp.uid] = si
   195  	}
   196  	return info, err
   197  }
   198  
   199  type ServiceFailure struct {
   200  	Uid     int
   201  	Service string
   202  	Error   string
   203  }
   204  
   205  func decodeServiceErrors(uid int, errorValue map[string]interface{}, kind string) ([]ServiceFailure, error) {
   206  	if errorValue[kind] == nil {
   207  		return nil, nil
   208  	}
   209  	errors, ok := errorValue[kind].(map[string]interface{})
   210  	if !ok {
   211  		return nil, fmt.Errorf("cannot decode %s failures: expected a map, got %T", kind, errorValue[kind])
   212  	}
   213  	var failures []ServiceFailure
   214  	var err error
   215  	for service, reason := range errors {
   216  		if reasonString, ok := reason.(string); ok {
   217  			failures = append(failures, ServiceFailure{
   218  				Uid:     uid,
   219  				Service: service,
   220  				Error:   reasonString,
   221  			})
   222  		} else if err == nil {
   223  			err = fmt.Errorf("cannot decode %s failure for %q: expected string, but got %T", kind, service, reason)
   224  		}
   225  	}
   226  	return failures, err
   227  }
   228  
   229  func (client *Client) serviceControlCall(ctx context.Context, action string, services []string) (startFailures, stopFailures []ServiceFailure, err error) {
   230  	headers := map[string]string{"Content-Type": "application/json"}
   231  	reqBody, err := json.Marshal(map[string]interface{}{
   232  		"action":   action,
   233  		"services": services,
   234  	})
   235  	if err != nil {
   236  		return nil, nil, err
   237  	}
   238  	responses, err := client.doMany(ctx, "POST", "/v1/service-control", nil, headers, reqBody)
   239  	if err != nil {
   240  		return nil, nil, err
   241  	}
   242  	for _, resp := range responses {
   243  		if agentErr, ok := resp.err.(*Error); ok && agentErr.Kind == "service-control" {
   244  			if errorValue, ok := agentErr.Value.(map[string]interface{}); ok {
   245  				failures, _ := decodeServiceErrors(resp.uid, errorValue, "start-errors")
   246  				startFailures = append(startFailures, failures...)
   247  				failures, _ = decodeServiceErrors(resp.uid, errorValue, "stop-errors")
   248  				stopFailures = append(stopFailures, failures...)
   249  			}
   250  		}
   251  		if resp.err != nil && err == nil {
   252  			err = resp.err
   253  		}
   254  	}
   255  	return startFailures, stopFailures, err
   256  }
   257  
   258  func (client *Client) ServicesDaemonReload(ctx context.Context) error {
   259  	_, _, err := client.serviceControlCall(ctx, "daemon-reload", nil)
   260  	return err
   261  }
   262  
   263  func (client *Client) ServicesStart(ctx context.Context, services []string) (startFailures, stopFailures []ServiceFailure, err error) {
   264  	return client.serviceControlCall(ctx, "start", services)
   265  }
   266  
   267  func (client *Client) ServicesStop(ctx context.Context, services []string) (stopFailures []ServiceFailure, err error) {
   268  	_, stopFailures, err = client.serviceControlCall(ctx, "stop", services)
   269  	return stopFailures, err
   270  }
   271  
   272  // PendingSnapRefreshInfo holds information about pending snap refresh provided to userd.
   273  type PendingSnapRefreshInfo struct {
   274  	InstanceName        string        `json:"instance-name"`
   275  	TimeRemaining       time.Duration `json:"time-remaining,omitempty"`
   276  	BusyAppName         string        `json:"busy-app-name,omitempty"`
   277  	BusyAppDesktopEntry string        `json:"busy-app-desktop-entry,omitempty"`
   278  }
   279  
   280  // PendingRefreshNotification broadcasts information about a refresh.
   281  func (client *Client) PendingRefreshNotification(ctx context.Context, refreshInfo *PendingSnapRefreshInfo) error {
   282  	headers := map[string]string{"Content-Type": "application/json"}
   283  	reqBody, err := json.Marshal(refreshInfo)
   284  	if err != nil {
   285  		return err
   286  	}
   287  	_, err = client.doMany(ctx, "POST", "/v1/notifications/pending-refresh", nil, headers, reqBody)
   288  	return err
   289  }