EST. 2026 — บันทึกงานเซิร์ฟเวอร์จริง SYSADMIN · SECURITY · AI · DIRECTADMIN เล่มที่ 01
THE SYSADMIN ALMANAC
หน้าหลัก / Linux / เปิดตอน login สองทาง เลยได้ daemon สองตัวเขียนไฟล์เดียวกัน
Linux

เปิดตอน login สองทาง เลยได้ daemon สองตัวเขียนไฟล์เดียวกัน

โดย Ton-Or ·13 พ.ย. 2026·อ่าน 10 นาที
เปิดตอน login สองทาง เลยได้ daemon สองตัวเขียนไฟล์เดียวกัน

pidfile คือการถามแล้วค่อยจด ระหว่างสองจังหวะนั้นมีช่องว่างเสมอ ถ้ามีงานช้า ๆ มาคั่นอยู่ตรงกลาง ช่องว่างนั้นกว้างพอให้โปรเซสอีกตัวเดินผ่านเข้ามาได้ทั้งตัว

สรุปใน 1 นาที

อาการ/ปัญหา
เดมอนถูกเปิดตอน login จากสองทาง pidfile ว่างทั้งคู่ เลยรันคู่กันทั้งวัน
ใครควรอ่าน
คนที่ทำเดมอนบน Windows ซึ่งต้องรันได้ตัวเดียว
ทางแก้แบบสั้น
pidfile ถามแล้วค่อยจด มีช่องว่างเสมอ ให้เปลี่ยนไปใช้ byte-range lock ของระบบปฏิบัติการ
ข้ามไปขั้นตอน

พอดีผมมีเดมอนเก็บข้อมูลเวลาจริงตัวหนึ่งรันอยู่บนเครื่อง Windows หน้าที่ของมันคือต่อเข้ากับโปรแกรมต้นทางข้อมูล ดึงข้อมูลมาเป็นรอบ แล้วรวมลงไฟล์รายวัน วันที่ 3 สิงหาคม เครื่องนั้นมีเดมอนตัวเดียวกันนี้รันอยู่สองตัวตลอดทั้งวัน เขียนลงที่เก็บเดียวกัน ทั้งที่ในโค้ดมีตัวกันรันซ้อนแบบ pidfile อยู่แล้ว ผมเลยต้องย้อนกลับไปดูว่าตัวกันที่เขียนไว้มันกันอะไรได้จริงบ้าง

ชื่อไฟล์ ชื่องาน และชื่อฟังก์ชันในบทนี้เปลี่ยนเป็นชื่อกลางทั้งหมด ส่วนลำดับเหตุการณ์ ตัวเลขเวลา และโครงของโค้ดเป็นของจริง

อาการ: เดมอนสองตัว เขียนไฟล์รายวันไฟล์เดียวกัน

เดมอนตัวนี้เขียนไฟล์แบบ merge แล้ว replace คืออ่านไฟล์ของวันนั้นขึ้นมา รวมกับข้อมูลรอบใหม่ เขียนลงไฟล์ชั่วคราว แล้วสลับชื่อทับไฟล์เดิม วิธีนี้ปลอดภัยดีตราบใดที่มีผู้เขียนคนเดียว พอมีสองตัวทำแบบเดียวกันกับไฟล์เดียวกัน ตัวที่ replace ทีหลังจะทับผลของตัวที่ replace ก่อน แถวข้อมูลที่ตัวแรกเพิ่งรวมเข้าไปแต่ตัวหลังยังไม่มีอยู่ในมือ จะหายไปในการเขียนรอบถัดไปโดยไม่มี error อะไรเลย เพราะในสายตาของแต่ละตัว การเขียนของมันสำเร็จทุกครั้ง

รันซ้อนไม่ได้พังแบบมี error

เดมอนสองตัวที่เขียนแบบ merge แล้ว replace ไม่ชนกันให้เห็น ไฟล์ไม่เสีย เปิดอ่านได้ปกติ ทุกรอบ log ขึ้นว่าเขียนสำเร็จ สิ่งที่เสียคือแถวของตัวที่แพ้ซึ่งหายไปเงียบ ๆ ในรอบถัดไป ความเสียหายแบบนี้ไม่มีอะไรมาเตือน ต้องกันที่ต้นทางคือไม่ให้ตัวที่สองได้เริ่มทำงานเลย

