בהמשך למסורת רבת השנים (ברצינות, היא כבר רבת שנים!), הנה פוסט יומולדת שמוסיף עוד חוליה לרשימה המקושרת!

סטארקרפט עדיין מעסיק אותי ומונע פוסטים (מעולם לא שיחקתי במשחק אחד כל כך הרבה זמן).
אז הנה משחק סטרקרפט טוב במיוחד שראיתי לאחרונה בין שנים מהקוריאנים הטובים ביותר בעולם:

בסימן אחר, קניתי קיט להרכבת מדפסת תלת מימדית (ריפרפ מסוג ‘פרוסה מנדל’), ולמעשה קיבלתי את החבילה ביום של היומולדת.
התחלתי לבנות את המדפסת עם החברה אתמול, והנה כמה תמונות של מה שיש עד כה (וכמובן שיהיה עוד פוסט מלא על המדפסת, או כמה) :

לפני חמש וחצי שנים (ואו) כתבתי פוסט משוויץ על שדרוג שביצעתי כדי לשחק בOblivion.
מאז ביטים רבים זרמו בריישת, וממש עכשיו אני מתחיל את הPre-load של Skyrim בסטים.
סקיירים הוא המשחק הבא בסדרה של The elder’s scrolls, ולפי כל הסימנים הוא יהיה מדהים.

המשחק ישוחרר בעוד יום וחצי, יאי!

מסיבות היסטוריות, תמיד העדפתי את החומרה שלי אמיתית ולא וירטואלית.
בחודש האחרון יצא לי להתנסות בעבודה עם AWS – הלו הוא שרותי הווב של אמזון (Amazon Web Services) שכולל בין היתר את:

  • EC2 – הענן האלסטי של אמזון, שמאפשר להקצות מכונות וירטואליות על פי דרישה, שמריצות מה שבא לכם.
  • S3 – שרות אחסון הנתונים הוותיק של אמזון
  • RDS – שרותי בסיס נתונים, למעשה מכונת MySQL בניהול של אמזון, שכולל גם שרותי גיבוי, Read replica, סנאפ-שוטים של הנתונים ועוד.

מכונה וירטואלית בEC2 כוללת שטח אכסון מקומי (הרדיסקים לצורך העניין).
בעבר – מערכת הקבצים של המכונה ישבה על הדיסקים האלו. הImage של מערכות בשיטה הזו נקרא Instance store image, ואוכסנו בS3 – שרות איכסון הנתונים של אמזון.

אחת התכונות של הדיסקים המקומיים היא שכשהמכונה מוקצה מקבלים דיסקים ריקים, וכשהמכונה משוחררת (Terminated) המידע על הדיסקים האלו אובד לתמיד.
הדבר הזה גרם לקשיים אמיתיים להריץ בסיס נתונים על EC2, עד שאמזון פיתחו את הEBS (Elastic block store):
הEBS דומה במהות שלו למערכת איכסון מרכזית שמקצה דיסקים למכונות קצה, ולכן ביצועי הקריאה/כתיבה שלו תלויים בעומס עליו. במילים אחרות – אתם עשויים לסבול מביצועי קריאה/כתיבה ירודים כי משתמשים אחרים מתפרעים.
אם חשובים לכם ביצועי הIO, ואתם מוכנים לספוג אובדן של הנתונים אם המכונה משוחררת, שווה לההשתמש בדיסקים המקומיים, תקבלו ביצועיי IO צפויים יותר ובנוסף לא יהיה לכם תשלום לפי נפח הIO שאתם מבצעים (בניגוד לשימוש בEBS).
אפשר כמובן להגדיר אותם בתצורת  RAID-0 כדי לשפר את הביצועים

היתרון המרכזי ביותר לVolume של EBS הוא שמחזור החיים שלו נפרד מזה של המכונה אליה הוא מחובר: אם המכונה שאליה הוא מחובר משוחררת, הוא ממשיך להתקיים בנפרד ואפשר לחבר אותו למכונה אחרת (אבל רק לאחת בו זמנית כמובן).

EBS שמאפשר הקצאה של “דיסק” בצורה דינמית, וחיבור שלו למכונה בצורה דינאמית. בנוסף אפשר כמובן לשמור את מערכת הקבצים (root) של המכונה שם. Image כזה נקרא EBS Image, והוא עדיף ברוב הבחינות על הImage הוותיק יותר.
גם מכונות מבוססות EBS כוללות דיסקים מקומיים שתכלו לנצל אם תרצו (מה שאומר לפעמים שצריך לפרמט אותם).

אחד היתרונות המהותיים ביותר של ספקי ענן על פני ספק שרתים קלאסי הוא הזמן הנדרש כדי לקבל מכונה חדשה:

בספק קלאסי, מדובר בדרך כלל בכמה ימים לכל הפחות, שבהם הוא יזמין חומרה חדשה לפי דרישתכם, יתקין עליה מערכת הפעלה ויספק את המכונה.
בEC2, התהליך לוקח דקות, ואתם לא צריכים לשלוח אימייל לאף אחד, ניתן להקצות את המכונה מממשק הניהול של AWS, או בשימוש בSDK בשפה המועדפת עליכם.
מכיוון שהתהליך הוא כל כך אוטומטי, יש כמה דברים שאין לכם עליהם שליטה:
קודם כל, מאפייני החומרה מוגבלים למספר מצומצם של סוגי מכונות, ואתם צריכים לבחור את סוג המתאים ביותר.
בנוסף, המכונה תקבל כתובת IP פנימית וחיצונית שונה בכל שתקצו אותה, ושם המכונה יהיה מגעיל במיוחד (איזה חרא שמבוסס על כתובת הIP הפנימית).
אם אתם מגיעים כמוני מחוות שרתים קלאסית, זה יהיה מעצבן במיוחד. הצורך לשלוט על השם של מכונה הוא טריויאלי וחיוני במקרים רבים.

