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:
1 parent
525f410046
commit
2ece6aadbc
11 files changed
+589
-9
No files matched your search
+39
-1
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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);
|
||||
|
||||
@@ -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 ..
|
||||
|
||||
@@ -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
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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
|
||||
@@ -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)
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
Reference in new issue
Block a user