ทางที่เปิดเดมอนตอน login บนเครื่องนี้มีมากกว่าหนึ่งทาง ทั้งทางลัดในโฟลเดอร์ Startup สคริปต์ autostart และงานใน Task Scheduler วันเกิดเหตุมีสองทางยิงห่างกันราว 20 วินาที เรื่องว่าทำไมถึงมีผู้เปิดสองราย และควรให้ใครเป็นเจ้าของการเปิด ผมแยกไปเขียนไว้ใน watchdog กับ autostart แย่งกันเปิดเดมอน บทนี้จะอยู่กับคำถามเดียวคือ ถึงจะถูกเปิดสองครั้ง ทำไมตัวกันรันซ้อนถึงปล่อยผ่าน

ตัวกันรุ่นเดิม: อ่าน pid แล้วค่อยเขียน

ตัวกันรุ่นเดิมเป็นแบบที่เห็นกันทั่วไป โครงโดยย่อเป็นแบบนี้ ผมเขียนใหม่ให้เห็นรูป ไม่ได้ยกมาทั้งฟังก์ชัน

# รุ่นเดิม (โครงโดยย่อ) - อ่านก่อน แล้วค่อยเขียน
import os
from pathlib import Path

def single_instance_old(pidfile: Path) -> bool:
    if pidfile.exists():
        text = pidfile.read_text().strip()
        if text and pid_is_running(int(text.split()[0])):   # (1) ถาม
            return False
    pidfile.write_text(str(os.getpid()))                    # (2) จด
    return True

อ่านแล้วไม่มีอะไรผิด ถ้ามีไฟล์และ pid ในนั้นยังมีชีวิต ก็ถอย ถ้าไม่มีก็จด pid ของตัวเองลงไป ผมเชื่อฟังก์ชันนี้มาตลอดเพราะทดสอบด้วยมือแล้วมันกันได้จริง เปิดตัวแรกทิ้งไว้ เปิดตัวที่สองตาม ตัวที่สองออกทันที ที่ผมไม่ได้คิดคือการทดสอบด้วยมือแบบนั้นมีระยะห่างระหว่างสองตัวเป็นนาที ตัวแรกจดเสร็จไปนานแล้วก่อนตัวที่สองจะถาม

ตำแหน่งที่เรียกฟังก์ชันนี้ก็สำคัญพอกัน รุ่นเดิมเรียกหลังจากต่อกับโปรแกรมต้นทางเสร็จแล้ว ด้วยเหตุผลที่ฟังขึ้นในตอนนั้นว่า ถ้าต่อไม่ติดก็ไม่ต้องไปจองอะไรให้เสียเวลา

ตัวจริง: ทั้งคู่ค้างใน attach ราว 30 วินาที แล้วออกมาเช็กพร้อมกัน

ขั้นตอนต่อเข้ากับโปรแกรมต้นทางเป็นการเรียกที่ block ตอนเครื่องเพิ่ง login โปรแกรมต้นทางยังเปิดไม่เสร็จ การเรียกนี้เลยค้างอยู่ราว 30 วินาทีจนกว่าอีกฝั่งจะตอบ เดมอนตัวแรกเข้าไปค้างในนั้น อีกราว 20 วินาทีต่อมาตัวที่สองถูกเปิดแล้วเข้าไปค้างในจุดเดียวกัน ตลอดช่วงนี้ pidfile ยังว่าง เพราะยังไม่มีใครเดินไปถึงบรรทัดที่จด

พอโปรแกรมต้นทางพร้อม ผมเข้าใจว่ามันตอบทั้งสองตัวในจังหวะใกล้กันมาก ทั้งคู่หลุดออกจาก attach แล้วเดินมาเช็ก pidfile ในมิลลิวินาทีเดียวกัน ต่างคนต่างเห็นว่าไฟล์ว่าง ต่างคนต่างจด pid ของตัวเอง แล้วต่างคนต่างเข้า loop หลัก ตัวที่จดทีหลังทับ pid ของตัวแรก ไฟล์เลยมี pid เดียวตามปกติ ดูจากไฟล์ไม่มีทางรู้ว่ามีสองตัว