כשמקצים מכונה בEC2, חוץ מהפרטים הרגילים של סוג המכונה, איפה היא תהיה, על איזה אימג’ היא תתבסס וכו’ – אפשר גם להעביר מידע כללי כלשהו למכונה, שיהיה זמין לה על פי דרישה.
ספציפית אפשר להעביר למכונה את שם ההוסט שאנו רוצים שהיא תקבל, ואולי גם כל מני פרטים שיעזרו למכונה להגדיר את עצמה לפי הצורך בעליה הראשונה.
אם משתמשים בVPC – הלו הוא ענן וירטואלי פרטי, מקבלים כמה יתרונות, קודם כל אפשר לשלוט על תצורת הרשת של המכונות לחלוטין, מה שאומר שתוכלו לבחור כתובות IP, סאבנטים, שרת DNS וכו’.
כדי לשלוט גם על שם ההוסט של המכונה, המכונה מעדכנת DNS פנימי בשם ובכתובת הIP שלה (הDNS הוא מכונה וירטואלית בעצמו בתוך הVPC שלכם). מכיוון שכל המכונות שלכם מוגדרות להשתמש בDNS הזה, שם ההוסט החדש יהיה מוכר לכל המכונות האחרות אוטומטית ברגע שהמכונה עולה ומעדכנת את הDNS.

כל המכונות הוירטואליות שלי מבוססות על אותו IMAGE, למרות שהן מסוגים שונים ומשונים, יש לי שם:

  • מכונות DNS
  • מכונות Memcached
  • מכונות WEB
  • מכונות Gearman
  • מכונת HAProxy
  • ועוד

אז איך יתכן שכל המכונות מבוססות על אותו IMAGE?
כאמור, כשאני מקצה מכונה אני מעביר לה את ה”סוג” שלה בUSER-DATA.
כשהמכונה עולה, היא מעבירה את הסוג שלה לשרת תצורה מרכזי מסוג Puppet, שאומר למכונה מה לעשות כדי להפוך למה שהיא אמורה להפוך.
אפשר למלא כמה פוסטים רק על Puppet (ואפילו כתבתי עליו כבר פעם), אבל הפעם אני לא ארחיב.

כאשר יש לכם מערכת אלסטית של שרתים, שבה שרתים יכולים לבוא וללכת לפי דרישה – נוצרת בעיה של ניהול. למשל, אם תרצו להפעיל מחדש את כל שרתי האפ’אצי במכונות הWEB – איך תעשו את זה? בתור התחלה, איך תדעו בכלל איזה שרתים הם מכונות WEB ומה הכתובות שלהם?
אם תרצו לברר מה הגרסא של חבילה מסויימת בכל השרתים, איך תדעו מה כל השרתים?
פתרון אחד הוא  לנהל בסיס נתונים מרכזי שישמור מידע על השרתים. הפתרון הזה עובד, אבל הוא בעייתי : צריך לתחזק אותו, ויש לשים לב במיוחד למה קורה כשמכונה משוחררת.

