סקלה

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

  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ים לסרטונים שמופיעים בפיד, למשל עם הסקריפט הזה:
[CODE]
GET "http://gdata.youtube.com/feeds/base/users/omryy/newsubscriptionvideos" | grep http://www.youtube.com/watch?v=[a-zA-Z0-9_\-]* -o | sort -u
[/CODE]
הסקריפט, בפשטות – מוריד את הפיד, מחפש בתוכו URLים שנראים כמו לינקים לסרטון ביוטיוב, ומבטל כפילויות שמופיעות מסיבה לא חשובה בפיד.

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

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

אז הפקודה השלמה להורדת הסרטונים שמעניינים אותי היא:
[CODE]
#!/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
[/CODE]

הסקריפט הזה (והבאים אחריו) משתמש בקובץ ההגדרות הבא:
[CODE]
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
[/CODE]
עד פה, הכל טוב.
הפקודה הזו תוריד רק קבצים חדשים שלא הורדו כבר (הדגל -w).

כשניסיתי להעלות לאייפד את הקבצים האלו, שמתי לב שהוא החליט לדלג על חלק גדול מהם.
מסתבר שכל הקבצים שדולגו היו ברזולוציה של HD מלא (1920X1080).
אוקיי, אז זה הדבר הבא שצריך לתקן.
בהתחלה חשבתי להשתמש בffmpeg – שהיא הפתרון המקובל לקידוד קבצי וידאו.
הבעיה היא שצריך לקמפל גרסא שלה שתומכת בH264 (הקידוד שמתאים למכשירי אפל), ושבאופן כללי די קשה לגרום לה לעשות מה שאתם רוצים.
אחרי המשך חפירות מצאתי את Handbreak שהיא תוכנה (בקוד פתוח כמו כל שאר הדברים בפוסט הזה) שתפקידה בחיים הוא לקודד סרטונים לאייפוד, אייפון אייפד ושאר חברים בצורה מאוד פשוטה.
Handbreak מגיעה עם ממשק GTK שלא ממש עניין אותי, ועם שורת פקודה שמאוד עניינה אותי.
אגב, היא משתמשת בlibavcodec מבית היוצר של מפתחי ffmpeg.
לעניינינו, הפקודה לקידוד סרטון לפורמט אייפד היא :
[CODE]
HandBrakeCLI -Z iPad -i input_file -o output_file.mp4
[/CODE]
לא יכול להיות יותר פשוט מזה.

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

השתמשתי בתסריט bash קטן כדי לעבור על הקבצים שירדו, ולקודד לספריה חדשה קבצים עם אותו שם אם הם כבר לא נמצאים שם.
[CODE]
$ 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
[/CODE]

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

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

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

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

ג'אווה וHTTPS

אחד הכאבים המפתיעים והלא צפויים שנתקלתי בהם לאחרונה עם ג'אווה היה כשניסיתי לתקשר עם שרת 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;
}
}

זליגת זכרון : הרוצח השקט

