9 जून 2026
macOSlaunchdAutomationDevOps

अपने Mac पर cron इस्तेमाल करना बंद करें: इसकी जगह launchd इस्तेमाल करें

cron एक शेड्यूल पर पोल करता है और आपके लैपटॉप के सोते समय जॉब्स मिस कर देता है। launchd की WatchPaths शेड्यूल की बजाय फ़ाइलसिस्टम इवेंट्स पर रिएक्ट करती है, और यह वह ऑटोमेशन है जिसने cron की जगह हमेशा के लिए ले ली।

9 मिनट पढ़ें

मुझे अपनी मशीन को बदलावों पर रिएक्ट करने की ज़रूरत थी, पोल करने की नहीं। जवाब macOS का एक 20 साल पुराना फ़ीचर निकला जिसे लगभग कोई इस्तेमाल नहीं करता, और यह हर उस मामले में cron से बेहतर निकला जो मायने रखता है।

पिछले हफ़्ते मैंने एक सिस्टम बनाया जो मेरी मशीन के कॉन्फ़िगरेशन को ऑटो-कमिट करता है: एक Homebrew पैकेज इंस्टॉल करो, और आठ सेकंड बाद GitHub पर उसे दर्ज करता एक git कमिट बन जाता है।

जब मैंने शुरू किया, मेरी मसल मेमोरी ने वही कहा जो हर डेवलपर की मसल मेमोरी कहती है:

बस इसे cron में डाल दो।

मैंने नहीं डाला। और इसकी वजहें मेरे सालों में सीखा सबसे काम का macOS सबक बन गईं।

⏰ पोलिंग की समस्या

मेरे ऑटोमेशन का cron वर्ज़न कुछ ऐसा दिखता है:

*/5 * * * * /Users/me/sync.sh

हर पांच मिनट पर पोल करता cron बनाम फ़ाइलसिस्टम इवेंट्स पर चलता launchd

हर पांच मिनट, हमेशा के लिए, मेरी मशीन एक प्रोसेस जगाकर पूछती है: कुछ बदला क्या?

लगभग हर बार, जवाब न होता है।

यही cron की बुनियादी शक्ल है: पोलिंग। इसे इवेंट्स के बारे में पता नहीं होता। इसे सिर्फ़ समय के बारे में पता होता है। तो आप एक बुरे समझौते में फंस जाते हैं:

  • बार-बार पोल करो → हर दिन सैकड़ों बेमतलब एक्ज़ीक्यूशन, बस एहतियातन
  • कभी-कभार पोल करो → बदलाव एक घंटे तक बिना कमिट रह जाते हैं, मकसद ही खत्म

और लैपटॉप पर cron की एक दूसरी, ज़्यादा चालाक समस्या भी है। cron मान लेता है कि मशीन चालू है। 11:58 पर लिड बंद करो, और दोपहर की जॉब बस होती ही नहीं। न रीट्राई, न कैच-अप, कुछ नहीं। सर्वर हमेशा चालू रहते हैं; लैपटॉप सोते हैं। cron सर्वरों के लिए डिज़ाइन किया गया था।

मैं जो असल में चाहता था वह कहना आसान था:

जब फ़ाइलसिस्टम बदले, मेरी स्क्रिप्ट चलाओ। वरना, कुछ भी मत करो।

यह कोई शेड्यूल नहीं है। यह एक इवेंट है।

⚙️ वह डेमन जो आपके पास पहले से है

macOS launchd के साथ आता है, यह सचमुच प्रोसेस 1 है, वह चीज़ जो आपके सिस्टम पर हर सर्विस को बूट करती है। Apple ने बरसों पहले अंदरूनी तौर पर cron की जगह इसे लगा दिया था (आपके Mac की cron जॉब्स खुद launchd द्वारा चलाई जाती हैं)।