פתרון אלטרנטיבי הוא להשתמש במערכת Marionette collective:
קונספטואלית, mcollective  היא נטולת שרת מרכזי.
יש שרת הודעות כללי (Message queue) מסוג ActiveMQ שמשמש לתקשורת בין המכונות. פלאגינים מיוחדים בשם Agents רצים על המכונות, ומאפשרים ביצוע פעולות שונות ומשונות עליהם.
הפעולות יכולות להיות מסוננות לפי “עובדות” שהמכונות יודעות על עצמן, במילים אחרות – מכונת WEB, שיודעת היא מכונת WEB, לא תבצע פעולה שמיועדת למכונת Gearman.
ההתקנה של MCollective היא לא מאוד מסובכת, ומה שיפה זה שהActiveMQ עובד, כל מכונה שעולה אוטומטית זמינה דרך הMCollective (כמובן – היא צריכה להריץ את השרת של MCollective, אבל כל זה חלק מהIMAGE הבסיסי.
שווה מאוד לראות את הסרטונים באתר של Mcollective כדי להבין במה מדובר.

אם לסכם, אני מרוצה מהמערכת שקמתי באמזון, ובגדול מהגמישות שנובעת מהתשתיות של אמזון.
יש לי עדיין לדאוג לכל העניין של איזורי הזמינות, גיבויים שוטפים וכו’.

אם תהיה דרישה, אולי אני אכתוב עוד כמה פוסטים שמפרטים לגבי חלקים ספציפיים מהתצורה שלי בAWS.

בעקבות הגעת NGN לאיזור שלי, שדרגתי את מהירות האינטרנט שלי ל15 מגה-ביט, באימייל “חוזה” שקיבלתי מבזק בין לאומי – ספק האינטרנט שלי – ראיתי את ההברקה הבאה:

@ גלישה ללא הגבלת זמן במהירות של עד 15.00 מס”ש קצב הורדה ו- 800 קס”ש קצב העלאה בהיקף העברת נתונים(הורדה והעלאה) של עד 65 ג’יגה בייט (GB) בחודש שירות (“מסגרת החבילה”).

העברת נתונים של עד 65 ג’יגה בייט לחודש. בואו נחשב רגע, 15 מגה-ביט זה 1.875 מגה בייט לשניה, או 6.7 ג’גה לשעה – או 162 ג’יגה בייט ליום.
במילים אחרות, בזק מקציבים לי הורדה רציפה של בערך חצי יום במהירות מקסימלית, ואז אני יכול לסגור את המחשב וללכת לים עד סוף החודש.
חשבתי לתומי שזו טעות, הרי זו הגבלה מטופשת בעליל – והתקשרתי אליהם.
הנציגה המסכנה לא ידעה מאיפה נפלתי לה, “אין שום הגבלה” היא חזרה ואמרה והקריאה לי את תנאי החוזה (המהירות, המחיר וההתחייבות)
אם אין הגבלה, הקשתי – למה כתוב שיש באימייל הרשמי ששלחת לי שמותר לי להוריד רצוף חצי יום בחודש?
“אני אשלח לך אימייל חדש שבו לא יהיה כתוב”, אמרה הנציגה.
הסכמתי בשימחה, וכשהגיע האימייל החדש כמובן זה עדיין היה שם.
שלחתי לה אותו חזרה, והיא הלכה לבדוק את זה מול הבכירים במשמרת.
היא חזרה ובשורה בפיה : “לא אוכפים את זה”.
“אם לא אוכפים את זה, למה זה כתוב?”, שאלתי,
אה, זו הנחיה מלמעלה.
טוב, כמובן שמציגת שרות לקוחות לא ציפיתי ליותר מדי, הרי היא לא באמת יכולה לשנות דברים כאלו.

פניתי לפניות הציבור של בזק Pniot@bezeqint.co.il (אל תטרחו עם הפורם באתר שלהם, הוא מגביל אתכם לשבע תווים לפניה).
קיבלתי תגובה, פרשתי את הטענה שלי – ואז כלום.
אחרי כמה שבועות, שלחתי עוד אימייל, ותהיתי אם הם מתעלמים.
אחרי כמה שבועות, עוד אימייל.
לבסוף חזרו אלי, טלפונית, ואחרי שיחה די מתישה שלא הגיעה לשום מקום מיוחד וכללה רמזים שהם שקלו לבטל את ההגבלה, קיבלתי את זה באימייל הסיכום:

  • בהתייחס לטענתך בדבר מסגרת תעבורת הנתונים בחבילה נבקש לציין, כי בזק בינלאומי, שהינה ספקית האינטרנט המובילה בישראל והאחרונה מבין כל הספקיות האינטרנט בארץ לבצע מהלך זה, בפועל טרם אכפה את המגבלה על השימוש בשירותים המצוינת במכתב ההצטרפות.
  • יצוין, מטרתה של מגבלה זו, הינה לאפשר לבזק בינלאומי לשמור על אפשרות לשלוט בחריגות קיצוניות בנפחי התעבורה ובכך ליישם מדיניות שימוש הוגן בשירותיה (fair usage policy), כמקובל בארץ ובעולם.
  • על אף האמור לעיל וכפי שסוכם בשיחתנו הטלפונית, לפנים משורת הדין ולמען שביעות רצונך, ככל שמסגרת תעבורת הנתונים תוחל בעניינך, תינתן לך האפשרות לבטל את שירותי הגלישה המסופקים לך על ידי בזק בינלאומי ללא תשלום יתרת חודשי ההתחייבות הנותרים בחבילה וזאת בתוך 30 יום מיום תחילת אכיפת המסגרת.
    יובהר, כי האמור בסעיף 4 לעיל תקף אך ורק ביחס לחבילה המוזכרת בסעיף 1 לעיל.
    ככל שיעלה הצורך לנצל את האמור בסעיף 4 לעיל, עליך לציין בפני נציגי החברה כי הנך זכאי למחווה שירותית זו בהתאם למספר פניה X.

אז בקיצור, “כולם עושים את זה, אנחנו האחרונים, ובכלל – אנחנו לא אוכפים את זה. אה  – וקח דג : אם נאכוף את זה תוכל לצאת מההתחייבות כג’סטה שירותית מיוחדת”

אבל מה הם אומרים בעצם: “לשמור על אפשרות לשלוט בחריגות קיצוניות בנפחי התעבורה”?
מה שהם אומרים זה “אם יהיה לנו עומס חריג על הרשת, נוכל לדפוק משתמשיים כבדים”.
התגובה שלי לזה: תקנו עוד רוחב פס לפני שיהיה לכם עומס חריג. המשתמשים הכבדים משלמים לכם את רוב הכסף שמשלם את המשכורות שלכם. הם לא טפילים, הם לא עלוקות – הם לקוחות חשובים.
אם כל המשתמשים היו משתמשים קלים, כולם היו משתמשים בחבילה של מגה וחצי (ואז היה לוקח להם שליש חודש להוריד 65 גיגה).

אז קודם כל, לידיעתכם.
דבר שני, אם זה מפריע לכם, תתלוננו.
אם אם יקבלו מספיק תלונות אולי הם יבטלו את זה.

קיבלתי דוח תנועה לפני מספר שבועות, ודרך אתר המשטרה הגעתי לאתר הטפסים הלאומי כדי למלט טופס ביטול דוח.
באתר הטפסים הלאומי, נדרשתי להוריד סרגל כלים כדי לקבל גישה לטופס, התחלה גרועה להמשך גרוע יותר.
ניסיתי להוריד את סרגל הכלים בפיירפוקס, והוא לא תומך בגרסאת הפיירפוקס שלי – הרי היא 6.0, עם זה אני מוכן לחיות, לפחות הם ניסו.
פתחתי את אינטרנט אקספלורר, וניכנסתי שוב.
התקנתי את סרגל הכלים, והתחלתי למלא את הטופס.
כמובן שהייתי צריך לנחש שאת מספר הדוח אני אמור להכניס בלי מקפים.
כשסיימתי חיפשתי כפתור אישור, שמירה, שליחה או כל דבר – אבל לא היה שום דבר.
באדום למעלה היה טקסט שאמר שאני אמור למלא ולהדפיס את הטופס.
די מטומטם, בשביל זה אני צריך סרגל כלים?
הדפסתי את המסך לקובץ, פתחתי אותו – ובפנים היו ההוראות המיוחלות:
אני צריך להדפס את הטופס דרך כפתור מזערי בסרגל הכלים.
השינאה שלי גדלה מרגע לרגע.
לחצתי על הכפתור המזערי, והדפסתי את הטופס לקובץ, כשפתחתי את הקובץ גיליתי שאת שדה הטקסט הקטן מדי בטופס הם הדפיסו בדיוק כמו שהוא נראה, מה שאומר שחצי מהטקסט שכתבתי לא מופיע בכלל בהדפסה.
השינאה גדלה עוד.
ערכתי את הטופס, מחקתי את כל האנטרים, והדפסתי שוב.
עכשיו אני אמור לשלוח את הטופס הזה בעצמי בדואר רשום, היעילות מעולם לא היתה כל כך נוחה.

אחרי החוויה המרנינה, החלטתי שאני חייב לשלוח להם משוב.
הרי יש להם חלק שלם במסך שמוקדש לשאלה “מה דעתך על תהליך מילוי הטופס?”.
אבל אפילו החוויה עד כה לא הכינה אותי להמשך.
לחיצה על הכפתור המעוצב למשעי “שלח משוב” גורמת להודעת השגיאה הבאה:

לאחר מספר נסיונות, הבנתי שבמקביל להודעת השגיאה הוא גם פותח טאב חדש עם טופס המשוב.
הקלדתי בששון את המשוב החינני שלי, וכשסיימתי בהיתי במסך בחיפוש אחרי כפתור השליחה.
הוא לא היה, אפילו לא בסרגל הכלים המשוקץ.
עדכנתי את המשוב, ולבסוף שלחתי אותו דרך דף צור קשר.
עצוב לחשוב שמיטב כספי משלם המיסים (הכסף שאני משלם!!) הולך לזבל האורגני המתכנה ממשל זמין.
הנה המשוב שלי, להנאתכם.

ראשית, הרשו לי להתנצל מראש על הטון של המשוב.
הוא בא מהלב.

קודם כל, זה מטומטמם שאני נדרש להוריד סרגל כלים כדי להשלים משימה טריויאלית שבסופה אני עוד אמור להדפיס ולשלוח את הטופס בעצמי.
בתום מילוי הפרטים, לא היה ברור לי איך להמשיך. אין כפתור אישר או המשך או הדפס.
רק כשהדפסתי את הטופס כרגיל קיבלתי מהמדפסת שלי את הנחיה להדפיס דרך סרגל הכלים. דבר מוזר ולא סטנדרטי בעולם המחשבים.
כשהדפסתי את הטופס דרך סרגל הכלים, הטקסט שכתבתי לא הוצג כראוי ונאלצתי לעמד אותו בצורה אחרת ולהדפיס מחדש.
כשרציתי לשלוח משוב, קיבלתי שגיאה על כל שהטופס לא אומת.
לבסוף הצלחתי לשלוח משוב (אני מקווה).

בגדול, אני חושב שהמערכת הזו היא אחד הכישלונות הגדולים ביותר שיצא לי לראות מאתר ממשלתי.
חווית המשתמש מזוויעה וגרמה לי לרצות להרביץ למישהו.

בתקווה שתפטרו את מי שפיתח את זה ותתחילו מחדש, הפעם עם צוות שאינו מטומטם.

קבלו תיקון, מערכת שליחת המשוב שלכם היא באיכות דומה לשאר המערכת ואין אפשרות לשלוח דרכה משוב.
ללא ספק היא פותחה על ידי אותו חבר מפגרים שפיתח את השאר.
אני אנסה לשלוח את המשוב בצורה אחרת.

שינאה.

מכונת הOpenSolaris שלי, מכילה שישה דיסקים:
שני דיסקים של 1TB, שניים של 1.5TB ושניים של 2TB.
מאגר האחסון (pool) העיקרי בנוי בעצם משלושה זוגות של דיסקים (או חלקי דיסקים), כך שאם אחד מהם מתקלקל המערכת יכולה להמשיך לתפקד.
החסרונות של הגישה הזו הם שאני מאבד חצי מהנפח, ושאם שני דיסקים מאותו יחידת ראי נופלים איבדתי הכל.
היתרונות הם ביצועי קריאה כפולים, וגמישות בשינויי תצורה. קל להוסיף דיסקים או לשדרג דיסקים קיימים לגדולים יותר.

ההסתברות לכשלון של דיסק מסויים גדלה ביחס ישיר למספר הדיסקים, במהלך השנתיים בערך שיש לי את המערכת הזו, שלושה דיסקים כשלו (!) ובכל זאת עד היום לא איבדתי מידע.
בכשלון האחרון הבחנתי לפני כמספר שבועות : לא הרגשתי בעיות, אבל ראיתי במקרה שZFS מדווח על שגיאה בדיסק, מה שהביא את המאגר כולו למצב degraded, שהוא בהחלט לא מצב שתרצו להשאר בו יותר זמן ממה שחייבים (כי דיסק נוסף שכושל עלול לדפוק את כל הנתונים).
אז נאלצתי להחליף את הדיסק.
הקטע הכי קשה הוא להבין איזה מהדיסקים הפיזיים צריך לנתק.
בפעם האחרונה דאגתי לסמן את הדיסקים במדבקות עם השם שלהם במערכת ההפעלה (למשל c8d0 עבור דיסק 1 בקונטרולר 8) אבל משום מה המדבקות היו קצת לא מדוייקות.
בכל מקרה, אחרי קצת ניסוי וטעיה, מצאתי איזה מהדיסקים הוא c8d1 (הדיסק שדווח כלא תקין).
הרצתי את הפקודה הבאה כדי להוציא אותו מהמאגר, וניתקתי אותו פיזית.

zpool detach storage c8d1

חיברתי למחשב אחר כדי לבדוק עם SpinRite (אין לי מסך מחובר לשרת הOpenSolaris).
SpinRite לא הצליח לתקן אבל ראיתי שSMART מדווח שיש לו המון שגיאות חיפוש (seek) וקריאה.

שלחתי למעבדה וקיבלתי דיסק אחר תוך כמה שבועות.
התקנתי את הדיסק במחשב, והרצתי את הפקודה הבאה כדי לחבר אותו חזרה:

zpool attach storage c9d1 c8d1

הפקודה די הפוכה לפקודת הניתוק הקודמת, רק שהיא מציינת לאיזה דיסק קיים להוסיף את החדש.
אחרי שהוספתי, זה המצב של המאגר storage :
אפשר לראות שמתבצעת פעולת resilver, שבעצם דואגת לסנכרן הנתונים של הדיסק החדש עם הדיסק הוותיק באותה יחידת ראי.

root@iron:~# zpool status storage
  pool: storage
 state: ONLINE
status: One or more devices is currently being resilvered.  The pool will
        continue to function, possibly in a degraded state.
action: Wait for the resilver to complete.
 scrub: resilver in progress for 0h14m, 0.60% done, 38h34m to go
config:

        NAME        STATE     READ WRITE CKSUM
        storage     ONLINE       0     0     0
          mirror-0  ONLINE       0     0     0
            c8d0p2  ONLINE       0     0     0
            c9d0p2  ONLINE       0     0     0
          mirror-1  ONLINE       0     0     0
            c9d1    ONLINE       0     0     0
            c8d1    ONLINE       0     0     0  6.70G resilvered
          mirror-2  ONLINE       0     0     0
            c11d0   ONLINE       0     0     0
            c10d0   ONLINE       0     0     0

errors: No known data errors

בפוסט הקודם דיברתי על הצורך שלי בצפיה של סרטוני יוטיוב על האייפד כשאני לא מחובר, ועל הפתרון שלי – שמבוסס על כמה סקריפטים שקושרים ביחד את הלוגיקה הבאה:

  1. הבא מהAPI של יוטיוב פיד RSS רלוונטי
  2. חלץ ממנו URLים של סרטונים
  3. הורד את הסרטונים עצמם מיוטיוב עם youtube-dl
  4. קודד את הסרטונים לפורמט של אייפד עם HandBreak
  5. צור פיד RSS שמאפשר להוריד את הקבצים המקודדים

ברגע שהתהליך הזה קיים, מה שנשאר זה לרשום את הפיד החדש בITunes, ומאותו רגע הוא מוריד אוטומטית את הסרטונים לצפייה אוף ליין.

לאחרונה שמעתי יותר ויותר על סקלה, סקלה היא שפה יחסית חדשה (הופיעה ב2003).
אז מה התכונות של סקלה?

  • שפה סטטית, הקומפיילר יגיד לכם כשיש לכם שגיאות טיפוס (בניגוד לשפות דינמיות כמו PHP, פייתון וכו’)
  • מבוססת על הJVM, סקלה מתקמפלת לבייט-קוד
  • מאפשרת גישה מלאה ונוחה לתשתיות וספריות של ג’אווה. אם יש לכם קוד ג’אווה תוכלו להשתמש בו בקלות מסקלה
  • הקוד בסקלה הוא הרבה יותר קומפקטי מג’אווה
  • תומכת בתכנות פונקציונלי, פונקציות הן אזרחיות מהשורה הראשונה, אפשר להעביר פונקציות כפרמטר וליצור פונקציות אנונימות בצורה מאוד אלגנטית.
  • תומכת במודל הActors, שהוא מודל שבו “שחקנים” שונים מתקשרים אחד עם השני על ידי שליחת הודעות, ולא חולקים שום State. המודל הזה חוסך לא מעט בעיות שנובעות ממיקבול גבוה
  • תמיכה בסקריפטים, אפשר להריץ קוד כסקריפט בלי לקמפל אותו מראש (למרות שכמובן אפשר לקמפל).

יש עוד כמה תכונות מעניינות לסקלה, אבל אני לא באמת מכיר את השפה לעומק עדיין.

החלטתי ללמוד סקלה, וכדי לעשות את זה כתבתי מחדש את הסקריפטים הקודמים כתוכנית סקלה אחת  וגם שיפרתי אותם על הדרך לתמוך בכמה ערוצי יוטיוב.
עשיתי את זה עם vim, ישירות על השרת – ובלי לקמפל, ממש כמו כתיבה של סקריפט.
אני בטוח שדוגמאות הקוד שלי יכולות להיות יותר אלגנטיות, אבל זה מה שיש לבינתיים :)
אז זה הmain של הסקריפט:

object YoutubeRSS
{
    val downloader = new Downloader
    def main(args: Array[String])
    {
        deleteOldFiles
        regenerateFeeds
        Source.fromFile(“channels”).getLines.foreach(x =>; process(x trim) )
        downloader ! Stop
    }
    //…
}

התוכנית מפעילה כמה פונקציות ואז קוראת שורות מקובץ,  ועל כל שורה מפעילה את הפונקציה process.
לבסוף היא שולחת הודעת Stop לActor של הDownloader.
אפשר לשים מיד לב לכמה דברים:

  • כשמצהירים על object, זה בעצם סינגלטון.
  • אין נקודה-פסיק בסוף שורה, בסקלה זה אופציונלי.
  • לא חייבים להשתמש בסוגריים כשקוראים לפונקציה בלי פרמטרים (למעשה, אם יש לפונקציה פרמטר בודד – אפשר בדרך כלל לוותר על הסוגריים גם אז)
  • בסקלה כמעט כל דבר חוקי כשם של פונקציה, סימן קריאה היא פונקציה די נפוצה בהרבה ספריות.

הנה עוד קצת מהתוכנית:

object Conf
{
    val YOUTUBE_CHANNEL_URL = “http://gdata.youtube.com/feeds/api/users/%s/uploads”
    val YOUTUBE_PROFILE_URL = “http://gdata.youtube.com/feeds/api/users/%s”
    val BIN_DIR = “/home/omry/youtube-rss-2/bin”
    val DOWNLOAD_DIR = “/home/omry/youtube-rss-2/download”
    val RSS_WEB_DIR=“/home/omry/www/youtube-rss.firefang.net”
    val RSS_BASE_URL=“http://youtube-rss.firefang.net”
    val DELETE_OLDER=14
}