זליגת זכרון היא אחד הבאגים החמקמקים ביותר, ממש הרוצח השקט של תוכנות מחשב.
לא משנה באיזה שפה אתם כותבים, לא קשה במיוחד לכתוב קוד שידלוף כמסננת (אם כי זליגות זכרון בשפות מנוהלות כמו ג'אווה ו#C הן בעלות מאפיינים שונים לגמרי מזליגות זכרון בשפות בהן המתכנת אחראי על ניהול הזכרון ואין איסוף זבל אוטומטי).
כל משתמש מנוסה מכיר את התופעה המציקה, שתוכנה עובדת מהר בהתחלה ואחרי פרק זמן לא קבוע היא מתחילה להאט ולהאט עד כדי זחילה.
לפעמים היא מצליחה להאט את כל המחשב על הדרך, אם היא זולגת כמויות זכרון שגורמות למחשב להתחיל להשתמש בצורה מופרזת בקובץ הSWAP.
בניגוד להרבה בעיות תכנות אחרות, המתכנת לא מקבל אזהרה או שגיאה כאשר הוא כותב באג של זליגת זכרון, התוכנה לא קורסת מייד כמו למשל בגישה לזכרון דרך מצביע לNULL בC או בC++, והקומפיילר לא יספר למתכנת על הטעות שהוא עשה.
הסיבה לכך היא לא שמפתחי הקומפיילרים הם עצלים מכדי לכתוב קוד שמוצא את הבעיה – אלא שפשוט לא ניתן לכתוב קוד שיזהה זליגת זכרון בכל המקרים בצורה סטטית (על בסיס הקוד הכתוב בלבד).
הדוגמא הפשוטה ביותר היא תוכנית שמקצה ומשחררת זכרון על פי בקשת המשתמש (לאו דווקא במונחים האלו, המשתמש יכול לבקש לפתוח קובץ ובתגובה התוכנה תקצה זכרון בגודל הקובץ ותטען את הקובץ פנימה). אם התוכנה עובדת ישירות עם הזכרון אז הקומפיילר לא יכול להבטיח שהזכרון שיוקצה בשלב מסויים גם ישוחרר – ולו כי זה יקרה רק אם המשתמש יבקש את זה.

אז מה הבעיה עם זליגת זכרון?
הבעיה הראשונה והטריויאלית ביותר היא שתוכנה שזולגת משתמשת ביותר זכרון ממה שהיא צריכה, והמצב מחמיר בהדרגה ככל שאותו קוד שזולג רץ יותר. תוכנה שצורכת יותר מדי זכרון היא לא בהכרח בעיה חמורה. למעשה – כמעט כל תוכנה ברמת מורכבות בינונית ומעלה שנכתבה בשפת C או C++ זולגת זכרון כאילו אין מחר, ובדרך כלל זה לא גורם בעיות כי המשתמש סוגר את התוכנה לפני שהיא מגיע למצב של חוסר זכרון.
הבעיה הזו היא קצת יותר חמור משנדמה במבט ראשון:
כאשר המתכנת מבקש להקצות זכרון, נניח 1MB, הוא מצפה לקבל 1MB של זכרון רציף, שאפשר לעבור עליו בלולאה אחת מתחילתו ועד סופו.
דמיינו מערכת עם 2 מגה זכרון שעומדים לרשות תוכנה שרצה שם, מספיק לכאורה להקצות 2 בלוקים של 1MB.
עכשיו, אם נקצה סך הכל של 1K זכרון, ניתן להניח שנוכל להקצות עוד 1999K ובוודאי שנוכל להקצות 1MB, נכון?
אז זהו – שלא בדיוק : אם נקצה את ה1K בארבע בלוקים של 250 בתים, ובהפרשים של כ500K, למעשה נמנע מהתוכנה אפשרות לקבל זכרון רציף של יותר מכ500K.
כלומר, למרות שהזכרון הפנוי מספיק – בפועל הזכרון הפנוי שבור לחתיכות קטנות מדי (Fragmented), ואין לנו יכולת להקצות את הזכרון הדרוש.
מכאן אפשר להסיק שגם דליפת זכרון קטנה עלולה במצבים מסויימים לגרום לחוסר זכרון קריטי.
אני מקווה שזה שיכנע אתכם שדליפת זכרון זה דבר רע שראוי לטפל בו.

כל מתכנת מתחיל יודע שלשפות מודרניות יש איסוף זבל אוטומטי שאמור למנוע זליגת זכרון.
יש פה כמה בעיות, קודם כל – למרות שלרוב השפות יש איסוף זבל אוטומטי, ברוב המקרים מדובר בזבל של איסוף זבל, ואם להיות פחות ציורי ויותר ספציפי – מדובר בקוד איטי ודי מוגבל, שלא תופס את כל המקרים.
אבל אם נסתכל בג'אווה וב#C (שאני מניח שיש לה איסוף זבל בקליבר דומה לזה של ג'אווה) – הן בהחלט מסוגלות לאסוף את הזבל במהירות וביעילות.
הבעיה היא שההגדרה של זבל הוא אובייקט שהוקצה ואין אליו התייחסות משום מקום (Reference), ולפעמים המתכנת לא מבחין שבעצם יש אליו התייחסות.
למשל, קוד הג'אווה הטריואלי הבא ירוץ עד שיעוף עקב חוסר זכרון:
[code lang="java"]
import java.util.HashSet;
import java.util.Set;