ज़्यादातर डेवलपर्स जानते हैं कि launchd मौजूद है। लगभग कोई इसका सबसे अच्छा फ़ीचर इस्तेमाल नहीं करता:

WatchPaths, launchd OS लेवल पर एक फ़ाइलसिस्टम पाथ पर नज़र रखेगा, और बदलते ही आपकी जॉब चला देगा।

<key>WatchPaths</key>
<array>
    <string>/opt/homebrew/Cellar</string>
    <string>/opt/homebrew/Caskroom</string>
</array>

यही पूरी तरकीब है। Homebrew हर पैकेज को /opt/homebrew/Cellar में इंस्टॉल करता है, तो कोई भी इंस्टॉल या अनइंस्टॉल, किसी भी शेल से, किसी भी स्क्रिप्ट से, किसी भी टूल से, उस डायरेक्टरी को बदल देता है। OS नोटिस करता है। मेरी स्क्रिप्ट चलती है।

कोई पोलिंग नहीं। कोई बर्बाद एक्ज़ीक्यूशन नहीं। आइडल रहते ज़ीरो CPU। जॉब एक रजिस्टर्ड इरादे के रूप में मौजूद रहती है, और कर्नेल नज़र रखने का काम करता है।

पूरी जॉब डेफ़िनिशन एक plist फ़ाइल है:

<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
    <key>Label</key>
    <string>com.paurushrai.dotfiles-sync</string>
 
    <key>ProgramArguments</key>
    <array>
        <string>/bin/bash</string>
        <string>/Users/paurushrai/Developer/infra/dotfiles/sync.sh</string>
    </array>
 
    <key>WatchPaths</key>
    <array>
        <string>/opt/homebrew/Cellar</string>
    </array>
 
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key><integer>12</integer>
        <key>Minute</key><integer>0</integer>
    </dict>
 
    <key>ThrottleInterval</key>
    <integer>60</integer>
 
    <key>StandardOutPath</key>
    <string>/Users/paurushrai/Library/Logs/dotfiles-sync.log</string>
</dict>
</plist>

इसे ~/Library/LaunchAgents/ में रखो, एक बार रजिस्टर करो:

launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.paurushrai.dotfiles-sync.plist

यह रीबूट के बाद भी टिकी रहती है। यह चलती है चाहे कोई टर्मिनल खुला हो या न हो। यह अब OS का हिस्सा है।

मैंने इसे ईमानदार तरीक़े से टेस्ट किया, एक पैकेज इंस्टॉल किया और कुछ नहीं छुआ:

$ brew install jq
... 8 seconds later, zero shell involvement:
67ba27e chore: auto-sync .Brewfile  → pushed to GitHub ✅

brew install jq आठ सेकंड में एक ऑटो-कमिट ट्रिगर करके GitHub पर पुश करता है

🧩 तीन कीज़ जो भारी काम करती हैं

ऊपर के plist में तीन डिज़ाइन फ़ैसले बेक किए गए हैं, और हर एक ने वह कोड बदल दिया जो मुझे वरना खुद लिखना पड़ता।

तीन launchd कीज़: ThrottleInterval, StartCalendarInterval, और StandardOutPath

ThrottleInterval: 60 एक मुफ़्त debouncer है। लगातार पांच पैकेज इंस्टॉल करो और फ़ाइलसिस्टम पांच इवेंट्स फ़ायर करता है, लेकिन launchd जॉब को 60 सेकंड में एक से ज़्यादा बार नहीं चलाएगा, तो मुझे पांचों को कवर करने वाला एक कमिट मिलता है। cron की दुनिया में, इस तरह की बैचिंग वह लॉजिक है जो आप खुद लिखते और टेस्ट करते हैं। यहां यह एक इंटीजर है।

