290 lines
7.5 KiB
Python
290 lines
7.5 KiB
Python
#!/usr/bin/env python3
|
|
"""
|
|
Minimal Flask app to isolate the crash.
|
|
Run with: python minimal_flask_crash.py
|
|
|
|
Then test with:
|
|
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1
|
|
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test2
|
|
curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test3
|
|
"""
|
|
|
|
import io
|
|
import gc
|
|
import os
|
|
import sys
|
|
import tempfile
|
|
|
|
# Minimal imports first
|
|
from flask import Flask, request, jsonify
|
|
from PIL import Image
|
|
import numpy as np
|
|
|
|
app = Flask(__name__)
|
|
app.config['MAX_CONTENT_LENGTH'] = 50 * 1024 * 1024 # 50MB
|
|
|
|
# Check for jpegio
|
|
try:
|
|
import jpegio as jio
|
|
HAS_JPEGIO = True
|
|
print("jpegio: available")
|
|
except ImportError:
|
|
HAS_JPEGIO = False
|
|
print("jpegio: NOT available")
|
|
|
|
|
|
@app.route('/test1', methods=['POST'])
|
|
def test1_pil_only():
|
|
"""Test 1: PIL only, no jpegio, no scipy"""
|
|
carrier = request.files.get('carrier')
|
|
if not carrier:
|
|
return jsonify({'error': 'No carrier'}), 400
|
|
|
|
data = carrier.read()
|
|
print(f"[test1] Read {len(data)} bytes")
|
|
|
|
img = Image.open(io.BytesIO(data))
|
|
width, height = img.size
|
|
fmt = img.format
|
|
img.close()
|
|
print(f"[test1] Image: {width}x{height} {fmt}")
|
|
|
|
gc.collect()
|
|
print("[test1] Returning response...")
|
|
|
|
return jsonify({
|
|
'test': 'pil_only',
|
|
'width': width,
|
|
'height': height,
|
|
'format': fmt,
|
|
})
|
|
|
|
|
|
@app.route('/test2', methods=['POST'])
|
|
def test2_multiple_opens():
|
|
"""Test 2: Open image multiple times like compare_modes does"""
|
|
carrier = request.files.get('carrier')
|
|
if not carrier:
|
|
return jsonify({'error': 'No carrier'}), 400
|
|
|
|
data = carrier.read()
|
|
print(f"[test2] Read {len(data)} bytes")
|
|
|
|
# First open
|
|
img1 = Image.open(io.BytesIO(data))
|
|
width, height = img1.size
|
|
img1.close()
|
|
print(f"[test2] Open 1: {width}x{height}")
|
|
|
|
# Second open
|
|
img2 = Image.open(io.BytesIO(data))
|
|
pixels = img2.size[0] * img2.size[1]
|
|
img2.close()
|
|
print(f"[test2] Open 2: {pixels} pixels")
|
|
|
|
# Third open
|
|
img3 = Image.open(io.BytesIO(data))
|
|
blocks = (img3.size[0] // 8) * (img3.size[1] // 8)
|
|
img3.close()
|
|
print(f"[test2] Open 3: {blocks} blocks")
|
|
|
|
gc.collect()
|
|
print("[test2] Returning response...")
|
|
|
|
return jsonify({
|
|
'test': 'multiple_opens',
|
|
'width': width,
|
|
'height': height,
|
|
'pixels': pixels,
|
|
'blocks': blocks,
|
|
})
|
|
|
|
|
|
@app.route('/test3', methods=['POST'])
|
|
def test3_with_jpegio():
|
|
"""Test 3: Include jpegio operations"""
|
|
if not HAS_JPEGIO:
|
|
return jsonify({'error': 'jpegio not available'}), 501
|
|
|
|
carrier = request.files.get('carrier')
|
|
if not carrier:
|
|
return jsonify({'error': 'No carrier'}), 400
|
|
|
|
data = carrier.read()
|
|
print(f"[test3] Read {len(data)} bytes")
|
|
|
|
# Check if JPEG
|
|
img = Image.open(io.BytesIO(data))
|
|
is_jpeg = img.format == 'JPEG'
|
|
width, height = img.size
|
|
img.close()
|
|
print(f"[test3] Image: {width}x{height}, JPEG: {is_jpeg}")
|
|
|
|
if not is_jpeg:
|
|
return jsonify({'error': 'Not a JPEG'}), 400
|
|
|
|
# Write to temp file
|
|
fd, temp_path = tempfile.mkstemp(suffix='.jpg')
|
|
os.write(fd, data)
|
|
os.close(fd)
|
|
print(f"[test3] Temp file: {temp_path}")
|
|
|
|
try:
|
|
# Read with jpegio
|
|
jpeg = jio.read(temp_path)
|
|
print(f"[test3] jpegio.read() OK")
|
|
|
|
coef = jpeg.coef_arrays[0]
|
|
coef_shape = coef.shape
|
|
print(f"[test3] Coef shape: {coef_shape}")
|
|
|
|
# Count positions like the real code does
|
|
positions = 0
|
|
h, w = coef.shape
|
|
for row in range(h):
|
|
for col in range(w):
|
|
if (row % 8 == 0) and (col % 8 == 0):
|
|
continue
|
|
if abs(coef[row, col]) >= 2:
|
|
positions += 1
|
|
print(f"[test3] Usable positions: {positions}")
|
|
|
|
# Cleanup
|
|
del coef
|
|
del jpeg
|
|
print(f"[test3] Deleted jpegio objects")
|
|
|
|
finally:
|
|
os.unlink(temp_path)
|
|
print(f"[test3] Removed temp file")
|
|
|
|
gc.collect()
|
|
print("[test3] Returning response...")
|
|
|
|
return jsonify({
|
|
'test': 'with_jpegio',
|
|
'width': width,
|
|
'height': height,
|
|
'coef_shape': list(coef_shape),
|
|
'positions': positions,
|
|
})
|
|
|
|
|
|
@app.route('/test4', methods=['POST'])
|
|
def test4_numpy_array_from_pil():
|
|
"""Test 4: Create numpy array from PIL image (like DCT does)"""
|
|
carrier = request.files.get('carrier')
|
|
if not carrier:
|
|
return jsonify({'error': 'No carrier'}), 400
|
|
|
|
data = carrier.read()
|
|
print(f"[test4] Read {len(data)} bytes")
|
|
|
|
img = Image.open(io.BytesIO(data))
|
|
width, height = img.size
|
|
print(f"[test4] Image: {width}x{height}")
|
|
|
|
# Convert to grayscale and numpy array
|
|
gray = img.convert('L')
|
|
arr = np.array(gray, dtype=np.float64, copy=True)
|
|
print(f"[test4] Array: {arr.shape} {arr.dtype}")
|
|
|
|
# Close PIL images
|
|
gray.close()
|
|
img.close()
|
|
print(f"[test4] PIL closed")
|
|
|
|
# Do some numpy operations
|
|
mean_val = float(np.mean(arr))
|
|
std_val = float(np.std(arr))
|
|
print(f"[test4] Stats: mean={mean_val:.2f}, std={std_val:.2f}")
|
|
|
|
# Clear array
|
|
del arr
|
|
gc.collect()
|
|
print("[test4] Returning response...")
|
|
|
|
return jsonify({
|
|
'test': 'numpy_from_pil',
|
|
'width': width,
|
|
'height': height,
|
|
'mean': mean_val,
|
|
'std': std_val,
|
|
})
|
|
|
|
|
|
@app.route('/test5', methods=['POST'])
|
|
def test5_file_read_keep_reference():
|
|
"""Test 5: Keep reference to file data in request scope"""
|
|
carrier = request.files.get('carrier')
|
|
if not carrier:
|
|
return jsonify({'error': 'No carrier'}), 400
|
|
|
|
# Don't read into local variable - read directly each time
|
|
# This mimics potential issues with Flask's file handling
|
|
|
|
print(f"[test5] File object: {carrier}")
|
|
|
|
# Read once
|
|
carrier.seek(0)
|
|
data1 = carrier.read()
|
|
print(f"[test5] First read: {len(data1)} bytes")
|
|
|
|
img = Image.open(io.BytesIO(data1))
|
|
width, height = img.size
|
|
img.close()
|
|
|
|
# Try to read again (should be empty or need seek)
|
|
data2 = carrier.read()
|
|
print(f"[test5] Second read (no seek): {len(data2)} bytes")
|
|
|
|
carrier.seek(0)
|
|
data3 = carrier.read()
|
|
print(f"[test5] Third read (after seek): {len(data3)} bytes")
|
|
|
|
gc.collect()
|
|
print("[test5] Returning response...")
|
|
|
|
return jsonify({
|
|
'test': 'file_handling',
|
|
'width': width,
|
|
'height': height,
|
|
'read1': len(data1),
|
|
'read2': len(data2),
|
|
'read3': len(data3),
|
|
})
|
|
|
|
|
|
@app.after_request
|
|
def after_request(response):
|
|
"""Log after each request"""
|
|
print(f"[after_request] Response status: {response.status}")
|
|
return response
|
|
|
|
|
|
@app.teardown_request
|
|
def teardown_request(exception):
|
|
"""Log during teardown"""
|
|
if exception:
|
|
print(f"[teardown] Exception: {exception}")
|
|
else:
|
|
print("[teardown] Clean teardown")
|
|
gc.collect()
|
|
|
|
|
|
if __name__ == '__main__':
|
|
print("\n" + "=" * 60)
|
|
print("MINIMAL FLASK CRASH TEST")
|
|
print("=" * 60)
|
|
print("\nTest endpoints:")
|
|
print(" /test1 - PIL only")
|
|
print(" /test2 - Multiple PIL opens")
|
|
print(" /test3 - With jpegio")
|
|
print(" /test4 - NumPy array from PIL")
|
|
print(" /test5 - File handling test")
|
|
print("\nUsage:")
|
|
print(' curl -X POST -F "carrier=@xx_2.jpg" http://localhost:5001/test1')
|
|
print("=" * 60 + "\n")
|
|
|
|
app.run(host='0.0.0.0', port=5001, debug=False, threaded=False)
|