public class a {
public static void main(String[] args) {
Set s = new HashSet();
int a = 0;
while(true)
s.add(String.valueOf(a++));
}
}
[/code]

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

בשפות עם ניהול זכרון ישיר יש מגוון רחב בהרבה של שגיאות שקשורות לזכרון, החל מזכרון שמוקצה ולא משוחרר, זכרון שמשוחרר פעמיים, זכרון שמוקצה עם Malloc של C ומשוחרר עם delete של C++, כתיבה או קריאה אל מחוץ לגבולות הזכרון שהוקצה ועוד ועוד.
הנה תוכנית C++ קטנה שמדגימה חלק מהשגיאות האפשריות:
[code lang="c++"]
#include

char* get1(){
return new char[10];
}

char* get2(){
return (char*)malloc(sizeof(char)*10);
}

char* get3(){
static char c[10];
return c;
}

int main() {
char *s1 = get1();
char *s2 = get2();
char *s3 = get3();

s1[11] = 'x'; // out of bounds access. mistake
free(s1); // mistake, should be delete[]
delete s2; // mistake should be free
delete s3; // mistake, should not be freed or deleted

char *s4 = new char[10];
delete s4; // mistake, should delete with delete[] and not with delete.
delete s4; // mistake, deleted twice.
char *s5 = new char[10]; // mistake, never deleted
return 0;
}
[/code]
כמו תמיד, C++ נותנת הרבה יותר חבל למתכנת התמים שרוצה לתלות את אח של אח שלו על עץ.
למעשה רק השגיאה האחרונה היא ממש זליגת זכרון, אבל כל השגיאות נפוצות וקשורות לזכרון.
יש טכניקות ידועות ומקובלות שמקטינות מאוד את הסבירות לטעויות שקשורות להקצאה ושחרור זכרון, ספציפית יש טכניקה שמוזכרת בספר The C++ programming language של סטראוסטרופ שנקראת Resource Acquisition Is Initialization או RAII בקיצור.
RAII הוא נושא שראוי לפוסט שלם בפני עצמו, אבל הנה דוגמת שימוש פשוטה (מדי, יש בה לא מעט חורים):
[code lang="c++"]
void direct_test(){
char *buffer = new char[100];
try{
work(buffer);
delete[] buffer;
}
catch(…){
delete[] buffer;
throw;
}
}
[/code]
הקוד למעלה מקצה זכרון, וקורא לפונקציה שעובדת עליו. לבסוף הוא משחרר את הזכרון וגם דואג שהוא ישוחרר כראוי אם הפונקציה תזרוק אקספשן.
התבנית הזו של טיפול ידני בשגיאות היא מאוד בעייתית ומועדת לטעויות.
אם היינו משתמשים בRAII, יכלנו להגדיר מחלקה בשם Buffer, שתדאג לשחרור ולהקצאה:
[code lang="c++"]
class Buffer{
char *buffer;
size_t size;

public:
Buffer(int size){
buffer = new char[size];
}

~Buffer(){
delete[] buffer;
}

char *get() {return buffer;}
};

void raii_test(){
Buffer buffer(100); // 100 bytes allocated in constructor
work(buffer.get());
// automatically freed when buffer destructor is called (will be called even if an exception is thrown).
}
[/code]

אפשר לראות שהקוד שמשתמש בזכרון הוא הרבה יותר פשוט עכשיו.
מצד שני, יש מספר לא מבוטל של באגים בדוגמא הזו (למשל, כשמעתיקים את buffer הזכרון שהוקצה ישוחרר פעמיים). כדי שזה באמת יעבוד צריך לדאוג לבנאי העתקה, אופרטור הצבה וכו'.
כל מתכנת C++ רציני חייב להכיר את הטכניקה הזו, שתחסוך לו שערות לבנות רבות מאוד.
אבל לפעמים אין ברירה וחייבים להשתמש בזכרון ישירות, בדרך כלל כאשר עובדים עם ספריות שכתובות ככה-ככה, או עם קוד אחר שנכתב על ידי מישהו שעדיין לא ראה את האור.
עוד דבר – בC לא אין אפשרות אמיתית להשתמש בRAII כי השפה לא מספיק עשירה, ולכן מתכנתים שכותבים בC אמיתי (ולא C+) לא יכולים להנות מהטכניקה הזו.