รุ่นเดิม: attach ก่อน แล้วค่อยเช็ก pidfileตัวที่ 1ค้างใน attach (pidfile ยังว่าง)เช็ก: ว่าง -> เขียน pid -> รันตัวที่ 2ค้างใน attachเช็ก: ว่าง -> เขียน pid -> รันt = 0+20 วินาทีพ้น attach พร้อมกันผล: เดมอน 2 ตัวเขียนที่เก็บเดียวกันทั้งวันรุ่นแก้: คว้า byte lock ก่อน attachตัวที่ 1ล็อกได้attach ราว 30 วินาทีรันตัวเดียวตัวที่ 2ล็อกไม่ได้ -> exitkernel ตัดสินเอง ไม่มีช่องระหว่างเช็กกับเขียนไบต์ที่ล็อกอยู่ที่ offset 1<<30 บรรทัด pid ที่ offset 0 ยังอ่านได้
ผังไม่ตามสเกล รุ่นเดิมช่วงค้างใน attach ทำให้การเช็กของสองตัวมาตกในจังหวะเดียวกัน รุ่นแก้ย้ายการจองมาไว้ก่อนงานที่ช้า และให้ระบบปฏิบัติการเป็นผู้ตัดสิน

อาการแบบนี้มีชื่อเรียกว่า TOCTOU คือเวลาที่เช็กกับเวลาที่ใช้ผลของการเช็กเป็นคนละจังหวะกัน ระหว่างบรรทัด (1) กับบรรทัด (2) ของรุ่นเดิมไม่มีอะไรรับประกันว่าโลกจะไม่เปลี่ยน ในการทดสอบด้วยมือช่องนี้แคบเป็นไมโครวินาทีเลยไม่เคยโดน แต่งานช้า 30 วินาทีที่วางไว้ก่อนหน้าทำหน้าที่เหมือนประตูกั้น มันรวบโปรเซสที่มาถึงต่างเวลากันให้ออกตัวพร้อมกัน ช่องที่แคบมากเลยถูกเล็งตรงเป้าทุกครั้งที่เครื่องบูต

ทำไมแก้ pidfile ให้ดีขึ้นแล้วก็ยังไม่จบ

ความคิดแรกของผมคือย้ายการเช็กขึ้นมาไว้ก่อน attach แค่นั้นน่าจะพอ ซึ่งช่วยได้จริงสำหรับเคสนี้ เพราะสองตัวห่างกัน 20 วินาที ตัวแรกจดเสร็จก่อนตัวที่สองเกิด แต่ช่องระหว่างถามกับจดยังอยู่ที่เดิม แค่กลับไปแคบเหมือนเก่า ถ้าวันหนึ่งผู้เปิดสองรายยิงห่างกันไม่กี่มิลลิวินาทีก็โดนอีก

pidfile ยังมีปัญหาอีกสองข้อที่ผมเลี่ยงมาตลอดด้วยโค้ดเสริม ข้อแรกคือไฟล์ค้าง ถ้าเดมอนถูกฆ่าหรือเครื่องดับ ไฟล์ยังอยู่พร้อม pid เก่า ต้องมีโค้ดไปตรวจว่า pid นั้นยังมีชีวิตไหม ข้อสองคือ pid ถูกใช้ซ้ำ เลขเดิมอาจกลายเป็นโปรแกรมอื่นไปแล้ว ตัวตรวจเลยตอบว่ายังอยู่ทั้งที่ไม่ใช่ ทุกชั้นที่เสริมเข้าไปคือการถามเพิ่มอีกรอบ ซึ่งก็คือช่องว่างเพิ่มอีกช่อง

pidfile

ถามเอง จดเอง เก็บกวาดเอง

การได้สิทธิ์เป็นสองขั้นแยกกัน มีช่องระหว่างกลางเสมอ โปรเซสตายแล้วไฟล์ยังค้าง ต้องตรวจ pid ซ้ำ และเลข pid ถูกนำกลับมาใช้ได้ ข้อดีอย่างเดียวคือเปิดไฟล์อ่านได้ว่าใครถืออยู่

byte-range lock

ให้ kernel เป็นคนตัดสิน

การขอล็อกเป็นขั้นเดียว ได้หรือไม่ได้รู้ทันที ไม่มีจังหวะกลาง โปรเซสตายด้วยสาเหตุอะไรก็ตาม kernel ปล่อยล็อกให้เอง ไม่มีของค้าง และยังเขียน pid ลงไฟล์เดียวกันไว้ให้คนอ่านได้ด้วย

แก้: ล็อก 1 ไบต์ที่ offset 1<<30 ให้ kernel ถือให้

