MacOS Caching Server : IPA/System Updates Report (from logs)

When using a Mac Mini how can you be sure that is is working, well there are two ways you can do this, one from the server itself and one from your iPhone.

Server Log Analysis

If you have access to the server run this command which will show the AssetCache on the server:

log show --predicate 'subsystem == "com.apple.AssetCache"' --info --last 1h

This will give you the last hour of the logs which should look something like this fore tracking when applications/iOS updates are requested, you will notice that once the server has cached the application/iOS update then you will see the update coming from the Caching server NOT the internet:

2025-01-17 18:15:28.377791+0000 0x210e     Default     0x0                  200    21   AssetCache: [com.apple.AssetCache:builtin] Since server start at 2025-01-17 17:24:37.634: 99.0 MB returned to clients, 0 bytes to peers, 0 bytes to children; 281.4 MB stored from Internet, 0 bytes from peers, 0 bytes from parents; 0 bytes imported.

2025-01-17 18:19:11.164694+0000 0x2f72     Default     0x0                  200    21   AssetCache: [com.apple.AssetCache:builtin] #juj1UapxWU6Y Received GET request by "com.apple.appstored/1.0" for /itunes-assets/Purple221/v4/8a/7b/7d/8a7b7d9b-d93f-b47c-7770-2a78cafe3ee6/pre-thinned6099621525846926815.thinned.signed.dpkg.ipa

2025-01-17 18:19:11.166427+0000 0x2f72     Default     0x0                  200    21   AssetCache: [com.apple.AssetCache:builtin] #juj1UapxWU6Y ECAssetHandler[0x6c09dfc60]: Asset /itunes-assets/Purple221/v4/8a/7b/7d/8a7b7d9b-d93f-b47c-7770-2a78cafe3ee6/pre-thinned6099621525846926815.thinned.signed.dpkg.ipa, path /Library/Application Support/Apple/AssetCache/Data/E791F849-EA78-46D2-88D0-C0882D73E678, extents none

2025-01-17 18:19:11.166984+0000 0x2f72     Default     0x0                  200    21   AssetCache: [com.apple.AssetCache:builtin] #juj1UapxWU6Y ECAssetRequestor[0x6c0ac1180]: Issuing outgoing full request for URL https://iosapps.itunes.apple.com/itunes-assets/Purple221/v4/8a/7b/7d/8a7b7d9b-d93f-b47c-7770-2a78cafe3ee6/pre-thinned6099621525846926815.thinned.signed.dpkg.ipa?accessKey=1737332349_5282499352200846955_BVVSVAzAOMwEB0ufwe%2BNloipf7BnA%2FFF7uU7VPBPQUqCLcN4X5l8KTAPE7EcLBZLrkofG3qJwc9k6mbgM%2BcGdZzKaiDe6LtC019LdOOrGTbfjlJSCdfWGJhEojuy5Vs7zUQg17Xiq2OSEebp%2B1zEK3sEAdjEMVGne8XsdAsuLNJT3scrbZBHkYPEqVn3Uo180T0RyCdPE%2FvrO9TbugN6pw%3D%3D

This then shows that this file from the App Store is also being cached on the MacOS device:

/itunes-assets/Purple221/v4/8a/7b/7d/8a7b7d9b-d93f-b47c-7770-2a78cafe3ee6/pre-thinned6099621525846926815.thinned.signed.dpkg.ipa

You can also get a report from the sever that will outline all the IPA applications and iOS/MacOS Updates which looks like this:



The script for this is below, simple run it from the Caching server:

Apple Script : check_requests.sh

#!/bin/bash

# Output file in current directory
OUTPUT_FILE="ios_cache_report.html"

# Get current timestamp
TIMESTAMP=$(date "+%Y-%m-%d %H:%M:%S")

# Get log data
LOG_DATA=$(log show --predicate 'subsystem == "com.apple.AssetCache"' --info --last 2h)