object YoutubeRSS
{
    //…
    def process(channel : String)
    {
        println(“Processing channel “ + channel)
        updateFeed(channel)
        val url = Conf.YOUTUBE_CHANNEL_URL.format(channel)
        val rss = Source.fromURL(url).mkString
        val xml = XML.loadString(rss)
        val entries = xml\“entry”;
        entries foreach(download(_,channel))
        println(“Done processing “ + channel)
    }
}

הקונפיגורציה כרגע היא ישירות בקוד בתוך אובייקט Conf.
שימו לב לאלגנטיות של הבאת הנתונים מהURL ובעיקר של פרסור הXML,
השורה הפשוטה

val entries = xml\“entry”;

מחזירה רשימה של entries מתוך הXML (שהוא פיד רסס).

עוד קצת קוד מהפונקציה שמכינה את קובץ הRSS הסופי:

new File(dir).listFiles.
                sortBy(_.lastModified).reverse.
                filter(_.getName().endsWith(“.mp4″)).
                map(file => file.substring(dir.length + 1)).
                foreach(file =>
                {
                    val f = new File(dir,file)
                    val entry = new SyndEntryImpl()
                    //..
 

הקוד פה מקבל (עם API של ג’אווה) את רשימת הקבצים בספריה, ממיין אותן לפי תאריך שינוי, הופך את הסדר, מפעיל פילטר שמשאיר רק קבצי mp4, מפעיל פוקציית map שמורידה את הספריה משם הקובץ ולבסוף מפעיל קוד כל סטרינג.

קצת על הפעלת תהליכים חיצוניים:
מי שניסה להפעיל פרוסס חיצוני מג’אווה בוודאי זוכר שזו לא חוויה מרנינה, צריך שני Threadים שיקראו את הפלט של התהליך (אחד לפלט סטנדרטי ואחד לשגיאה סטנדרטי) ובאופן כללי זה די מסורבל.
בסקלה (2.9 ומעלה) זה הרבה יותר פשוט:

// Run process
“ls -l”!
// Run process and get output into string:
val output = “ls -l”!!
// Run process from a list of arguments
val fname = List(“youtube-dl”, “–get-filename”,“-o”,“download/”+channel+“/%(uploader)s-%(stitle)s.%(ext)s”,link.toString)!!

הרבה יותר נחמד מבג’אווה.

אפשר להוריד את הכל מפה.

בשורה התחתונה, בינתיים אני אוהב את סקלה, ונראה לי שאני אתחיל להשתמש בה באופן יותר קבוע.

כן, אני יודע שמזמן לא היה פה פוסט, תתבעו אותי ;) .

אחד הדברים שאף פעם לא עובד כמו שצריך באייפון ובאייפד (ובאייפוד) זה YouTube. כלומר – יש ישום של אפל לצורך העניין, אבל החוויה לא מרנינה:
בדרך כלל לא תצליחו לראות סרטון בלי גמגומים, גם אם אתם משתמשים ברשת אלחוטית מהירה עם חיבור אינטרנט שמן וטוב.

לאחרונה אני צופה בדי הרבה משחקי סטארקרפט ביוטיוב, והפריע לי חוסר היכולת לראות את המשחקים על האייפד.
זה הביא לי רעיון:
מה אם נוריד מראש את הסרטונים, וניתן אותם לאייפד בצורת פיד RSS – ממש כמו שצופים בפוקסט וידאו – ככה שנוכל לראות אותם בלי להיות תלויים בחסדי מהירות התקשורת ליוטיוב?

אז בתור התחלה, הצצה למטרה של כל הפוסט הזה:
הנה פיד הרסס של הערוצים שאני מנוי עלהם ביוטיוב (Subscriptions).
את הפיד הזה הוספתי לאייטונז (כפודקאסט), ומאותו רגע הוא מוריד את הסרטונים ומסנכרן אותם לאייפד.

אז איך כל זה קורה?

הכל מתחיל בAPI הנתונים של יוטיוב, שמאפשר קבלת פידי RSS שונים ומשונים, כשהשימושי שבהם מבחינתי הוא פיד שמאפשר קבלת הסרטונים החדשים בערוצים שמשתמש מסויים מנוי עליהם:

http://gdata.youtube.com/feeds/base/users/omryy/newsubscriptionvideos

שימו לב ששם המשתמש שלי הוא חלק מהURL.
קל למדי לחלץ מפה את רשימת הURLים לסרטונים שמופיעים בפיד, למשל עם הסקריפט הזה:

GET “http://gdata.youtube.com/feeds/base/users/omryy/newsubscriptionvideos” | grep http://www.youtube.com/watch?v=[a-zA-Z0-9_\-]* -o | sort -u

הסקריפט, בפשטות – מוריד את הפיד, מחפש בתוכו URLים שנראים כמו לינקים לסרטון ביוטיוב, ומבטל כפילויות שמופיעות מסיבה לא חשובה בפיד.

הצעד הבא הוא להוריד את הסרטים עצמם (הלינקים בפיד הם לינקים לצפיה ישירות ביוטיוב).
לשם כך נשתמש בכלי החביב במיוחד, youtube-dl,

הפקודה הבאה תוריד את הסרטון מהURL הנתון, ותשמור אותו בפורמט נחמד וברור שכולל את השם של מי שהעלה, וכן שם קובץ של הכותרת של הסרטים.
youtube-dl -w YOUTUBE_URL -o “%\(uploader\)s-%\(stitle\)s.%\(ext\)s”
אפשר לחבר את הפקודה הזו לפקודה שמחלצת את הURLים מהפיד תוך שימוש בxargs, אבל בחרתי להשתמש בGNU Parallel שמאפשר הפעלה של פקודה מסויימת כמה פעמים במקביל (בדומה מאוד לxargs, רק במקביל).

אז הפקודה השלמה להורדת הסרטונים שמעניינים אותי היא:

#!/bin/bash
source conf
pushd $YT_DOWNLOAD_DIR
GET “http://gdata.youtube.com/feeds/base/users/omryy/newsubscriptionvideos” | grep http://www.youtube.com/watch?v=[a-zA-Z0-9_\-]* -o | sort -u | $BIN_DIR/parallel -j $CONCURRENT_DOWNLOADS $BIN_DIR/youtube-dl -w {} -o “%\(uploader\)s-%\(stitle\)s.%\(ext\)s”
popd

הסקריפט הזה (והבאים אחריו) משתמש בקובץ ההגדרות הבא:

BIN_DIR=/home/omry/youtube-rss/bin
YT_DOWNLOAD_DIR=/home/omry/youtube-rss/download
CONCURRENT_DOWNLOADS=20
RSS_WEB_DIR=/home/omry/www/youtube-rss.firefang.net
RSS_BASE_URL=http://youtube-rss.firefang.net

# Delete older than X days
DELETE_OLDER=14

עד פה, הכל טוב.
הפקודה הזו תוריד רק קבצים חדשים שלא הורדו כבר (הדגל -w).

כשניסיתי להעלות לאייפד את הקבצים האלו, שמתי לב שהוא החליט לדלג על חלק גדול מהם.
מסתבר שכל הקבצים שדולגו היו ברזולוציה של HD מלא (1920X1080).
אוקיי, אז זה הדבר הבא שצריך לתקן.
בהתחלה חשבתי להשתמש בffmpeg – שהיא הפתרון המקובל לקידוד קבצי וידאו.
הבעיה היא שצריך לקמפל גרסא שלה שתומכת בH264 (הקידוד שמתאים למכשירי אפל), ושבאופן כללי די קשה לגרום לה לעשות מה שאתם רוצים.
אחרי המשך חפירות מצאתי את Handbreak שהיא תוכנה (בקוד פתוח כמו כל שאר הדברים בפוסט הזה) שתפקידה בחיים הוא לקודד סרטונים לאייפוד, אייפון אייפד ושאר חברים בצורה מאוד פשוטה.
Handbreak מגיעה עם ממשק GTK שלא ממש עניין אותי, ועם שורת פקודה שמאוד עניינה אותי.
אגב, היא משתמשת בlibavcodec מבית היוצר של מפתחי ffmpeg.
לעניינינו, הפקודה לקידוד סרטון לפורמט אייפד היא :

HandBrakeCLI -Z iPad -i input_file -o output_file.mp4

לא יכול להיות יותר פשוט מזה.

התהליך עצמו יכול לקחת די הרבה זמן, תלוי באורך הסרט ובעוצמת העיבוד של המחשב שלכם.
Handbreak מספיק מוצלחת כדי להשתמש בכל הליבות שלכם (אפשר לומר לה להשתמש במספר מסויים אם רוצים) ולרוץ בעדיפות נמוכה כדי לא לחנוק תהליכים אחרים.

השתמשתי בתסריט bash קטן כדי לעבור על הקבצים שירדו, ולקודד לספריה חדשה קבצים עם אותו שם אם הם כבר לא נמצאים שם.

$ cat encode_new.sh
#!/bin/bash
source conf
for file in `ls -1 $YT_DOWNLOAD_DIR/*.mp4 $YT_DOWNLOAD_DIR/*.flv $YT_DOWNLOAD_DIR/*.video`
do
base=`basename ${file%.*}`
ipad=${RSS_WEB_DIR}/${base}.mp4
if [ -f $ipad ]
then
echo “Skipping $ipad”
else
$BIN_DIR/HandBrakeCLI -Z iPad -i $file -o $ipad
touch -c -r $file $ipad
./update_feed.sh
fi
done

אחרי כל קובץ שמקודד, הסקיפט update_feed.sh נקרא.
סקריפט זה משתמש בתוכנית ג’אווה קטנה שכתבתי שמכינה קובץ RSS על בסיס ספריה עם סרטוני וידאו.
לבסוף, נשאר רק למחוק קבצים ישנים יותר מ14 יום כדי למנוע התפוצצות, ולהריץ את כל העסק פעם ביום או משהו עם CRON.

$ cat delete_old.sh
#!/bin/bash
source conf
find -mtime +$DELETE_OLDER -exec echo rm $YT_DOWNLOAD_DIR/{} $RSS_WEB_DIR/{} \;

אפשר להוריד את כל העסק מפה, שימו לב שזה כולל את HandBreak בגרסאת AMD64 ללינוקס, אבל אפשר להוריד מהאתר שלהם גרסאות אחרות.

לסיום, הנה סרטון סטרקראפט מצחיק:

אחד הכאבים המפתיעים והלא צפויים שנתקלתי בהם לאחרונה עם ג’אווה היה כשניסיתי לתקשר עם שרת HTTPS שנחתם בחתימה של StartSSL.
מסתבר שג’אווה מגיעה כמעט בלי חתימות של ספקי חתימות (verisign שם, אבל הרבה מאוד אחרים לא).
חפירות באינטרנט הובילו לכל מני פתרונות שלא עבדו, אולי כי החתימה שלי היא Wildcard certificate (*.site.com).
פתרון אפשרי הוא ליבא את חתימת השורש של StartSSL לתוך הJVM, אבל זו פעולה ידנית שכל משתמש צריך לעשות ובכל מקרה היא לא עבדה לי, אולי בגלל סוג החתימה.
כל זה מעצבן למדי, הדפדפן סומך על האתר אבל ג’אווה לא מסכימה להתחבר:
נסיון להשתמש בURL הרגיל של ג’אווה כדי להתחבר בHTTPS בדרך כלל מוביל לשגיאה הנפלאה הבאה (אלא אם מדובר בחתימה שחתם השורש שלה ידוע לJVM):

Caused by: sun.security.validator.ValidatorException: PKIX path building failed: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:294)
        at sun.security.validator.PKIXValidator.engineValidate(PKIXValidator.java:200)
        at sun.security.validator.Validator.validate(Validator.java:218)
        at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.validate(X509TrustManagerImpl.java:126)
        at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:209)
        at com.sun.net.ssl.internal.ssl.X509TrustManagerImpl.checkServerTrusted(X509TrustManagerImpl.java:249)
        at com.sun.net.ssl.internal.ssl.ClientHandshaker.serverCertificate(ClientHandshaker.java:1053)
        … 16 more
