mirror of
https://github.com/priyanshujain/sanderling.git
synced 2026-10-02 19:17:10 +00:00
feat(ioscompanion): wire gRPC companion client and Dial
This commit is contained in:
1 parent
9d52b20d89
commit
c8c405b6b3
1 file changed
+209
@@ -0,0 +1,209 @@
|
||||
package transport
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials/insecure"
|
||||
|
||||
pb "github.com/priyanshujain/sanderling/internal/driver/ioscompanion/companionpb"
|
||||
)
|
||||
|
||||
// maxReceiveBytes is generous because screenshots can be large.
|
||||
const maxReceiveBytes = 64 * 1024 * 1024
|
||||
|
||||
type grpcCompanion struct {
|
||||
conn *grpc.ClientConn
|
||||
client pb.CompanionServiceClient
|
||||
}
|
||||
|
||||
// Dial connects to the companion listening at address and returns a Companion.
|
||||
func Dial(address string) (Companion, error) {
|
||||
conn, err := grpc.NewClient(address,
|
||||
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||
grpc.WithDefaultCallOptions(grpc.MaxCallRecvMsgSize(maxReceiveBytes)))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &grpcCompanion{conn: conn, client: pb.NewCompanionServiceClient(conn)}, nil
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Close() error { return c.conn.Close() }
|
||||
|
||||
func (c *grpcCompanion) AccessibilityInfo(ctx context.Context) (string, error) {
|
||||
resp, err := c.client.AccessibilityInfo(ctx, &pb.AccessibilityInfoRequest{
|
||||
Format: pb.AccessibilityInfoRequest_LEGACY,
|
||||
})
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return resp.GetJson(), nil
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) SendHID(ctx context.Context, events ...HIDEvent) error {
|
||||
stream, err := c.client.Hid(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, e := range events {
|
||||
if err := stream.Send(e.event); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
_, err = stream.CloseAndRecv()
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Screenshot(ctx context.Context) ([]byte, string, error) {
|
||||
resp, err := c.client.Screenshot(ctx, &pb.ScreenshotRequest{})
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
}
|
||||
return resp.GetImageData(), resp.GetImageFormat(), nil
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Launch(ctx context.Context, bundleID string, foregroundIfRunning bool) error {
|
||||
stream, err := c.client.Launch(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = stream.Send(&pb.LaunchRequest{Control: &pb.LaunchRequest_Start_{Start: &pb.LaunchRequest_Start{
|
||||
BundleId: bundleID,
|
||||
ForegroundIfRunning: foregroundIfRunning,
|
||||
}}})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if _, err := stream.Recv(); err != nil && err != io.EOF {
|
||||
return err
|
||||
}
|
||||
return stream.CloseSend()
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Terminate(ctx context.Context, bundleID string) error {
|
||||
_, err := c.client.Terminate(ctx, &pb.TerminateRequest{BundleId: bundleID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Uninstall(ctx context.Context, bundleID string) error {
|
||||
_, err := c.client.Uninstall(ctx, &pb.UninstallRequest{BundleId: bundleID})
|
||||
return err
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) ListApps(ctx context.Context) ([]InstalledApp, error) {
|
||||
resp, err := c.client.ListApps(ctx, &pb.ListAppsRequest{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
apps := make([]InstalledApp, 0, len(resp.GetApps()))
|
||||
for _, a := range resp.GetApps() {
|
||||
apps = append(apps, InstalledApp{
|
||||
BundleID: a.GetBundleId(),
|
||||
Name: a.GetName(),
|
||||
InstallType: a.GetInstallType(),
|
||||
ProcessState: processStateFromProto(a.GetProcessState()),
|
||||
Debuggable: a.GetDebuggable(),
|
||||
ProcessIdentifier: a.GetProcessIdentifier(),
|
||||
})
|
||||
}
|
||||
return apps, nil
|
||||
}
|
||||
|
||||
func (c *grpcCompanion) Install(ctx context.Context, appPath string) error {
|
||||
info, err := os.Stat(appPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return fmt.Errorf("install: %s is not an app bundle directory", appPath)
|
||||
}
|
||||
|
||||
stream, err := c.client.Install(ctx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// First message sets the destination, then the payload carries the bundle
|
||||
// as a gzip-compressed tar archive the companion unpacks.
|
||||
if err := stream.Send(&pb.InstallRequest{Value: &pb.InstallRequest_Destination_{
|
||||
Destination: pb.InstallRequest_APP,
|
||||
}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
archive, err := tarGzipDirectory(appPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := stream.Send(&pb.InstallRequest{Value: &pb.InstallRequest_Payload{
|
||||
Payload: &pb.Payload{Source: &pb.Payload_Data{Data: archive}},
|
||||
}}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := stream.CloseSend(); err != nil {
|
||||
return err
|
||||
}
|
||||
for {
|
||||
if _, err := stream.Recv(); err != nil {
|
||||
if err == io.EOF {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// tarGzipDirectory packs dir into a gzip-compressed tar archive. Entry paths are
|
||||
// relative to the parent of dir so the bundle directory itself is preserved.
|
||||
func tarGzipDirectory(dir string) ([]byte, error) {
|
||||
var buffer bytes.Buffer
|
||||
gzipWriter := gzip.NewWriter(&buffer)
|
||||
tarWriter := tar.NewWriter(gzipWriter)
|
||||
|
||||
base := filepath.Dir(dir)
|
||||
walkErr := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
relative, err := filepath.Rel(base, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header, err := tar.FileInfoHeader(info, "")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
header.Name = filepath.ToSlash(relative)
|
||||
if err := tarWriter.WriteHeader(header); err != nil {
|
||||
return err
|
||||
}
|
||||
if info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
_, err = io.Copy(tarWriter, file)
|
||||
return err
|
||||
})
|
||||
if walkErr != nil {
|
||||
return nil, walkErr
|
||||
}
|
||||
if err := tarWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := gzipWriter.Close(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return buffer.Bytes(), nil
|
||||
}
|
||||
Reference in new issue
Block a user