StartCalendarInterval cron का शेड्यूल है, बस cron की लैपटॉप समस्या के बिना। यह मुझे हर छूटी हुई चीज़ के लिए कैच-ऑल के तौर पर एक डेली दोपहर का रन देता है। और cron के उलट, अगर दोपहर में मशीन सो रही हो, तो launchd जॉब को जागने पर चलाता है। छूटी हुई जॉब्स कभी न होने की बजाय देर से होती हैं। लैपटॉप के लिए, यह अकेला बर्ताव तुलना खत्म कर देता है।

StandardOutPath मुफ़्त ऑब्ज़र्वेबिलिटी है। हर रन का आउटपुट मेरे चुने हुए लॉग फ़ाइल में जुड़ जाता है। जब मैंने सोचा कि कुछ क्यों नहीं चला, जवाब पहले से लिखा हुआ था।

🔁 वह जाल जो सबको काटता है

अब कबूलनामा। मेरी स्क्रिप्ट एक git रिपॉज़ में कमिट करती है, और मेरी पहली सोच रिपॉज़ की डायरेक्टरी को ही वॉच करने की थी। एक वॉच एंट्री, हर फ़ाइल कवर।

यह एक इनफ़ाइनाइट लूप भी है।

फ़ीडबैक लूप: एक कमिट .git को म्यूटेट करता है, जो एजेंट को फ़ायर करता है, जो फिर कमिट करता है

हर git कमिट .git/ को म्यूटेट करता है। तो: एक बदलाव एजेंट को फ़ायर करता है → एजेंट कमिट करता है → कमिट .git/ बदलता है → जो वॉच की गई डायरेक्टरी के अंदर है → जो एजेंट को फिर फ़ायर करता है। हमेशा के लिए।

फिक्स एक नियम है जो याद रखने लायक है:

कभी उस डायरेक्टरी को वॉच मत करो जिसमें आपकी जॉब लिखती है।

मैं इसकी बजाय अलग-अलग फ़ाइलों को वॉच करता हूं, हर ट्रैक की गई dotfile को अपनी WatchPaths एंट्री मिलती है, और रिपॉज़ की रूट जानबूझकर गायब है। ट्रेड-ऑफ़: किसी गहरी सबडायरेक्टरी में एक बिल्कुल नई फ़ाइल तुरंत नोटिस नहीं होती; डेली दोपहर का रन उसे बटोर लेता है। एक ऐसे सिस्टम के लिए वाजिब कीमत जो अपनी ही पूंछ नहीं पकड़ सकता।

उसी दोपहर के दो और जाल, ताकि आपको मेरी ट्यूशन दोबारा न चुकानी पड़े:

  • plist को सिमलिंक मत करो। मैं plist को अपने dotfiles रिपॉज़ में रखता हूं और उसे ~/Library/LaunchAgents/ में कॉपी करता हूं, launchd आधुनिक macOS पर सिमलिंक की गई जॉब डेफ़िनिशन के साथ भरोसेमंद नहीं है। रिपॉज़ सच का स्रोत रखता है; इंस्टॉलर एक कॉपी डिप्लॉय करता है।
  • launchd जॉब्स को न्यूनतम एनवायरनमेंट मिलता है। कोई आपके-शेल का PATH नहीं, कोई एलियस नहीं, आपकी .zshrc में से कुछ नहीं। मेरी स्क्रिप्ट पहली लाइन में अपना PATH सेट करती है। अगर आपकी जॉब "टर्मिनल में काम करती है पर launchd में नहीं", तो लगभग हमेशा यही वजह होती है।

🔔 इसे बोलने दो, फिर इसे रुकने दो

एक बैकग्राउंड जॉब जिसे आप देख नहीं सकते, वह जॉब है जिस पर आप भरोसा करना बंद कर देंगे।

मेरी स्क्रिप्ट हर कमिट को नेटिव macOS की एक लाइन से बताती है:

osascript -e 'display notification "Committed & pushed: .Brewfile" with title "dotfiles sync"'

पहला टेस्ट: मैंने इसे देखा ही नहीं। बैनर नोटिफ़िकेशन पांच सेकंड में गायब हो जाते हैं।