# Process logs to get statistics
TOTAL_IOS_SERVED=$(echo "$LOG_DATA" | grep -E "Served all.*?(ipa|MobileAsset_SoftwareUpdate)" | tail -n 1 | grep -oE "Since server start.*?MB|GB" | grep -oE "[0-9.]+ [MG]B")
TOTAL_IOS_CACHE=$(echo "$LOG_DATA" | grep -E "Served all.*?(ipa|MobileAsset_SoftwareUpdate)" | tail -n 1 | grep -oE "from cache, [0-9.]+ [MG]B" | grep -oE "[0-9.]+ [MG]B")
TOTAL_IOS_INTERNET=$(echo "$LOG_DATA" | grep -E "Served all.*?(ipa|MobileAsset_SoftwareUpdate)" | tail -n 1 | grep -oE "from Internet, [0-9.]+ [MG]B" | grep -oE "[0-9.]+ [MG]B")

# Extract iOS and IPA requests
REQUESTS=$(echo "$LOG_DATA" | awk '
BEGIN { FS="[[:space:]]+"; OFS="|" }
/Received GET request.*\.ipa|Received GET request.*MobileAsset_SoftwareUpdate/ { 
    for(i=1;i<=NF;i++) {
        if($i ~ /^#/) {
            id = substr($i, 2)
            time = $1
            # Get full URL path
            for(j=i;j<=NF;j++) {
                if($j == "for") {
                    path = $(j+1)
                    for(k=j+2;k<=NF;k++) {
                        path = path " " $k
                    }
                }
            }
            # Store the request info
            print time, id, path, "pending", "no"
        }
    }
}
/Served all/ {
    for(i=1;i<=NF;i++) {
        if($i ~ /^#/) {
            id = substr($i, 2)
            size = ""
            cached = "no"
            speed = "0"
            for(j=i;j<=NF;j++) {
                if($j == "all") {
                    size = $(j+1) " " $(j+2)
                }
                if($j ~ /cache,/ && $(j-1) != "0") {
                    cached = "yes"
                }
            }
            if(size != "") {
                print time, id, path, size, cached
            }
        }
    }
}' | sort -t'|' -k1,1r | uniq)

# Generate HTML report
cat > "$OUTPUT_FILE" << HTML
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>iOS Asset Cache Report</title>
    <style>
        body {
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
            line-height: 1.6;
            max-width: 1400px;
            margin: 0 auto;
            padding: 20px;
            background: #f5f5f7;
            color: #1d1d1f;
        }
        .header {
            margin-bottom: 30px;
            display: flex;
            justify-content: space-between;
            align-items: center;
        }
        .header-title {
            flex: 1;
        }
        .header-time {
            font-size: 14px;
            color: #86868b;
        }
        .stats-grid {
            display: grid;
            grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
            gap: 20px;
            margin-bottom: 30px;
        }
        .stat-card {
            background: white;
            padding: 20px;
            border-radius: 10px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
        }
        .stat-card h3 {
            margin: 0 0 10px 0;
            color: #86868b;
            font-size: 14px;
            text-transform: uppercase;
            letter-spacing: 0.5px;
        }
        .stat-value {
            font-size: 24px;
            font-weight: 600;
            color: #1d1d1f;
            margin-bottom: 5px;
        }
        .stat-detail {
            font-size: 12px;
            color: #86868b;
        }
        .requests-container {
            background: white;
            border-radius: 10px;
            box-shadow: 0 1px 3px rgba(0,0,0,0.1);
            padding: 20px;
            overflow-x: auto;
        }
        .requests-table {
            width: 100%;
            border-collapse: collapse;
        }
        .requests-table th {
            text-align: left;
            padding: 12px;
            border-bottom: 1px solid #e6e6e6;
            color: #86868b;
            font-weight: 500;
            white-space: nowrap;
        }
        .requests-table td {
            padding: 12px;
            border-bottom: 1px solid #e6e6e6;
        }
        .badge {
            display: inline-block;
            padding: 4px 8px;
            border-radius: 12px;
            font-size: 12px;
            font-weight: 500;
        }
        .badge-cache {
            background: #e8f5e9;
            color: #2e7d32;
        }
        .badge-internet {
            background: #e3f2fd;
            color: #1976d2;
        }
        .badge-ipa {
            background: #fff3e0;
            color: #e65100;
        }
        .badge-ios {
            background: #f3e5f5;
            color: #7b1fa2;
        }
        .request-id {
            font-family: ui-monospace, monospace;
            font-size: 0.9em;
            color: #666;
        }
        .timestamp {
            font-family: ui-monospace, monospace;
            color: #666;
            white-space: nowrap;
        }
        .path-cell {
            max-width: 400px;
            overflow: hidden;
            text-overflow: ellipsis;
            white-space: nowrap;
        }
        .refresh-time {
            color: #86868b;
            font-size: 14px;
            margin-top: 20px;
            text-align: right;
        }
    </style>
</head>
<body>
    <div class="header">
        <div class="header-title">
            <h1>iOS Asset Cache Report</h1>
            <p>Monitoring IPA and iOS Software Updates</p>
        </div>
        <div class="header-time">
            Report generated: $TIMESTAMP
        </div>
    </div>
    
    <div class="stats-grid">
        <div class="stat-card">
            <h3>Total iOS Assets Served</h3>
            <div class="stat-value">$TOTAL_IOS_SERVED</div>
            <div class="stat-detail">Combined IPA and iOS update traffic</div>
        </div>
        <div class="stat-card">
            <h3>From Cache</h3>
            <div class="stat-value">$TOTAL_IOS_CACHE</div>
            <div class="stat-detail">Bandwidth saved from local cache</div>
        </div>
        <div class="stat-card">
            <h3>From Internet</h3>
            <div class="stat-value">$TOTAL_IOS_INTERNET</div>
            <div class="stat-detail">Downloaded from Apple servers</div>
        </div>
    </div>

    <div class="requests-container">
        <table class="requests-table">
            <thead>
                <tr>
                    <th>Time</th>
                    <th>Request ID</th>
                    <th>Type</th>
                    <th>Path</th>
                    <th>Size</th>
                    <th>Source</th>
                </tr>
            </thead>
            <tbody>
HTML

# Add request rows to HTML
echo "$REQUESTS" | grep -v "pending" | while IFS='|' read -r time id path size cached; do
    # Determine asset type
    if [[ $path == *".ipa"* ]]; then
        asset_type="IPA"
        type_badge="badge-ipa"
    elif [[ $path == *"MobileAsset_SoftwareUpdate"* ]]; then
        asset_type="iOS Update"
        type_badge="badge-ios"
    else
        continue
    fi
    
    cat >> "$OUTPUT_FILE" << ROW
                <tr>
                    <td class="timestamp">$(echo "$time" | cut -d' ' -f2)</td>
                    <td class="request-id">$id</td>
                    <td><span class="badge $type_badge">$asset_type</span></td>
                    <td class="path-cell">$path</td>
                    <td>$size</td>
                    <td>
                        <span class="badge ${cached:+badge-cache}${cached:-badge-internet}">
                            ${cached:+Cache}${cached:-Internet}
                        </span>
                    </td>
                </tr>
ROW
done

# Complete HTML file
cat >> "$OUTPUT_FILE" << HTML
            </tbody>
        </table>
    </div>
</body>
</html>
HTML

# Open the report in default browser
open "$OUTPUT_FILE" 2>/dev/null || echo "Report generated at: $OUTPUT_FILE"

Phone Log Analysis

If you wish to check the phone, as you have no access to the server, then you need to get a application like HTTP Catcher which you can find here

When this is installed, start the application which will setup a VPN connection to monitor all the data sent from your phone, then simply download an App Store application and when you review the log within the application you will see an entry like this:

Note : Behind the red box you will get the internal IP of your MacOS caching server, rather than the Apple CDN address:


This them proves you are downloading your data from the local MacOS server and not from the Internet.
Previous Post Next Post

نموذج الاتصال