Faire tourner le simulateur iOS 6 sur macOS, avec souris et clavier

- Compilation sur macOS : en-têtes OpenSSL et libslirp de Homebrew, apps et
  agent compilés avec le clang de Xcode (cible 5.1, code ARM : le ld récent
  ne pose plus le bit Thumb sur LC_MAIN ni sur les IMP Objective-C).
- Sauvegarde du disque : copie par clone APFS (cp -c) au lieu des options GNU.
- Agent : commandes text et key (événements GS clavier matériel construits à
  la main, appui vide puis relâchement portant le caractère).
- Fenêtre macOS native (macos/, tools/build_mac_app.sh) : affichage via le VNC
  local, souris convertie en gestes, clavier transmis à l'agent.
- tools/update_agent.py remplace l'agent à chaud ; tools/vnc_input.py ajoute
  la souris et le clavier à un client VNC classique.
This commit is contained in:
Victor Bodinaud committed 2026-10-05 11:49:27 +02:00
1 parent 525f410046
commit 2ece6aadbc
11 files changed
+589 -9

No files matched your search

+39 -1
View File
@@ -734,6 +734,42 @@ static void cmd_touch(int fd, char **a, int n)
reply_str(fd,rc==0,"touch %d %s",rc,bundle);
}
static const char *front_target(char *bundle, size_t len)
{
load_sbs(); bundle[0]=0;
CFStringRef front=p_SBSCopyFrontmostApplicationDisplayIdentifier ? p_SBSCopyFrontmostApplicationDisplayIdentifier() : NULL;
if(front) { cfstr(front,bundle,len); CFRelease(front); }
return bundle[0] && strcmp(bundle,"com.apple.springboard") ? bundle : NULL;
}
/* text <chaîne UTF-8> : une touche par unité UTF-16, vers l'app au premier plan. */
static void cmd_text(int fd, char **a, int n)
{
if (n != 1) { reply_str(fd, 0, "text chaine"); return; }
CFStringRef s = CFStringCreateWithCString(NULL, a[0], kCFStringEncodingUTF8);
if (!s) { reply_str(fd, 0, "UTF-8 invalide"); return; }
char bundle[256]; const char *target = front_target(bundle, sizeof bundle);
int rc = 0;
for (CFIndex i = 0; i < CFStringGetLength(s) && !rc; i++) rc = send_key_event(CFStringGetCharacterAtIndex(s, i), target);
CFRelease(s);
reply_str(fd, rc == 0, "text %d %s", rc, bundle);
}
/* key return|backspace|tab|0xNNNN */
static void cmd_key(int fd, char **a, int n)
{
static const struct { const char *name; UniChar code; } keys[] = { {"return", '\n'}, {"backspace", 0x08}, {"tab", '\t'} };
if (n < 1 || n > 3) { reply_str(fd, 0, "key return|backspace|tab|0xNNNN [sequence DdUu [keycode]]"); return; }
long code = -1;
for (size_t i = 0; i < sizeof keys / sizeof *keys; i++) if (!strcmp(a[0], keys[i].name)) code = keys[i].code;
if (code < 0 && !strncmp(a[0], "0x", 2)) { char *end; code = strtol(a[0] + 2, &end, 16); if (*end || code <= 0 || code > 0xffff) code = -1; }
if (code < 0) { reply_str(fd, 0, "touche inconnue : %s", a[0]); return; }
char bundle[256]; const char *target = front_target(bundle, sizeof bundle);
int rc = n > 1 ? send_key_sequence((UniChar)code, target, a[1], n > 2 ? (uint16_t)strtol(a[2], NULL, 0) : 0)
: send_key_event((UniChar)code, target);
reply_str(fd, rc == 0, "key %d %s", rc, bundle);
}
static void handle(int fd, char *line)
{
char *args[64];
@@ -745,6 +781,8 @@ static void handle(int fd, char *line)
int na = n - 1;
if (!strcmp(cmd, "ping")) reply_str(fd, 1, "emuagent %s", VERSION);
else if (!strcmp(cmd, "touch")) cmd_touch(fd,a,na);
else if (!strcmp(cmd, "text")) cmd_text(fd,a,na);
else if (!strcmp(cmd, "key")) cmd_key(fd,a,na);
else if (!strcmp(cmd, "home")) {
void *h=dlopen("/System/Library/PrivateFrameworks/SpringBoardServices.framework/SpringBoardServices",RTLD_NOW);
int (*fn)(void)=h?dlsym(h,"SBSSuspendFrontmostApplication"):NULL;
@@ -778,7 +816,7 @@ static void handle(int fd, char *line)
else if (!strcmp(cmd, "net")) cmd_net(fd);
else if (!strcmp(cmd, "respring")) reply_str(fd, kill_by_name("SpringBoard", SIGTERM) > 0, "SpringBoard relancé");
else if (!strcmp(cmd, "sync")) { sync(); reply_str(fd, 1, "ok"); }
else reply_str(fd, 0, "commande inconnue : %s (ping exec launch fetch sc routes settime screen wake front apps put get ls rm mkdir ps kill net respring sync)", cmd);
else reply_str(fd, 0, "commande inconnue : %s (ping touch text key exec launch fetch sc routes settime screen wake front apps put get ls rm mkdir ps kill net respring sync)", cmd);
}
static void serve_client(int fd)
+291
View File
@@ -0,0 +1,291 @@
// Fenêtre macOS pour la VM iOS 6 : affiche l'écran via le VNC local de QEMU
// et transmet la souris et le clavier à l'agent invité (127.0.0.1:7796).
import AppKit
let vncPort: UInt16 = 5906
let agentPort: UInt16 = 7796
// MARK: - Sockets bloquantes
final class Socket {
let fd: Int32
init?(port: UInt16) {
fd = socket(AF_INET, SOCK_STREAM, 0)
if fd < 0 { return nil }
var one: Int32 = 1
setsockopt(fd, SOL_SOCKET, SO_NOSIGPIPE, &one, socklen_t(MemoryLayout<Int32>.size))
var addr = sockaddr_in()
addr.sin_family = sa_family_t(AF_INET)
addr.sin_port = port.bigEndian
addr.sin_addr.s_addr = inet_addr("127.0.0.1")
let ok = withUnsafePointer(to: &addr) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
connect(fd, $0, socklen_t(MemoryLayout<sockaddr_in>.size)) == 0
}
}
if !ok { close(fd); return nil }
}
deinit { close(fd) }
func read(_ n: Int) throws -> [UInt8] {
var out = [UInt8](repeating: 0, count: n)
var got = 0
while got < n {
let r = out.withUnsafeMutableBytes { recv(fd, $0.baseAddress! + got, n - got, 0) }
if r <= 0 { throw POSIXError(.ECONNRESET) }
got += r
}
return out
}
func readInto(_ buffer: UnsafeMutableRawPointer, _ n: Int) throws {
var got = 0
while got < n {
let r = recv(fd, buffer + got, n - got, 0)
if r <= 0 { throw POSIXError(.ECONNRESET) }
got += r
}
}
func write(_ bytes: [UInt8]) throws {
var sent = 0
while sent < bytes.count {
let r = bytes.withUnsafeBytes { send(fd, $0.baseAddress! + sent, bytes.count - sent, 0) }
if r <= 0 { throw POSIXError(.EPIPE) }
sent += r
}
}
func readLine() throws -> String {
var line = [UInt8]()
while true {
let c = try read(1)[0]
if c == 10 { return String(decoding: line, as: UTF8.self) }
line.append(c)
}
}
}
func u16(_ b: [UInt8], _ i: Int) -> Int { Int(b[i]) << 8 | Int(b[i + 1]) }
func u32(_ b: [UInt8], _ i: Int) -> Int { u16(b, i) << 16 | u16(b, i + 2) }
func be16(_ v: Int) -> [UInt8] { [UInt8(v >> 8 & 0xff), UInt8(v & 0xff)] }
// MARK: - Agent invité
final class Agent {
private let queue = DispatchQueue(label: "agent")
private var warnedKeyboard = false
/// Envoie une commande de l'agent ; les gestes sont exécutés dans l'ordre.
func send(_ fields: [String]) {
queue.async {
guard let s = Socket(port: agentPort) else { return }
do {
try s.write(Array((fields.joined(separator: "\t") + "\n").utf8))
let header = try s.readLine().split(separator: " ", maxSplits: 2).map(String.init)
let size = header.count > 1 ? Int(header[1]) ?? 0 : 0
let body = String(decoding: try s.read(size), as: UTF8.self)
if header.first == "ERR" {
if body.contains("commande inconnue") {
if !self.warnedKeyboard { self.warnedKeyboard = true; NSLog("Clavier indisponible : l'agent n'a pas encore text/key") }
} else { NSLog("Agent : %@", body) }
}
} catch {}
}
}
}
// MARK: - Client VNC (encodage brut)
final class VNCClient {
var onFrame: ((CGImage?) -> Void)?
private var width = 0, height = 0
private var pixels = [UInt8]()
func run() {
Thread.detachNewThread { [weak self] in
while let self {
try? self.session()
DispatchQueue.main.async { self.onFrame?(nil) }
Thread.sleep(forTimeInterval: 1)
}
}
}
private func session() throws {
guard let s = Socket(port: vncPort) else { return }
_ = try s.read(12)
try s.write(Array("RFB 003.008\n".utf8))
let count = Int(try s.read(1)[0])
guard count > 0, try s.read(count).contains(1) else { return }
try s.write([1])
guard try s.read(4) == [0, 0, 0, 0] else { return }
try s.write([1]) // connexion partagée
let initMsg = try s.read(24)
resize(u16(initMsg, 0), u16(initMsg, 2))
_ = try s.read(u32(initMsg, 20))
// 32 bpp, profondeur 24, little-endian, rouge << 16 : octets BGRX.
try s.write([0, 0, 0, 0, 32, 24, 0, 1, 0, 255, 0, 255, 0, 255, 16, 8, 0, 0, 0, 0])
try s.write([2, 0] + be16(2) + [0, 0, 0, 0] + [0xff, 0xff, 0xff, 0x21]) // brut, DesktopSize
try request(s, incremental: false)
while true {
let type = try s.read(1)[0]
switch type {
case 0:
let head = try s.read(3)
for _ in 0..<u16(head, 1) {
let r = try s.read(12)
let x = u16(r, 0), y = u16(r, 2), w = u16(r, 4), h = u16(r, 6)
let encoding = Int32(bitPattern: UInt32(u32(r, 8)))
if encoding == -223 { resize(w, h); continue }
guard encoding == 0 else { return }
try readRect(s, x, y, w, h)
}
publish()
try request(s, incremental: true)
case 1:
let head = try s.read(5)
_ = try s.read(6 * u16(head, 3))
case 2: break
case 3:
let head = try s.read(7)
_ = try s.read(u32(head, 3))
default: return
}
}
}
private func resize(_ w: Int, _ h: Int) {
width = w; height = h
pixels = [UInt8](repeating: 0, count: w * h * 4)
}
private func readRect(_ s: Socket, _ x: Int, _ y: Int, _ w: Int, _ h: Int) throws {
guard x + w <= width, y + h <= height else { throw POSIXError(.EINVAL) }
try pixels.withUnsafeMutableBytes { buf in
for row in 0..<h {
try s.readInto(buf.baseAddress! + ((y + row) * width + x) * 4, w * 4)
}
}
}
private func request(_ s: Socket, incremental: Bool) throws {
try s.write([3, incremental ? 1 : 0, 0, 0, 0, 0] + be16(width) + be16(height))
}
private func publish() {
let data = Data(pixels) as CFData
guard let provider = CGDataProvider(data: data) else { return }
let info = CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipFirst.rawValue | CGBitmapInfo.byteOrder32Little.rawValue)
let image = CGImage(width: width, height: height, bitsPerComponent: 8, bitsPerPixel: 32,
bytesPerRow: width * 4, space: CGColorSpaceCreateDeviceRGB(), bitmapInfo: info,
provider: provider, decode: nil, shouldInterpolate: false, intent: .defaultIntent)
DispatchQueue.main.async { self.onFrame?(image) }
}
}
// MARK: - Vue de l'écran
final class ScreenView: NSView {
let agent = Agent()
var screenSize = CGSize(width: 320, height: 480)
private var press: (CGPoint, Date)?
override var isFlipped: Bool { true }
override var acceptsFirstResponder: Bool { true }
override func acceptsFirstMouse(for event: NSEvent?) -> Bool { true }
func show(_ image: CGImage?) {
wantsLayer = true
layer?.backgroundColor = NSColor.black.cgColor
layer?.contentsGravity = .resize
layer?.magnificationFilter = .nearest
layer?.contents = image
if let image { screenSize = CGSize(width: image.width, height: image.height) }
}
private func guestPoint(_ event: NSEvent) -> CGPoint {
let p = convert(event.locationInWindow, from: nil)
let x = min(max(p.x / bounds.width * screenSize.width, 0), screenSize.width - 1)
let y = min(max(p.y / bounds.height * screenSize.height, 0), screenSize.height - 1)
return CGPoint(x: x.rounded(), y: y.rounded())
}
override func mouseDown(with event: NSEvent) { press = (guestPoint(event), Date()) }
override func mouseUp(with event: NSEvent) {
guard let (start, time) = press else { return }
press = nil
var end = guestPoint(event)
var ms = Int(Date().timeIntervalSince(time) * 1000)
if abs(end.x - start.x) < 6 && abs(end.y - start.y) < 6 { end = start }
ms = min(max(ms, 96), 5000)
agent.send(["touch", "\(Int(start.x))", "\(Int(start.y))", "\(Int(end.x))", "\(Int(end.y))", "\(ms)"])
}
override func rightMouseDown(with event: NSEvent) { agent.send(["home"]) }
override func keyDown(with event: NSEvent) {
switch event.keyCode {
case 53: agent.send(["home"]) // Échap
case 36, 76: agent.send(["key", "return"])
case 51: agent.send(["key", "backspace"])
case 48: agent.send(["key", "tab"])
default:
guard let text = event.characters, !text.isEmpty,
!event.modifierFlags.contains(.command),
text.unicodeScalars.allSatisfy({ $0.value >= 0x20 && $0.value != 0x7f && $0.value < 0xf700 })
else { return super.keyDown(with: event) }
agent.send(["text", text])
}
}
}
// MARK: - Application
final class AppDelegate: NSObject, NSApplicationDelegate {
var window: NSWindow!
let vnc = VNCClient()
func applicationDidFinishLaunching(_ note: Notification) {
let view = ScreenView(frame: NSRect(x: 0, y: 0, width: 320, height: 480))
view.show(nil)
window = NSWindow(contentRect: view.frame, styleMask: [.titled, .closable, .miniaturizable, .resizable],
backing: .buffered, defer: false)
window.title = "iOS 6"
window.contentView = view
window.contentAspectRatio = NSSize(width: 320, height: 480)
window.setContentSize(NSSize(width: 320, height: 480))
window.center()
window.makeKeyAndOrderFront(nil)
window.makeFirstResponder(view)
buildMenu(view)
vnc.onFrame = { [weak view] image in view?.show(image) }
vnc.run()
NSApp.activate(ignoringOtherApps: true)
}
func applicationShouldTerminateAfterLastWindowClosed(_ app: NSApplication) -> Bool { true }
private func buildMenu(_ view: ScreenView) {
let main = NSMenu()
let appItem = NSMenuItem(); main.addItem(appItem)
let appMenu = NSMenu()
appMenu.addItem(withTitle: "Quitter", action: #selector(NSApplication.terminate(_:)), keyEquivalent: "q")
appItem.submenu = appMenu
let deviceItem = NSMenuItem(); main.addItem(deviceItem)
let device = NSMenu(title: "Appareil")
for (title, key, action) in [("Home", "h", #selector(home)), ("Taille réelle", "1", #selector(size1)), ("Taille ×2", "2", #selector(size2))] {
let item = device.addItem(withTitle: title, action: action, keyEquivalent: key)
item.keyEquivalentModifierMask = [.command, .shift]
item.target = self
}
deviceItem.submenu = device
NSApp.mainMenu = main
}
@objc func home() { (window.contentView as? ScreenView)?.agent.send(["home"]) }
@objc func size1() { window.setContentSize(NSSize(width: 320, height: 480)) }
@objc func size2() { window.setContentSize(NSSize(width: 640, height: 960)) }
}
let app = NSApplication.shared
let delegate = AppDelegate()
app.delegate = delegate
app.setActivationPolicy(.regular)
app.run()
+47
View File
@@ -40,6 +40,53 @@ static int send_touch_gesture(float x1, float y1, float x2, float y2, unsigned d
return 0;
}
/* Touche clavier matériel (GSEventKeyDown 10 / KeyUp 11). GSEventCreateKeyEvent
* exige le clavier d'un processus UIKit : l'enregistrement est donc construit
* à la main. GSHardwareKeyInfo (144 octets) suit l'en-tête de 52 octets :
* keyCode, charIgnoringModifier, charCode, charSet, puis longueur et UTF-16
* des caractères (32 max), même chose pour les caractères non modifiés. */
static int send_key_sequence(UniChar unicode, const char *bundle, const char *seq, uint16_t keycode);
static int send_key_event(UniChar unicode, const char *bundle)
{
/* Vérifié sous iOS 6.1.6 : UIKit insère le caractère porté par chaque
* événement, et seul un relâchement avec caractère arrête la répétition.
* L'appui est donc vide et le relâchement porte le caractère. */
return send_key_sequence(unicode, bundle, "dU", 0);
}
/* seq : D/d appui avec/sans caractère, U/u relâchement avec/sans caractère. */
static int send_key_sequence(UniChar unicode, const char *bundle, const char *seq, uint16_t keycode)
{
void *gs = dlopen("/System/Library/PrivateFrameworks/GraphicsServices.framework/GraphicsServices", RTLD_NOW);
if (!gs) return -1;
void (*send)(const void *) = dlsym(gs, "GSSendSystemEvent");
void (*send_port)(const void *, unsigned) = dlsym(gs, "GSSendEvent");
unsigned (*copy_port)(const char *) = dlsym(gs, "GSCopyPurpleNamedPort");
uint64_t (*timestamp)(void) = dlsym(gs, "GSCurrentEventTimestamp");
if (!send || !timestamp) { dlclose(gs); return -1; }
unsigned port = bundle && copy_port ? copy_port(bundle) : 0;
if (bundle && (!port || !send_port)) { dlclose(gs); return -2; }
for (const char *step = seq; *step; step++) {
uint32_t packet[49] = {0}; /* 52 + 144 octets */
unsigned char *bytes = (void *)packet;
uint64_t time = timestamp();
UniChar info[4] = {keycode, unicode, unicode, 0};
uint16_t one = *step == 'D' || *step == 'U';
packet[0] = *step == 'D' || *step == 'd' ? 10 : 11;
memcpy(bytes + 28, &time, sizeof(time));
packet[12] = 144; /* infoSize */
memcpy(bytes + 52, info, sizeof(info));
memcpy(bytes + 60, &one, 2); /* characters_length */
memcpy(bytes + 62, &unicode, 2); /* characters[0] */
memcpy(bytes + 126, &one, 2); /* unmodified_characters_length */
memcpy(bytes + 128, &unicode, 2);
if (port) send_port(packet, port); else send(packet);
usleep(50000); /* 20 ms : caractères perdus près des espaces */
}
dlclose(gs);
return 0;
}
static void send_unlock_swipe(void)
{
send_touch_gesture(50, 433, 245, 433, 800, NULL);
+1
View File
@@ -18,6 +18,7 @@ cd build
--target-list=arm-softmmu --disable-docs --disable-werror \
--disable-gtk --disable-sdl --disable-slirp --disable-capstone \
--disable-tools --disable-guest-agent --disable-debug-info \
--extra-cflags="$(pkg-config --cflags openssl 2>/dev/null)" \
--extra-ldflags="$(pkg-config --libs-only-L openssl 2>/dev/null) -lcrypto" > ../evidence/configure.log 2>&1
ninja -j"${IOS6_BUILD_JOBS:-4}" qemu-system-arm > ../evidence/build.log 2>&1
cd ..
+12 -2
View File
@@ -3,6 +3,16 @@ set -euo pipefail
sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$sim_root"
mkdir -p build
/opt/cctools/bin/arm-apple-darwin9-gcc -isysroot /opt/theos/sdks/iPhoneOS2.2.1.sdk -miphoneos-version-min=2.2.1 -std=gnu99 -O2 agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent
/opt/cctools/bin/ldid -Sagent/entitlements.plist build/ios6-agent
if [[ -x /opt/cctools/bin/arm-apple-darwin9-gcc ]]; then
/opt/cctools/bin/arm-apple-darwin9-gcc -isysroot /opt/theos/sdks/iPhoneOS2.2.1.sdk -miphoneos-version-min=2.2.1 -std=gnu99 -O2 agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent
/opt/cctools/bin/ldid -Sagent/entitlements.plist build/ios6-agent
else
# macOS : clang de Xcode, mêmes contraintes que tools/build_app.py
# (cible 5.1 pour LC_UNIXTHREAD, code ARM seulement).
sdk=${IOS6_SDK:-$sim_root/references/sdk/iPhoneOS6.0.sdk}
[[ -f $sdk/SDKSettings.plist ]] || sdk=${THEOS:-$HOME/theos}/sdks/iPhoneOS6.1.sdk
xcrun clang -target armv7-apple-ios5.1 -marm -isysroot "$sdk" -std=gnu99 -O2 -Wno-incompatible-pointer-types-discards-qualifiers \
agent/ios6-agent.c -framework CoreFoundation -framework SystemConfiguration -o build/ios6-agent
ldid -Sagent/entitlements.plist build/ios6-agent
fi
bash tools/build_netd.sh
+18 -4
View File
@@ -1,15 +1,29 @@
#!/usr/bin/env python3
"""Compile un projet Objective-C simple (.m) en bundle ARM pour la VM iOS 6."""
import argparse,os,pathlib,plistlib,subprocess
import argparse,os,pathlib,plistlib,shutil,subprocess
ROOT=pathlib.Path(__file__).resolve().parents[1]
CCTOOLS=pathlib.Path('/opt/cctools/bin')
def default_sdk():
if os.environ.get('IOS6_SDK'): return os.environ['IOS6_SDK']
for sdk in (ROOT/'references/sdk/iPhoneOS6.0.sdk',pathlib.Path(os.environ.get('THEOS',pathlib.Path.home()/'theos'))/'sdks/iPhoneOS6.1.sdk'):
if (sdk/'SDKSettings.plist').is_file(): return str(sdk)
return str(ROOT/'references/sdk/iPhoneOS6.0.sdk')
def compiler():
"""GCC cctools sous Linux, clang de Xcode sous macOS."""
if (CCTOOLS/'arm-apple-darwin9-gcc').exists(): return [str(CCTOOLS/'arm-apple-darwin9-gcc'),'-arch','armv7','-miphoneos-version-min=6.0'],str(CCTOOLS/'ldid')
# Le ld récent ne pose plus le bit Thumb sur le point d'entrée LC_MAIN ni sur
# les IMP Objective-C : sous iOS 6, le saut part en mode ARM (SIGBUS
# EXC_ARM_DA_ALIGN). Cible 5.1 (crt1.o, LC_UNIXTHREAD) et code ARM seulement.
return ['xcrun','clang','-target','armv7-apple-ios5.1','-marm'],shutil.which('ldid') or 'ldid'
def main():
p=argparse.ArgumentParser(description=__doc__);p.add_argument('project',type=pathlib.Path,nargs='?',default=ROOT/'apps/SimulatorLab');p.add_argument('--bundle-id');p.add_argument('--sdk',default=os.environ.get('IOS6_SDK',str(ROOT/'references/sdk/iPhoneOS6.0.sdk')));a=p.parse_args()
p=argparse.ArgumentParser(description=__doc__);p.add_argument('project',type=pathlib.Path,nargs='?',default=ROOT/'apps/SimulatorLab');p.add_argument('--bundle-id');p.add_argument('--sdk',default=default_sdk());a=p.parse_args()
if not pathlib.Path(a.sdk,'SDKSettings.plist').is_file(): p.error('SDK iOS absent : exécuter tools/prepare_sdk.py ou fournir --sdk')
name=a.project.resolve().name;out=ROOT/'build/apps'/(name+'.app');out.mkdir(parents=True,exist_ok=True)
a.bundle_id=a.bundle_id or 'org.ios6emu.'+name
sources=sorted(a.project.glob('*.m'))
if not sources: p.error('Aucune source Objective-C')
subprocess.run(['/opt/cctools/bin/arm-apple-darwin9-gcc','-isysroot',a.sdk,'-miphoneos-version-min=6.0','-arch','armv7','-fblocks','-std=gnu99','-O2','-Wall','-Wextra',*map(str,sources),'-framework','UIKit','-framework','Foundation','-framework','CoreGraphics','-o',str(out/name)],check=True)
cc,ldid=compiler()
subprocess.run([*cc,'-isysroot',a.sdk,'-fblocks','-std=gnu99','-O2','-Wall','-Wextra',*map(str,sources),'-framework','UIKit','-framework','Foundation','-framework','CoreGraphics','-o',str(out/name)],check=True)
(out/'Info.plist').write_bytes(plistlib.dumps({'CFBundleIdentifier':a.bundle_id,'CFBundleExecutable':name,'CFBundleName':name,'CFBundleDisplayName':name,'CFBundlePackageType':'APPL','CFBundleVersion':'1','MinimumOSVersion':'6.0','LSRequiresIPhoneOS':True,'UIDeviceFamily':[1]}))
subprocess.run(['/opt/cctools/bin/ldid','-S',str(out/name)],check=True);print(out)
subprocess.run([ldid,'-S',str(out/name)],check=True);print(out)
if __name__=='__main__': main()
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# Compile la fenêtre macOS de la VM iOS 6 dans build/iOS6 Simulator.app.
set -euo pipefail
sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
app="$sim_root/build/iOS6 Simulator.app"
mkdir -p "$app/Contents/MacOS"
xcrun swiftc -O -o "$app/Contents/MacOS/iOS6Simulator" "$sim_root/macos/iOS6Simulator.swift"
cat > "$app/Contents/Info.plist" <<'PLIST'
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>CFBundleIdentifier</key><string>org.ios6emu.mac</string>
<key>CFBundleName</key><string>iOS6 Simulator</string>
<key>CFBundleExecutable</key><string>iOS6Simulator</string>
<key>CFBundlePackageType</key><string>APPL</string>
<key>CFBundleShortVersionString</key><string>1.0</string>
<key>LSMinimumSystemVersion</key><string>13.0</string>
<key>NSHighResolutionCapable</key><true/>
</dict></plist>
PLIST
codesign -s - --force "$app" >/dev/null
echo "$app"
+2 -1
View File
@@ -4,5 +4,6 @@ set -euo pipefail
sim_root=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")/.." && pwd)
cd "$sim_root"
mkdir -p build
cc -O2 agent/netd.c -o build/ios6-netd.new $(pkg-config --cflags --libs slirp)
cc -O2 agent/netd.c -o build/ios6-netd.new \
-I"$(pkg-config --variable=includedir slirp)" $(pkg-config --cflags --libs slirp)
mv build/ios6-netd.new build/ios6-netd
+8 -1
View File
@@ -11,8 +11,10 @@ import argparse
import json
import os
import pathlib
import shutil
import struct
import subprocess
import sys
def checksum(data):
@@ -97,7 +99,12 @@ def recover(source, destination):
position = advance(position, used)
headers += 1
# Toutes les validations précèdent la création de la copie.
subprocess.run(['cp', '--reflink=auto', '--sparse=always', str(source), str(destination)], check=True)
if sys.platform == 'darwin':
# cp BSD : clone APFS (-c), copie simple si le volume ne le permet pas.
if subprocess.run(['cp', '-c', str(source), str(destination)]).returncode:
shutil.copyfile(source, destination)
else:
subprocess.run(['cp', '--reflink=auto', '--sparse=always', str(source), str(destination)], check=True)
with destination.open('r+b') as image:
for target, data in writes:
image.seek(target)
+28
View File
@@ -0,0 +1,28 @@
#!/usr/bin/env python3
"""Remplace l'agent invité par build/ios6-agent ; launchd (KeepAlive) le relance.
Le fichier est écrit de façon atomique puis l'agent courant est arrêté. Lancer
ensuite `bin/ios6 stop` pour conserver la nouvelle version sur le disque.
"""
import time
from simulator import Agent, ROOT, command
def main():
data = (ROOT / 'build/ios6-agent').read_bytes()
with Agent() as agent:
agent.call('put', '/usr/libexec/ios6-agent', '755', len(data), data=data)
agent.call('sync')
# Chaque client est servi par un fils : arrêter le processus lancé par launchd.
main_pid = next(line.split()[0] for line in command('ps').splitlines()
if line.split()[1:] == ['1', '0', 'ios6-agent'])
try: command('kill', main_pid, 15, timeout=5)
except Exception: pass # l'agent s'arrête avant de répondre
deadline = time.monotonic() + 30
while True:
time.sleep(1)
try: print('agent relancé :', command('ping', timeout=5)); return
except Exception:
if time.monotonic() > deadline: raise SystemExit("L'agent ne répond plus")
if __name__ == '__main__': main()
+121
View File
@@ -0,0 +1,121 @@
#!/usr/bin/env python3
"""Relais VNC : transmet l'affichage de QEMU et convertit la souris en gestes tactiles.
Le client VNC se connecte au relais (127.0.0.1:5907 par défaut). L'affichage
et les messages du client passent tels quels vers QEMU (127.0.0.1:5906), sauf
les événements souris et clavier : un appui suivi d'un relâchement du bouton
gauche devient un geste `touch` de l'agent (tap ou glissé, avec sa durée), le
bouton droit et la touche Échap envoient `home`. Les touches imprimables
deviennent `text <caractère>`, Entrée, Retour arrière et Tab `key <nom>`.
"""
import argparse
import queue
import socket
import struct
import sys
import threading
import time
from simulator import command
WIDTH, HEIGHT = 320, 480
KEY_ESCAPE, KEY_HOME = 0xff1b, 0xff50
SPECIAL_KEYS = {0xff0d: 'return', 0xff8d: 'return', 0xff08: 'backspace', 0xff09: 'tab'}
def recv_exact(sock, n):
data = bytearray()
while len(data) < n:
chunk = sock.recv(n - len(data))
if not chunk: raise ConnectionError
data.extend(chunk)
return bytes(data)
def pipe(src, dst):
try:
while chunk := src.recv(65536): dst.sendall(chunk)
except OSError: pass
finally:
for s in (src, dst):
try: s.shutdown(socket.SHUT_RDWR)
except OSError: pass
def gestures(actions):
warned = False
while True:
args = actions.get()
try: command(*args)
except Exception as e:
if 'commande inconnue' not in str(e): print('geste refusé :', e, file=sys.stderr, flush=True)
elif not warned:
warned = True
print("clavier indisponible : l'agent invité n'a pas encore les commandes text/key", file=sys.stderr, flush=True)
def clamp(v, top): return min(max(v, 0), top - 1)
def client_messages(client, qemu, actions):
"""Lit le flux client RFB message par message pour isoler la souris et le clavier."""
version = recv_exact(client, 12); qemu.sendall(version)
if version != b'RFB 003.003\n': qemu.sendall(recv_exact(client, 1)) # type de sécurité
qemu.sendall(recv_exact(client, 1)) # ClientInit
buttons, press = 0, None
while True:
kind = recv_exact(client, 1)[0]
if kind == 5:
mask, x, y = struct.unpack('>BHH', recv_exact(client, 5))
x, y = clamp(x, WIDTH), clamp(y, HEIGHT)
if mask & 1 and not buttons & 1: press = (x, y, time.monotonic())
elif buttons & 1 and not mask & 1 and press:
x0, y0, t0 = press; press = None
ms = int((time.monotonic() - t0) * 1000)
if abs(x - x0) < 6 and abs(y - y0) < 6: x, y, ms = x0, y0, max(ms, 96)
actions.put(('touch', x0, y0, x, y, min(max(ms, 96), 5000)))
if mask & 4 and not buttons & 4: actions.put(('home',))
buttons = mask
continue
if kind == 4:
down, key = struct.unpack('>B2xI', recv_exact(client, 7))
if not down: continue
if key in (KEY_ESCAPE, KEY_HOME): actions.put(('home',))
elif key in SPECIAL_KEYS: actions.put(('key', SPECIAL_KEYS[key]))
elif 0x20 <= key < 0x100 and key != 0x7f: actions.put(('text', chr(key)))
elif key & 0xff000000 == 0x01000000: actions.put(('text', chr(key & 0xffffff)))
continue
if kind == 255:
body = recv_exact(client, 11) # événement clavier étendu QEMU, ignoré
if body[0] != 0: raise ConnectionError(f'message QEMU {body[0]} non géré')
continue
if kind == 0: body = recv_exact(client, 19)
elif kind == 2:
head = recv_exact(client, 3); body = head + recv_exact(client, 4 * struct.unpack('>xH', head)[0])
elif kind == 3: body = recv_exact(client, 9)
elif kind == 6:
head = recv_exact(client, 7); body = head + recv_exact(client, struct.unpack('>3xI', head)[0])
elif kind == 150: body = recv_exact(client, 9)
elif kind == 248:
head = recv_exact(client, 7); body = head + recv_exact(client, 16 * head[4])
elif kind == 250:
head = recv_exact(client, 8); body = head + recv_exact(client, head[7])
else: raise ConnectionError(f'message client {kind} non géré')
qemu.sendall(bytes([kind]) + body)
def serve(client, target, actions):
with client, socket.create_connection(target) as qemu:
threading.Thread(target=pipe, args=(qemu, client), daemon=True).start()
try: client_messages(client, qemu, actions)
except (ConnectionError, OSError) as e:
if str(e): print('connexion fermée :', e, file=sys.stderr, flush=True)
def main():
p = argparse.ArgumentParser(description=__doc__)
p.add_argument('--port', type=int, default=5907)
p.add_argument('--qemu-port', type=int, default=5906)
a = p.parse_args()
actions = queue.Queue()
threading.Thread(target=gestures, args=(actions,), daemon=True).start()
server = socket.create_server(('127.0.0.1', a.port))
print(f'Relais VNC tactile : vnc://127.0.0.1:{a.port}', flush=True)
while True:
client, _ = server.accept()
threading.Thread(target=serve, args=(client, ('127.0.0.1', a.qemu_port), actions), daemon=True).start()
if __name__ == '__main__': main()