feat(ioscompanion): pre-grant pasteboard access so unicode input skips the OS prompt

This commit is contained in:
pj committed 2026-06-07 11:21:49 +05:30
1 parent 8d38210da9
commit edd70b661b
1 file changed
+43 -8
+43 -8
View File
@@ -82,7 +82,6 @@ type Driver struct {
set bool
}
pastePrimed bool
clearStateWarned bool
// restart rebuilds the transport in place after a connection-level failure.
@@ -100,6 +99,10 @@ type Driver struct {
// A seam so tests skip the simctl shell-outs.
reinstallApp func(ctx context.Context) error
// grantPaste pre-authorizes the app's pasteboard access. A seam so tests
// skip the sqlite shell-out.
grantPaste func(ctx context.Context) error
// idleClock drives WaitForIdle's settle poll. A seam so tests substitute a
// fake clock and avoid the real settle cap.
idleClock Clock
@@ -159,6 +162,7 @@ func New(ctx context.Context, options Options) (*Driver, error) {
driverInstance.restart = driverInstance.respawnAndRedial
driverInstance.resetContainer = driverInstance.resetDataContainer
driverInstance.reinstallApp = driverInstance.simctlReinstall
driverInstance.grantPaste = driverInstance.grantPasteboardAccess
driverInstance.processContext, driverInstance.processCancel = context.WithCancel(ctx)
if err := driverInstance.bringUp(ctx); err != nil {
@@ -292,18 +296,22 @@ func (d *Driver) Launch(ctx context.Context, bundleID string, clearState bool, e
}
}
// Grant the app pasteboard access before it runs so unicode input (which
// must go through the pasteboard, since HID cannot express it) never trips
// the iOS paste-permission prompt. clearState reinstall resets the grant,
// so it is reapplied on every launch. Best effort: if it fails, the paste
// path still handles the prompt, just slower.
if d.bundleID != "" {
if err := d.grantPaste(ctx); err != nil {
fmt.Fprintf(d.output, "grant pasteboard access failed (continuing): %v\n", err)
}
}
if err := d.withRecovery(ctx, func() error {
return d.companion.Launch(ctx, d.bundleID, true)
}); err != nil {
return fmt.Errorf("launch %s: %w", d.bundleID, err)
}
if !d.pastePrimed {
if err := warmUpPaste(ctx, d.makeRunner()); err != nil {
fmt.Fprintf(d.output, "paste warm-up failed (continuing): %v\n", err)
}
d.pastePrimed = true
}
return nil
}
@@ -336,6 +344,33 @@ func (d *Driver) simctlReinstall(ctx context.Context) error {
return nil
}
// grantPasteboardAccess authorizes the app to read the pasteboard without the
// iOS permission prompt, by writing an allow row into the simulator's privacy
// (TCC) database. This is the simulator counterpart to `simctl privacy grant`,
// which does not expose the pasteboard service. Without it, every unicode input
// (which must paste, since HID cannot express unicode) blocks on a modal that
// costs seconds; with it the paste lands in one frame.
func (d *Driver) grantPasteboardAccess(ctx context.Context) error {
databasePath := filepath.Join(
os.Getenv("HOME"), "Library", "Developer", "CoreSimulator", "Devices",
d.udid, "data", "Library", "TCC", "TCC.db",
)
if _, err := os.Stat(databasePath); err != nil {
return fmt.Errorf("locate privacy database: %w", err)
}
statement := fmt.Sprintf(
"INSERT OR REPLACE INTO access "+
"(service,client,client_type,auth_value,auth_reason,auth_version,indirect_object_identifier) "+
"VALUES ('kTCCServicePasteboard','%s',0,2,4,1,'UNUSED');",
d.bundleID,
)
output, err := exec.CommandContext(ctx, "sqlite3", databasePath, statement).CombinedOutput()
if err != nil {
return fmt.Errorf("write privacy grant: %w: %s", err, strings.TrimSpace(string(output)))
}
return nil
}
// resetDataContainer deletes the contents of the app's data container so the
// next launch starts with empty storage.
func (d *Driver) resetDataContainer(ctx context.Context) error {