फिक्स कोड नहीं था, एक सेटिंग थी जिसे लगभग कोई नहीं बदलता: System Settings → Notifications → Script Editor → Alerts। अलर्ट-स्टाइल नोटिफ़िकेशन तब तक स्क्रीन पर रहते हैं जब तक खारिज न किए जाएं। अब हर सिंक कोने में मेरे मंज़ूर करने का इंतज़ार करता है, बदली हुई फ़ाइलों के नाम मैसेज में लिए हुए।

एक डिज़ाइन डीटेल जो मायने रखता है: स्क्रिप्ट सिर्फ़ तभी नोटिफ़ाई करती है जब वाकई कुछ कमिट हुआ हो। नो-ऑप रन ख़ामोश रहता है। लगातार बजने वाला नोटिफ़िकेशन वह नोटिफ़िकेशन है जिसे नज़रअंदाज़ करना आप पहले ही सीख चुके हैं, अलर्ट फ़टीग किसी भी अलर्ट न होने से ज़्यादा तेज़ी से ऑब्ज़र्वेबिलिटी को मार देती है।

🛠 चीट शीट

अपना बनाने के लिए जो कुछ चाहिए:

# register (once, or after editing the plist)
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.example.myjob.plist
# unregister
launchctl bootout gui/$(id -u)/com.example.myjob
# is it loaded?
launchctl list | grep myjob
# what happened?
tail -f ~/Library/Logs/myjob.log

और वह डिसिज़न टेबल जिसकी मुझे काश पहले से जानकारी होती:

  • जॉब को फ़ाइल या डायरेक्टरी बदलने पर रिएक्ट करना चाहिए → WatchPaths
  • जॉब को शेड्यूल पर, लैपटॉप-सेफ़ तरीक़े से चलना चाहिए → StartCalendarInterval
  • जॉब बर्स्ट में फ़ायर होती है जिसे बैच होना चाहिए → ThrottleInterval
  • जॉब को रजिस्टर होते ही तुरंत चलना चाहिए → RunAtLoad
  • आप Mac पर crontab -e टाइप करने ही वाले हैं → रुको, एक plist लिखो

🎯 सबक के पीछे का सबक

जो ऑटोमेशन मैंने बनाया वह छोटा है। सिद्धांत नहीं है।

मैं लगभग cron की तरफ़ गया क्योंकि cron वह था जो मैं जानता था, और मैं लगभग एक फ़ाइल-वॉचर npm पैकेज की तरफ़ गया क्योंकि सर्च रिज़ल्ट्स यही सुझाते हैं। दोनों काम कर जाते, पर बुरी तरह: एक टाइमर पर पोल करते हुए, दूसरा वह करने के लिए एक डिपेंडेंसी जोड़ते हुए जो OS पहले से नेटिवली करता है।

बोरिंग, बिल्ट-इन, बीस साल पुराना टूल दोनों से बेहतर निकला। पोल की बजाय इवेंट-ड्रिवन। सर्वर मान लेने की बजाय लैपटॉप-अवेयर। एक और चीज़ इंस्टॉल करने की बजाय ज़ीरो डिपेंडेंसीज़।

कोई टूल जोड़ने से पहले, पूछो कि OS पहले से क्या देता है।

जवाब अक्सर आपकी सोच से ज़्यादा होता है: वह सब कुछ जो आपको चाहिए था, प्रोसेस 1 में बैठा हुआ, 2005 से इंतज़ार कर रहा।

आपके LaunchAgents फ़ोल्डर में क्या चल रहा है?

मुझे जिज्ञासा है कि दूसरों ने OS लेवल पर क्या ऑटोमेट किया है, और ऐसा करते हुए किन जालों में फंसे। WatchPaths वाला फ़ीडबैक लूप अकेला तो नहीं हो सकता।

ब्लॉग से और

सभी देखें