ข้ามไปยังเนื้อหา

บทที่ 8 COM Lifecycle

ทำไมต้องจัดการ COM Lifecycle

COM objects ที่สร้างจาก comtypes ไม่ได้ถูกปล่อยอัตโนมัติ ตาม Python garbage collector ถ้าไม่ cleanup — COM reference จะค้างอยู่ทำให้:

  • ETABS process ค้าง ปิดไม่ได้หลังสคริปต์จบ
  • Memory Leak — ใช้ memory เพิ่มขึ้นเรื่อยๆ
  • RPC Error ในการรันครั้งต่อไป
%%{init: {
  "theme": "base",
  "flowchart": {
    "curve": "basis",
    "htmlLabels": true,
    "nodeSpacing": 42,
    "rankSpacing": 56,
    "padding": 16
  }
}}%%
flowchart LR
    P["Python<br/>creates COM ref"] --> C["COM<br/>ref count + 1"]
    C --> E["ETABS<br/>holds memory"]
    E --> D["del obj<br/>+ gc.collect"]
    D --> CF["COM<br/>ref count - 1"]
    CF --> F["ref = 0<br/>→ ETABS ปล่อย memory"]

วิธี Cleanup ใน Python

Pattern: del + gc.collect()

import gc
# ... ทำงานกับ API ...
# Cleanup — ลำดับสำคัญ: child → parent
del sap_model # ปล่อย SapModel ก่อน
del etabs # แล้วปล่อย ETABSObject
gc.collect() # บังคับ garbage collection ทันที

เปรียบเทียบ: C# vs Python Cleanup

หลักการC#Python
ปล่อย COMMarshal.FinalReleaseComObject(obj)del obj + gc.collect()
RAII PatternIDisposable + using blockContext Manager + with
Cleanup ลำดับchild → parent (เหมือนกัน)child → parent (เหมือนกัน)
GC Timingdeterministic (Dispose)non-deterministic (gc.collect)

Context Manager: ทางออกที่ดีที่สุด

สร้าง class ที่จัดการ lifecycle อัตโนมัติ:

import comtypes.client
import gc
class EtabsSession:
"""จัดการ ETABS COM lifecycle อัตโนมัติ
ใช้กับ with statement เพื่อ auto-cleanup
"""
def __init__(self, mode: str = "attach"):
"""
Args:
mode: "attach" = เชื่อม ETABS ที่เปิดอยู่
"create" = สร้าง ETABS ใหม่
"""
self.mode = mode
self._etabs = None
self._sap_model = None
@property
def etabs(self):
return self._etabs
@property
def sap_model(self):
return self._sap_model
def connect(self):
"""เชื่อม ETABS"""
if self.mode == "attach":
self._etabs = comtypes.client.GetActiveObject(
"CSI.ETABS.API.ETABSObject"
)
else:
self._etabs = comtypes.client.CreateObject(
"CSI.ETABS.API.ETABSObject"
)
self._etabs.ApplicationStart()
if self._etabs is None:
raise ConnectionError("ไม่สามารถเชื่อม ETABS ได้")
self._sap_model = self._etabs.SapModel
if self._sap_model is None:
raise ConnectionError("ไม่สามารถเข้าถึง SapModel ได้")
def close(self):
"""ปล่อย COM objects ตามลำดับที่ถูกต้อง"""
if self._sap_model is not None:
del self._sap_model
self._sap_model = None
if self._etabs is not None:
del self._etabs
self._etabs = None
gc.collect()
# === Context Manager ===
def __enter__(self):
self.connect()
return self
def __exit__(self, exc_type, exc_val, exc_tb):
self.close()
return False # ไม่ suppress exception

การใช้งาน

# Simple usage
with EtabsSession() as session:
model = session.sap_model
path = model.GetModelFilename()
print(f"📁 Model: {path}")
# ทำงานกับ API...
# ← close() เรียกอัตโนมัติเมื่อออกจาก with
# ← ถ้าเกิด exception ก็ยัง cleanup ให้

จัดการหลาย COM objects

with EtabsSession() as session:
model = session.sap_model
# เก็บ intermediate COM objects ไว้ใน list เพื่อ cleanup
com_objects = []
frame_api = model.FrameObj
com_objects.append(frame_api)
point_api = model.PointObj
com_objects.append(point_api)
# Cleanup intermediate objects
for obj in reversed(com_objects):
del obj
gc.collect()
# session cleanup จะจัดการ etabs + sap_model ให้

Best Practices

✅ ทำ

  • ใช้ with statement (EtabsSession) ทุกครั้ง
  • del objects ตามลำดับ child → parent
  • เรียก gc.collect() หลัง del ทุกตัว
  • เก็บ reference เฉพาะที่ต้องใช้ (ไม่สร้าง COM object เกินจำเป็น)

❌ อย่าทำ

  • ปล่อยให้ script จบโดยไม่ cleanup → ETABS process ค้าง
  • เก็บ COM reference ไว้ใน global variable
  • ส่ง COM object ข้าม thread (COM เป็น STA by default)

ตรวจว่า COM ปล่อยสำเร็จ

หลัง script จบ ตรวจใน Task Manager:

  • ถ้า ETABS.exe ยังค้างอยู่ (ทั้งที่ปิดหน้าต่างแล้ว) → มี COM leak
  • ให้ End Task แล้วตรวจโค้ดว่า del ครบหรือไม่