รุ่นที่ใช้อยู่ตอนนี้เลิกถามเองทั้งหมด เปิดไฟล์ล็อกแล้วขอ byte-range lock แบบไม่รอ บน Windows ใช้ msvcrt.locking บนระบบตระกูล Unix ใช้ fcntl.flock ถ้าขอไม่ได้แปลว่ามีตัวอื่นถืออยู่ ก็ออก

import os
from datetime import datetime, timezone
from pathlib import Path

DATA_DIR = Path(os.environ.get("STORE_DIR", "store"))
_LOCK_FD = None                 # ถือไว้ตลอดอายุของโปรเซส
_LOCK_BYTE = 1 << 30            # ไบต์ที่ล็อก อยู่ไกลจากบรรทัด pid มาก

def single_instance(tag: str = "collector") -> bool:
    global _LOCK_FD
    path = DATA_DIR / f"{tag}.lock"
    try:
        DATA_DIR.mkdir(parents=True, exist_ok=True)
        fd = os.open(str(path), os.O_CREAT | os.O_RDWR)
    except OSError as e:
        print(f"[WARN] cannot open {path.name} ({e}) -- continuing without a lock")
        return True
    try:
        if os.name == "nt":
            import msvcrt
            os.lseek(fd, _LOCK_BYTE, os.SEEK_SET)
            msvcrt.locking(fd, msvcrt.LK_NBLCK, 1)      # 1 ไบต์ ไม่รอ
        else:
            import fcntl
            fcntl.flock(fd, fcntl.LOCK_EX | fcntl.LOCK_NB)
    except OSError:
        try:
            os.lseek(fd, 0, os.SEEK_SET)
            held = os.read(fd, 200).decode("utf-8", "replace").strip()
        except OSError:
            held = "?"
        os.close(fd)
        print(f"[WARN] another {tag} already holds {path.name} [{held}] -- exiting")
        return False
    os.lseek(fd, 0, os.SEEK_SET)
    os.ftruncate(fd, 0)
    os.write(fd, f"{os.getpid()} {datetime.now(timezone.utc).isoformat()}".encode())
    _LOCK_FD = fd               # ไม่ปิด ปล่อยเองเมื่อโปรเซสจบ
    return True

มีรายละเอียดสี่จุดที่ผมตั้งใจทำ

จุดแรกคือ offset ที่ล็อก byte-range lock บน Windows เป็นแบบบังคับ โปรเซสอื่นอ่านช่วงไบต์ที่ถูกล็อกไม่ได้ ถ้าผมล็อกไบต์แรกของไฟล์ ใครเปิดไฟล์ขึ้นมาดูว่า pid อะไรถืออยู่ก็จะอ่านไม่ออก ผมเลยเลื่อนไปล็อก 1 ไบต์ที่ offset 1 << 30 ซึ่งอยู่เลยท้ายไฟล์ไปไกล Windows ยอมให้ล็อกช่วงที่เลยท้ายไฟล์ได้ และขนาดไฟล์ไม่โตตาม บรรทัด pid ที่ offset 0 เลยยังอ่านได้ตามปกติ

จุดที่สองคือ LK_NBLCK กับ LOCK_NB ต้องเป็นแบบไม่รอเสมอ ตัวที่มาทีหลังต้องรู้ผลทันทีแล้วออก ถ้าใช้แบบรอ มันจะค้างอยู่เป็นโปรเซสที่สองซึ่งพร้อมจะรับช่วงทันทีที่ตัวแรกตาย ฟังดูดีแต่ทำให้การเปิดคืนไปอยู่นอกสายตาของตัวเฝ้า

จุดที่สามคือ _LOCK_FD ถูกเก็บไว้ในตัวแปรระดับโมดูลและไม่มีที่ไหนปิดมัน ล็อกผูกกับ file descriptor ถ้าปล่อยให้เป็นตัวแปรในฟังก์ชันแล้วมีโค้ดส่วนไหนปิด fd นั้น ล็อกจะหลุดโดยที่เดมอนยังรันอยู่ การปล่อยล็อกมีทางเดียวคือโปรเซสจบ ไม่ว่าจะจบดี ถูกฆ่า หรือเครื่องดับ kernel เก็บให้ทั้งหมด ไม่มีไฟล์ค้างให้ตามล้าง