אחד הכלים החזקים ביותר לזיהוי בעיות שקשורות לעבודה עם זכרון הוא valgrind.
ואלגרינד הוא למעשה קבוצה של כלים, כשהידוע בהם הוא memcheck שבמגלה שגיאות בעבודה עם זכרון.
הוא כולל כלים אחרים לניתוח ביצועים (cachegrind, callgrind) ועוד.
ואלגרינד עובד על רוב מערכות ההפעלה ממשפחת היוניקסים (BSD, Linux, MacOS X וכו'), בתאוריה אפשר לבדוק איתו תוכנות חלונות דרך WINE.
ואלגרינד עובד בשורת הפקודה, מה שמרתיע משתמשים רבים. לאחרונה חיפשתי ומצאתי פתרון שמאפשר אינטגרציה של ולגרינד עם CDT:
התמיכה היא כחלק מפרוייקט בשם LinuxTools שמפותח על ידי RedHat (כנראה).
יש פה דמו וידאו של השימוש בפלאגין.
קובץ הוידאו נמצא כאן למי שלא מצליח לראות אותו בבלוג (דורש פיירפוקס 3.5 או דפדפן שתומך בתג וידאו).

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

API לזיהוי פנים

בתקופה האחרונה עבדנו קשה בface.com על API פתוח למציאה וזיהוי פנים, ואתמול בערב השקנו אותו.
זיהוי פנים מורכב מכמה שלבים:
השלב הראשון הוא למצוא אוטומטית היכן בתמונה יש פנים, או face detection.
השלב השני הוא להשוות את הפנים שנמצאו למודל (או מודלים) שהכינונו מבעוד מועד של מי שאנחנו רוצים למצוא. (face recognition)
השלב המקדים הוא להכין את המודל, ובשביל זה צריך לתת דוגמאות אימון של אותו אדם למערכת (traininng).
הAPI תומך כרגע בשלוש קריאות שמתאימות לשלושת השלבים האלו ובעוד כמה קריאות עזר כמו שמירה ואיחזור של טאגים וכו'.
כמובן, יש שימושים רבים למציאת פנים שלא מערבים זיהוי.

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

שחררנו ספריית PHP וספריית JavaScript, אבל זה לא יהיה קשה לכתוב ספריות בשפות נוספות (הAPI מבוסס HTTP, והתשובות חוזרות כJSON או XML).
תתפרעו 🙂

נ.ב:
יש מגבלה של 200 בקשות זיהוי/מציאת פנים לשעה לAPI KEY, זה בטוח מספיק להתנסות וגם לרוב השימושים, יש עוד מידע פה.

חלון מהיר

לפני כמה פוסטים שאלתי את השאלה הבאה:

נתון מערך בגודל NxN של פיקסלים כאשר כל הפיקסלים שחורים חוץ מאחוז מסויים מהם שאינו שחור ומפוזר באופן אקראי,
בהינתן קואורדינטה (x,y) מסויימת בתמונה, מצא במהירות את כל הנקודות שנמצאות בתוך חלון בגודל K על K (כאשר K קבוע בבעיה) שמרכזו (x,y), משהו כזה:

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

אז הנה הפתרון שלי:
נחלק את התמונה למשבצות בגודל KxK, ונשמור רשימה של כל הנקודות בכל משבצת.
כאשר מבקשים מאיתנו לתת את רשימת הנקודות בחלון מסויים, נסתכל בארבעת החלונות שהחלון הזה חותך ונסנן את כל הנקודות בארבעת החלונות שלא נמצאות בתוך החלון המבוקש.
זה הכל בעצם.
הביצועים של הפתרון הזה מעולים, כדי להכין את הגריד (Grid) נעבור על כל הפיקסלים בתמונה, ולכל נקודה נמצא בזמן קבוע את המשבצת שאליה היא שייכת.
כשממש מחפשים את הנקודות בחלון מסויים, נבדוק במקרה הגרוע 2K*2K פיקסלים (אם כל הנקודות לבנות) אבל במקרה הרגיל נבדוק הרבה פחות (ואם החלון לא מכיל נקודות כלל נדע את זה מהר מאוד).
זה הקוד של העיבוד המקדים:
[code lang="java"]
private static int [][][] buildgrid(byte[][] image, int width, int height, int gridSize)
{
int grid[][][] = new int[width / gridSize][height / gridSize][];
// count the number of points in each grid cell
int numpoints[][] = new int[width / gridSize][height / gridSize];
for(int x=0;x Byte.MIN_VALUE)
{
numpoints[x/gridSize][y/gridSize]++;
}
}
}