Caused by: sun.security.provider.certpath.SunCertPathBuilderException: unable to find valid certification path to requested target
        at sun.security.provider.certpath.SunCertPathBuilder.engineBuild(SunCertPathBuilder.java:174)
        at java.security.cert.CertPathBuilder.build(CertPathBuilder.java:238)
        at sun.security.validator.PKIXValidator.doBuild(PKIXValidator.java:289)

פתרון:
מעבר לHTTPClient של אפאצ’י ושימוש בקוד הבא (דוגמא לGET ולPOST)
התיעוד של הספריה לא משהו, יש כמה גרסאות לא תואמות שלה והתיעוד מתייחס לגרסא ישנה.
הקוד הבא עובד עם גרסא 4.0.3 של HTTPClient ו4.1.0 של HTTPCore (זו תלות נדרשת לHTTPClient, אפשר להוריד מאותו אתר).
קחו בחשבון שהפתרון הזה מאפשר תקיפת MAN IN THE MIDDLE כדי לזייף את החתימה, אבל מבחינה פרקטית עדיף משהו שעובד אבל קצת פגיע מאשר משהו שלא עובד בכלל. (וכמובן ששימוש בHTTPS הוא עדיף על שימוש בHTTP נקי בכל מקרה).

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;

import javax.net.ssl.SSLContext;
import javax.net.ssl.TrustManager;
import javax.net.ssl.X509TrustManager;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.conn.ClientConnectionManager;
import org.apache.http.conn.scheme.Scheme;
import org.apache.http.conn.scheme.SchemeRegistry;
import org.apache.http.conn.ssl.SSLSocketFactory;
import org.apache.http.entity.InputStreamEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.params.CoreConnectionPNames;
import org.apache.http.util.EntityUtils;