จุดที่สี่คือสาขาที่เปิดไฟล์ล็อกไม่ได้ ผมให้มันเตือนแล้วทำงานต่อโดยไม่มีล็อก เป็นการเลือกว่าถ้าดิสก์หรือสิทธิ์มีปัญหาจนสร้างไฟล์ล็อกไม่ได้ ขอให้เก็บข้อมูลต่อไปดีกว่าหยุดทั้งตัว ใครที่ความเสียหายจากการรันซ้อนแพงกว่าการหยุดเก็บ ควรกลับสาขานี้เป็น return False

flock กับ msvcrt.locking ไม่เหมือนกันทุกอย่าง

fcntl.flock ล็อกทั้งไฟล์และเป็นแบบแนะนำ โปรเซสอื่นยังอ่านเขียนไฟล์ได้ถ้าไม่ได้ขอล็อกเอง เรื่อง offset เลยไม่เกี่ยว ส่วน msvcrt.locking ล็อกเป็นช่วงไบต์นับจากตำแหน่งปัจจุบันของไฟล์ และกันการอ่านเขียนช่วงนั้นจริง สองฝั่งให้ผลที่ต้องการเหมือนกันคือมีผู้ถือได้คนเดียวและปล่อยเองเมื่อโปรเซสจบ แต่รายละเอียดรอบข้างต่างกัน อย่าเอาสมมติฐานของฝั่งหนึ่งไปใช้กับอีกฝั่ง

ต้องคว้าล็อกก่อน attach เสมอ

ล็อกที่ถูกต้องแต่วางผิดที่ก็ยังทิ้งเวลา 30 วินาทีไว้ให้สองตัวไปค้างด้วยกัน ถึงผลสุดท้ายจะมีตัวเดียวได้ล็อก แต่ตัวที่แพ้ได้ต่อเข้ากับโปรแกรมต้นทางไปแล้วหนึ่งรอบ ซึ่งไม่ควรเกิด ลำดับใน main เลยเป็นแบบนี้

def main() -> int:
    args = parse_args()
    one_shot = args.once or args.backfill is not None

    # จองก่อนงานที่ช้า - attach ค้างได้ราว 30 วินาที
    if not one_shot and not single_instance():
        return 0

    if not attach_source():
        return 1
    return run_forever(args)
ลำดับการเริ่มที่ใช้อยู่อ่านอาร์กิวเมนต์ไม่แตะของนอก->single_instanceได้หรือออก ทันที->attachค้างได้ 30 วินาที->อ่านสถานะเดิมต่อจากของล่าสุด->loop หลักผู้เขียนคนเดียว

โหมดรันรอบเดียวกับโหมดเติมข้อมูลย้อนหลังได้รับยกเว้น ไม่ต้องขอล็อก เพราะสองโหมดนี้ผมสั่งด้วยมือในขณะที่เดมอนหลักรันอยู่ ถ้าบังคับล็อกมันจะสั่งไม่ได้เลย ข้อยกเว้นนี้แลกมาด้วยความรับผิดชอบของคนสั่งว่าต้องรู้ว่ากำลังเขียนช่วงไหน

ค่า return ของตัวที่แพ้เป็น 0 ไม่ใช่รหัสผิดพลาด เพราะการที่มีตัวอื่นรันอยู่แล้วคือสถานะปกติของระบบ ถ้าคืนรหัสผิดพลาด ตัวเปิดงานจะมองว่างานล้มแล้วพยายามเปิดซ้ำ

ทดสอบโดยบีบระยะห่างให้แคบ

บทเรียนจากรุ่นเดิมคือการทดสอบด้วยมือที่ห่างกันเป็นนาทีไม่ได้พิสูจน์อะไร ต้องทดสอบในระยะที่การแข่งเกิดจริง

  1. เปิดเดมอนสองตัวจากสคริปต์เดียวให้ห่างกันน้อยที่สุด แล้วนับ process ที่เหลือหลังผ่านช่วง attach ต้องเหลือ 1
  2. ดู log ของตัวที่แพ้ ต้องมีบรรทัด another collector already holds พร้อม pid ของตัวที่ถืออยู่
  3. เปิดไฟล์ล็อกอ่านจากอีกหน้าต่างขณะเดมอนรัน ต้องเห็น pid กับเวลาเริ่ม ถ้าอ่านไม่ได้แปลว่าล็อกผิด offset
  4. ฆ่าเดมอนด้วย taskkill แบบบังคับ แล้วเปิดใหม่ทันที ต้องขึ้นได้โดยไม่ต้องลบไฟล์อะไร
  5. เปิดโหมดรันรอบเดียวขณะเดมอนหลักรันอยู่ ต้องทำงานได้ตามข้อยกเว้น
  6. logoff แล้ว login ใหม่ ปล่อยให้ผู้เปิดทั้งสองทางยิงตามจริง แล้วนับ process อีกครั้ง