for(int x=0;x Byte.MIN_VALUE)
{
int gy = y/gridSize;
int gx = x/gridSize;
int list[] = grid[gx][gy];
if (list == null)
{
int np = numpoints[gx][gy];
list = new int[np * 2];
grid[gx][gy] = list;
numpoints[gx][gy] = 0;
}

list[numpoints[gx][gy]++] = x;
list[numpoints[gx][gy]++] = y;
}
}
}
return grid;
}
[/code]

הקוד הבא סופר את המספר הנקודות בתוך החלון שמרכזו (x,y)' שימו לב שהלולאות עוברות על ארבעה ריבועים בגריד בלבד:
[code lang="java"]
// window boundaries:
int rx1 = x – grid_size/2;
int ry1 = y – grid_size/2;
int rx2 = rx1 + grid_size;
int ry2 = ry1 + grid_size;

// top left grid cell (out of 2×2 group to be checked)
int gx = rx1 / grid_size;
int gy = ry1 / grid_size;

for(int grid_x=gx;grid_x= 0 && grid_x < gwidth;grid_x++) { for(int grid_y=gy;grid_y= 0 && grid_y < gheight;grid_y++) { int list[] = grid[grid_x][grid_y]; if (list == null) continue; for(int i=0;i= rx1 && dx < rx2 && dy >= ry1 && dy < ry2) { c++; } } } } [/code] יתכן שאפשר לשפר את הביצועים אם נחלק את הגריד לריבועים יותר קטנים ונכלול ריבועים שלמים שנופלים בתוך החלון בלי לבדוק כל נקודה באופן פרטני, אבל לא ניסיתי. הביצועים של הפתרון הזה די מטורפים. על Q6600 במהירות 2.4GHZ, עם תמונה בגודל 500X500 פיקסלים וחלון של 20X20, בדיקה של כמה זמן לוקח לספור את הפיקסלים בכל 250,000 החלונות האפשריים לקחה כ160 מילישניות, מה שנותן מהירות בדיקה של מעל 1500 חלונות במילישניה (או יותר ממליון וחצי חלונות בשניה), בלי תלות בגודל התמונה שנבדקת (על תמונה של 1000X1000 המהירות היתה דומה). האפלט הבאה מדגימה את האלגוריתם. אפשר להוריד אותה מפה. הקוד כלול בתוך הJAR (אפשר לפתוח את הJAR עם תוכנה לפתיחת קבצי ZIP).
בנוסף, האפלט יכולה לרוץ כתוכנית רגילה : java -jar fast_window.jar
אפשר ללחוץ בתוך האפלט כדי לסמן את הנקודות בחלון.


הדפדפן שלך לא תומך בג'אווה, ככה האפלט ניראית (רק שהיא אינטראקטיבית) :


JNotify 0.92

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

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

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

מכיוון שהרבה יותר קל לכתוב ולדבג קוד ג'אווה מאשר קוד C++, המימוש ללינוקס היה הרבה יותר יציב מזה של חלונות – שנטה להתרסק או להתקע לעיתים נדירות בכל מני סיטואציות שלא הצלחתי לשחזר.

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

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

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

JNotify התעורר.

