blob: ae181d8f0ba5299164c7a7a377c2f962e131d115 [file] [log] [blame]
// Copyright 2015 The Vanadium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package impl
import (
"bytes"
"errors"
"os/exec"
"runtime"
"strings"
"v.io/v23/services/build"
"v.io/v23/services/device"
"v.io/x/ref/services/profile"
)
// ComputeDeviceProfile generates a description of the runtime
// environment (supported file format, OS, architecture, libraries) of
// the host device.
//
// TODO(jsimsa): Avoid computing the host device description from
// scratch if a recent cached copy exists.
func ComputeDeviceProfile() (*profile.Specification, error) {
result := profile.Specification{}
// Find out what the supported operating system, file format, and
// architecture is.
var os build.OperatingSystem
if err := os.SetFromGoOS(runtime.GOOS); err != nil {
return nil, err
}
result.Os = os
switch os {
case build.OperatingSystemDarwin:
result.Format = build.FormatMach
case build.OperatingSystemLinux:
result.Format = build.FormatElf
case build.OperatingSystemWindows:
result.Format = build.FormatPe
default:
return nil, errors.New("Unsupported operating system: " + os.String())
}
var arch build.Architecture
if err := arch.SetFromGoArch(runtime.GOARCH); err != nil {
return nil, err
}
result.Arch = arch
// Find out what the installed dynamically linked libraries are.
switch runtime.GOOS {
case "linux":
// For Linux, we identify what dynamically linked libraries are
// installed by parsing the output of "ldconfig -p".
command := exec.Command("/sbin/ldconfig", "-p")
output, err := command.CombinedOutput()
if err != nil {
return nil, err
}
buf := bytes.NewBuffer(output)
// Throw away the first line of output from ldconfig.
if _, err := buf.ReadString('\n'); err != nil {
return nil, errors.New("Could not identify libraries.")
}
// Extract the library name and version from every subsequent line.
result.Libraries = make(map[profile.Library]struct{})
line, err := buf.ReadString('\n')
for err == nil {
words := strings.Split(strings.Trim(line, " \t\n"), " ")
if len(words) > 0 {
tokens := strings.Split(words[0], ".so")
if len(tokens) != 2 {
return nil, errors.New("Could not identify library: " + words[0])
}
name := strings.TrimPrefix(tokens[0], "lib")
major, minor := "", ""
tokens = strings.SplitN(tokens[1], ".", 3)
if len(tokens) >= 2 {
major = tokens[1]
}
if len(tokens) >= 3 {
minor = tokens[2]
}
result.Libraries[profile.Library{Name: name, MajorVersion: major, MinorVersion: minor}] = struct{}{}
}
line, err = buf.ReadString('\n')
}
case "darwin":
// TODO(jsimsa): Implement.
case "windows":
// TODO(jsimsa): Implement.
default:
return nil, errors.New("Unsupported operating system: " + runtime.GOOS)
}
return &result, nil
}
// getProfile gets a profile description for the given profile.
//
// TODO(jsimsa): Avoid retrieving the list of known profiles from a
// remote server if a recent cached copy exists.
func getProfile(name string) (*profile.Specification, error) {
profiles, err := getKnownProfiles()
if err != nil {
return nil, err
}
for _, p := range profiles {
if p.Label == name {
return p, nil
}
}
return nil, nil
// TODO(jsimsa): This function assumes the existence of a profile
// server from which the profiles can be retrieved. The profile
// server is a work in progress. When it exists, the commented out
// code below should work.
/*
var profile profile.Specification
client, err := r.NewClient()
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v", err))
}
defer client.Close()
server := // TODO
method := "Specification"
inputs := make([]interface{}, 0)
call, err := client.StartCall(server + "/" + name, method, inputs)
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server + "/" + name, method, inputs, err))
}
if err := call.Finish(&profiles); err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(%v) failed: %v\n", &profiles, err))
}
return &profile, nil
*/
}
// getKnownProfiles gets a list of description for all publicly known
// profiles.
//
// TODO(jsimsa): Avoid retrieving the list of known profiles from a
// remote server if a recent cached copy exists.
func getKnownProfiles() ([]*profile.Specification, error) {
return []*profile.Specification{
{
Label: "linux-amd64",
Description: "",
Arch: build.ArchitectureAmd64,
Os: build.OperatingSystemLinux,
Format: build.FormatElf,
},
{
// Note that linux-386 is used instead of linux-x86 for the
// label to facilitate generation of a matching label string
// using the runtime.GOARCH value. In VDL, the 386 architecture
// is represented using the value X86 because the VDL grammar
// does not allow identifiers starting with a number.
Label: "linux-386",
Description: "",
Arch: build.ArchitectureX86,
Os: build.OperatingSystemLinux,
Format: build.FormatElf,
},
{
Label: "linux-arm",
Description: "",
Arch: build.ArchitectureArm,
Os: build.OperatingSystemLinux,
Format: build.FormatElf,
},
// TODO(caprita): Add other profiles for Mac, Pi, etc.
}, nil
// TODO(jsimsa): This function assumes the existence of a profile
// server from which a list of known profiles can be retrieved. The
// profile server is a work in progress. When it exists, the
// commented out code below should work.
/*
knownProfiles := make([]profile.Specification, 0)
client, err := r.NewClient()
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("NewClient() failed: %v\n", err))
}
defer client.Close()
server := // TODO
method := "List"
inputs := make([]interface{}, 0)
call, err := client.StartCall(server, method, inputs)
if err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("StartCall(%s, %q, %v) failed: %v\n", server, method, inputs, err))
}
if err := call.Finish(&knownProfiles); err != nil {
return nil, verror.New(ErrOperationFailed, nil, fmt.Sprintf("Finish(&knownProfile) failed: %v\n", err))
}
return knownProfiles, nil
*/
}
// matchProfiles inputs a profile that describes the host device and a
// set of publicly known profiles and outputs a device description that
// identifies the publicly known profiles supported by the host device.
func matchProfiles(p *profile.Specification, known []*profile.Specification) device.Description {
result := device.Description{Profiles: make(map[string]struct{})}
loop:
for _, profile := range known {
if profile.Format != p.Format {
continue
}
if profile.Os != p.Os {
continue
}
if profile.Arch != p.Arch {
continue
}
for library := range profile.Libraries {
// Current implementation requires exact library name and version match.
if _, found := p.Libraries[library]; !found {
continue loop
}
}
result.Profiles[profile.Label] = struct{}{}
}
return result
}
// Describe returns a Description containing the profile that matches the
// current device. It's declared as a variable so we can override it for
// testing.
var Describe = func() (device.Description, error) {
empty := device.Description{}
deviceProfile, err := ComputeDeviceProfile()
if err != nil {
return empty, err
}
knownProfiles, err := getKnownProfiles()
if err != nil {
return empty, err
}
result := matchProfiles(deviceProfile, knownProfiles)
if len(result.Profiles) == 0 {
// For now, return "unknown" as the profile, if no known profile
// matches the device's profile.
//
// TODO(caprita): Get rid of this crutch once we have profiles
// defined for our supported systems; for now it helps us make
// the integration test work on e.g. Mac.
result.Profiles["unknown"] = struct{}{}
}
return result, nil
}