public class HTTPClientSSLExample
{
        public static void main(String[] args) throws MalformedURLException, IOException
        {
                byte[] bytes = getURLBytes_httpclient(“https://www.startssl.com/”, 30000, 30000);
                System.out.println(new String(bytes));
        }
       
        public static byte[] getURLBytes_httpclient(String url, int connectionTimeout, int readTimeout) throws IOException
        {
                long now = System.currentTimeMillis();
               
                DefaultHttpClient httpclient = getHttpClient(url);
               
                httpclient.getParams().setIntParameter(CoreConnectionPNames.SO_TIMEOUT, readTimeout);
                httpclient.getParams().setIntParameter(CoreConnectionPNames.CONNECTION_TIMEOUT, connectionTimeout);
               
                HttpGet get = new HttpGet(url);
                HttpResponse response = httpclient.execute(get);
                int res = response.getStatusLine().getStatusCode();
                if (res == 200)
                {
                HttpEntity entity = response.getEntity();
                        int len = (int) entity.getContentLength();
                        InputStream in = entity.getContent();
                        ByteArrayOutputStream bout = new ByteArrayOutputStream(len > 0 ? len : 1000);
                        pump(in, bout);
                        return bout.toByteArray();
                }
                else
                {
                        String bs = “”;
                        try
                        {
                                HttpEntity entity = response.getEntity();
                                bs = entity == null ? null : EntityUtils.toString(entity);
                        }
                        catch (IOException e)
                        {
                                bs += ” || Exception while trying to read data from stream : “ + e.getMessage();
                        }
                       
                        throw new IOException(“Server returned HTTP “ + res + ” after “ + (System.currentTimeMillis() – now) + ” ms, URL : “ + url + ” data: “ + bs);
                }              
        }
       

        public static ByteArrayInputStream openInputStream_httpclient(String url, int connectionTimeout, int readTimeout, byte[] postdata) throws IOException
        {
                if (postdata != null)
                {
                        DefaultHttpClient httpClient = getHttpClient(url);
                        HttpPost post = new HttpPost(url);
                        InputStreamEntity reqEntity = new InputStreamEntity(new  ByteArrayInputStream(postdata), postdata.length);
                        reqEntity.setContentType(“binary/octet-stream”);
                        post.setEntity(reqEntity);
                        HttpResponse response = httpClient.execute(post);
                        int res = response.getStatusLine().getStatusCode();
                        if (res == 200)
                        {
                        HttpEntity entity = response.getEntity();
                                int len = (int) entity.getContentLength();
                                InputStream in = entity.getContent();
                                ByteArrayOutputStream bout = new ByteArrayOutputStream(len > 0 ? len : 1000);
                                pump(in, bout);
                                return new ByteArrayInputStream(bout.toByteArray());
                        }
                        else
                        {
                                throw new IOException(“Http response code “ + res);
                        }
                }
                else
                {
                        byte[] bytes = getURLBytes_httpclient(url, connectionTimeout, readTimeout);
                        return new ByteArrayInputStream(bytes);
                }
        }

        private static DefaultHttpClient getHttpClient(String url1) throws IOException
        {
                DefaultHttpClient httpclient = new DefaultHttpClient();
               
                try
                {
                        SSLContext ctx = SSLContext.getInstance(“TLS”);
                        X509TrustManager tm = new X509TrustManager()
                        {

                                public void checkClientTrusted(X509Certificate[] xcs, String string) throws CertificateException
                                {
                                }

                                public void checkServerTrusted(X509Certificate[] xcs, String string) throws CertificateException
                                {
                                }

                                public X509Certificate[] getAcceptedIssuers()
                                {
                                        return null;
                                }
                        };
                        ctx.init(null, new TrustManager[]
                        {
                                tm
                        }, null);
                        SSLSocketFactory ssf = new SSLSocketFactory(ctx);
                        ssf.setHostnameVerifier(SSLSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER);
                        ClientConnectionManager ccm = httpclient.getConnectionManager();
                        SchemeRegistry sr = ccm.getSchemeRegistry();
                        sr.register(new Scheme(“https”, ssf, 443));
                        httpclient = new DefaultHttpClient(ccm, httpclient.getParams());
                }
                catch (Exception ex)
                {
                        ex.printStackTrace();
                        return null;
                }
               
                // This block handles urls with user:password@server block
                URL u = new URL(url1);
                String userInfo = u.getUserInfo();
                if (userInfo != null)
                {
                        String user;
                        String password;
                        int i = userInfo != null ? userInfo.indexOf(‘:’) : -1;
                        if (i == -1)
                        {
                                user = userInfo.substring(0);
                                password = “”;
                        }
                        else
                        {
                                user = userInfo.substring(0, i);
                                password = userInfo.substring(i + 1);
                        }
                        httpclient.getCredentialsProvider().setCredentials(new AuthScope(u.getHost(), u.getPort()), new UsernamePasswordCredentials(user, password));
                }
                return httpclient;
        }

        /**
         * Writes the bytes read from the given input stream into the given output
         * stream until the end of the input stream is reached. Returns the amount
         * of bytes actually read/written.
         */

        public static int pump(InputStream in, OutputStream out) throws IOException
        {
                byte[] buf = new byte[4096];
                int count;
                int amountRead = 0;

                while ((count = in.read(buf)) != -1)
                {
                        out.write(buf, 0, count);
                        amountRead += count;
                }

                return amountRead;
        }
}

פאול באטלר – מתמחה בפייסבוק – קנה לו קצת תהילה כשרינדר את הגרף החברתי של פייסבוק במערכת קורדינטות גאוגרפיות לפי המיקום של המשתמשים, כשכל שני חברים מתוך דגימה של עשרה מליון משתמשים אקראיים בפייסבוק מצויירת כקו.
התוצאה מרשימה מאוד, ולמעשה מציירת את המפה של רוב העולם.

(לחצו כדי לקבל תמונה ברזולוציה גבוהה).

FireStats icon ‏מריץ FireStats‏