חיבור לתהליך ג'אווה מרוחק עם VisualVM למחשב שמוגן על ידי פיירוואל

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

  • jps : מראה תהליכי ג'אווה
  • jstack : מציג את רשימת הת'רדים בתהליך ג'אווה, ומה כל אחד מהם עושה.
  • jmap: מאפשר שמירה לקובץ של תמונת הזכרון (heap dump) של תהליך ג'אווה. אפשר לבחון את הקובץ עם jhat או עם VisualVM.
  • jhat : מאפשר ניתוח קבצי heap dump שנוצרו בעזרת jmap.
  • jconsole : ממשק פשוט שמאפשר בחינה של שימוש הזכרון, שימוש בזמן מעבד ועוד.
  • visualvm: כלי חזק יותר מjconsole, שהחל מעדכון 18 של JDK 1.6.0 מגיע עם הJDK. ויז'ואל VM מאפשר גם ניתוח ביצועים פשוט (cpu and memory profiling).

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

VisualVM

אז איך בכל זאת אפשר להתחבר לתהליך ג'אווה מרוחק עם VisualVM?
התהליך קצת מורכב, ולכן אני אתעד אותו כאן:

על המחשב המרוחק:

1. צרו קובץ permissions.txt שיכיל את זה:

[code]
grant {
permission java.security.AllPermission;
};

[/code]