# ยิงสองตัวติดกัน แล้วนับหลังพ้นช่วง attach
1..2 | ForEach-Object {
    Start-Process python -ArgumentList 'collector.py' -WindowStyle Minimized
}
Start-Sleep -Seconds 45
@(Get-CimInstance Win32_Process -Filter "Name='python.exe'" |
  Where-Object { $_.CommandLine -like '*collector.py*' }).Count    # ต้องได้ 1
Get-Content .\store\collector.lock                                # pid + เวลาเริ่ม
ล็อกคุ้มครองเฉพาะคนที่ขอล็อก

ตัวกันนี้กันได้เฉพาะโปรเซสที่เรียก single_instance ด้วย tag เดียวกันและชี้ไปที่เก็บเดียวกัน สคริปต์ตัวอื่นที่เขียนไฟล์รายวันโดยตรง หรือเดมอนสำเนาที่ถูกตั้งค่าที่เก็บผิดจนไปชี้ทับกัน ไม่ถูกกันด้วยล็อกนี้ ผมวางไฟล์ล็อกไว้ในที่เก็บเองแทนที่จะวางในโฟลเดอร์ชั่วคราว เพื่อให้ขอบเขตของล็อกเท่ากับขอบเขตของสิ่งที่มันปกป้อง

ตัวเลขของเรื่องนี้

2 ทางที่ยิงเปิดในวันเกิดเหตุ
20 วินาทีระยะห่างของสองทาง
30 วินาทีที่ค้างอยู่ใน attach
2 ตัวรันคู่กันทั้งวัน
1 ไบต์ขนาดของช่วงที่ล็อก
1<<30offset ของไบต์ที่ล็อก
0 ไฟล์ที่ค้างหลังโปรเซสตาย
2 โหมดที่ยกเว้นไม่ต้องขอล็อก

เรื่องนี้เป็นคนละชั้นกับงานตามเวลาที่ไม่ถูกเปิดเลยหลังเครื่องดับ ซึ่งผมเขียนไว้ใน ไฟดับ 25 ชั่วโมง แล้วงานตามเวลาที่พลาดไปหายไปเลย บทนั้นคืองานถูกเปิดน้อยไป บทนี้คือถูกเปิดมากไป ส่วนตัวการเขียนไฟล์แบบสลับชื่อทับของเดมอนตัวเดียวกันนี้ก็มีเรื่องของมันเอง อยู่ใน os.replace ล้มบน Windows ทั้งที่ไฟล์เป็นของเรา

สรุปคือตัวกันรันซ้อนต้องเป็นการขอครั้งเดียวที่ระบบปฏิบัติการตัดสินให้ ไม่ใช่การถามแล้วค่อยจดด้วยโค้ดของเราเอง และต้องวางไว้ก่อนงานที่ช้าที่สุดของขั้นตอนเริ่มต้น ส่วนตัวผมใช้ไฟล์ล็อกเพราะได้ทั้งล็อกและบรรทัด pid ไว้อ่านในที่เดียว ใครจะใช้ named mutex ของ Windows แทนก็ได้ผลเรื่องผู้ถือคนเดียวเหมือนกัน ตามสะดวกครับ ตรงไหนผมอธิบายตกหล่นหรือพิมพ์ผิด เดี๋ยวกลับมาแก้เพิ่มให้ภายหลังครับ

อยากให้เราทำให้ไหม?

ให้เราช่วยติดตั้ง ตั้งค่า ย้ายระบบ หรือแก้ปัญหาเซิร์ฟเวอร์ เพื่อให้คุณมีเวลาไปทำงานที่สำคัญกว่า

คุยเรื่องงานกับเรา

เคยเจองานแบบนี้ไหม? ติดตรงไหน?

มาแลกวิธีกันต่อในบอร์ด LinuxThai

→ คุยต่อในบอร์ด

ความคิดเห็น 0

ยังไม่มีความคิดเห็น — เป็นคนแรกเลย