2. הריצו את jstatd (שמאפשר לתהליכים מרוחקים לקבל רשימה של מכונות ג'אווה שרצות על המכונה ולהתחבר אליהן) :

[code]

jstatd -J-Djava.security.policy=permissions.txt
[/code]

3. הריצו את התוכנית שלהם עם הפרמטר הבא:

[code]
java -Dcom.sun.management.jmxremote MainClass
[/code]

על המחשב המקומי:

1. צרו תעלות SSH לשרת כך:

[code]
ssh -D9696 -L1101:localhost:1099 your_server
[/code]
שימו לב שיש פה שתי 'תעלות':

  • תעלת TCP מ1101 אל 1099 בשרת המרוחק. זו תעלה שמאפשרת להתחבר אל הjstatd ישירות.
  • פרוקסי SOCKS5 שמאזין על פורט 9696

2. הפעילו את visualvm עם הפרמטרים הבאים :

[code]
visualvm -J-Dnetbeans.system_socks_proxy=localhost:9696 -J-Djava.net.useSystemProxies=true
[/code]

שאומרים לו להשתמש בפרוקסי SOCKS5 שהקמנו.

3. מתוך VisualVM, הוסיפו לחיבור המקומי חיבור לjstatd בפורט 1101.

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

מגה פוסט – שרת חדש, טיפוסי אישיות ועוד

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

שרת חדש

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

למעשה הייתי מרוצה מספיק מeSecureData (אליהם עברתי מHCOOP לפני פחות משנה וחצי).
מהירות התקשורת היתה קצת מאכזבת אבל לא היו יותר מדי תקלות.
אז למה עברתי?
במסגרת העבודה שלי בface.com (כן, אני עובד בface.com, לא חושב שיצא לי להזכיר את זה פה עד עכשיו) אני עובד מול חברת אכסון בשם CentralHost בסן דייגו, ומחזיק אצלם מספר לא מבוטל של שרתים. בשנה וחצי בשנה חצי שאני עובד שם, הייתי מאוד מרוצה מהם ומרמת התמיכה שלהם – אם כי המחיר שלהם הוא גבוה משמעותית מזה של eSecureData, לפחות למכונות החזקות שאני מחזיק שם במסגרת העבודה.
בכל אופן, יש לי יחסים מצויינים עם בעל החברה, וכששאלתי אם יש לו הצעה אלטרנטיבית שתאפשר לי להעביר את השרת שלי אליו הוא הסכים לתת לי מחשב באותו מחיר שאני מקבל בeSecureData – שזה $79 לחודש, עם יותר זכרון (4 ג'יגה במקום 2 ג'יגה), ושני הרדיסקים של 500 בתצורת רייד 1 (ראי, אם הרדיסק אחד מת לא קורה כלום ופשוט מחליפים אותו) לעומת הרדיסק אחד בלבד בשרת הישן.
העברתי בעזרתו את השרת למכונה החדשה, ולמעט שינויי הגדרות קטנים בשרת הDNS שאני מריץ, ובהגדרות הDNS ברשת הדומיינים שלי הכל פשוט עובד.
אני בטוח שיהיו עוד חבלי לידה, אבל אם אתם קוראים את זה, אתם עובדים מול השרת החדש.
אז כמו תמיד, יש לי מקום לאורחים. מי שחושב לעבור שרת למשהו יותר רציני מאיכסון משותף בגודדי שיצור קשר. (רק לאנשים שמרגישים בנוח עם SSH).

BoneCP

השתמשתי בProxool בתור Connection pool לבסיס הנתונים במשך די הרבה זמן, ועם הזמן גיליתי בעיות – שאומנם די נדירות אבל מספיק חמורות כדי שאני אחפש פתרון.
חשבתי לפתור אותן בעצמי, וגיליתי שהוא לא מתקמפל על JDK מודרני. שלחתי מייל לרשימת התפוצה שלו לגבי העניין, וחודשים עברו בלי שום תגובה ובלי הודעות נוספות.
היה ריח של פרוייקט מת באוויר, וכשנתקלתי בבעיה נוספת (100% CPU בקוד של פרוקסול לעיתים נדירות), החלטתי לחפש Connection pool אחד.
בסוף הגעתי לBoneCP שאומנם מוגדר כבטא, אבל עובד היטב – וחשוב יותר – המפתח שלו עונה לאימיילים ומתקן באגים.

טיפוסי אישיות

מי שלמד פסיכולוגיה (לא אני) מכיר את העניין הזה לעומק כנראה, אבל אני מניח שרוב הקוראים פה לא למדו פסיכולוגיה אני אכנס לעניין:
במהלך שיטוטי באינטרנט בחיפוש אחרי ספרייה לDecoding של תמונות בC או C++ (יש מילה בעברית לDecoding? פענוח? פרישה?) נתקלתי בCxImage, וראיתי בדף האודות של המפתח שהוא טוען שהאישיות שלו היא מסוג INTJ.
אני די בטוח שנתקלתי בסוג הקטלוג הזה למחלקות אישיות בעבר, אבל הפעם הסתקרנתי, אם הוא INTJ אז מה אני? וכמה הדבר הזה בכלל עובד?

התחלתי לחקור, מסתבר שמדובר במבחן פסיכומטרי שמבוסס על העבודה של קארל יאנג מ1920 ופותח על ידי קתרין קוק בריגס וביתה איזבל בריגס מיירס, והמבחן קרוי על שמן: Myers-Briggs Type Indicator או MTBI.
בגדול, הרעיון הוא שהאישיות של בני אדם נקבעת על ידי ארבעה תכונות, כאשר לכל תכונה יש שני צדדים (מוחצן – מופנם, חושב – מרגיש) וכו', ואצל כל אחד תכונה אחת מכל זוג היא יותר דומיננטית מהשניה.

הזוגות הם:

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

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

סך הכל יש ארבע 'ביטים' כשלכל ביט יש שתי אפשרויות, מה שיוצר 16 קומבינציות.

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

בעיית החלוקה

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

הרחבה טבעית של השאלה הזו, היא איך מחלקים את הקבוצה לk קבוצות, לאו דווקא רק לשתיים.
הנה כמה דוגמאות למצבים שבהם אלגוריתם כזה יהיה שימושי:

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

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

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

Java with authenticated urls

התקן של HTTP כולל קביעה של שם משתמש וסיסמא ישירות בURL, למשל:
[code]
http://user:password@site.com/file.txt
[/code]

מסיבה לא ברורה, ג'אווה לא מסתדר עם URLים כאלו ונראה שהוא לא מעביר את שם המשתמש והסיסמא לאתר.
הנה פתרון לעניין (עקום אך גנרי) :
[code lang="java"]
Authenticator.setDefault(new Authenticator()
{
@Override
protected PasswordAuthentication getPasswordAuthentication()
{
URL url = getRequestingURL();
String userInfo = url.getUserInfo();
String user;
String password;
int i = userInfo.indexOf(':');
if (i == -1)
{
user = userInfo.substring(0);
password = "";
}
else
{
user = userInfo.substring(0, i);
password = userInfo.substring(i+1);
}
PasswordAuthentication ps = new PasswordAuthentication(user, password.toCharArray());
return ps;
}
});
[/code]

עוגיה למי שסביר למה המימוש הסטנדרטי של ג'אווה לא תומך בדבר הטריויאלי הזה.