Archive for April, 2009

使用 Artistic Style 对代码进行重新排版

2009-4-29 23:12 | by 2ndboy

  今天又捡起 AStyle 用了一下,是因为在看 Live555 的时候发现它的代码风格实在是跟我平时所习惯的大不相同,于是看代码之前都先用 AStyle 处理一下。这下,整个世界清静了!

  晚上干脆又看着 AStyle 的 document 整理出一份最适合自己的配置来,以后再看别人写的代码的时候方便一些,既然捡起来了,总要有所收获吧:)

最终命令行配置如下:
astyle.exe -A1 -T4 -S -w -m0 -p -D -v sample.cpp

各 option 简单解释如下:
-A1: 用 ANSI 预定义 style 来重新缩进,每个大括号单独一行
-T4: 强制使用 Tab 做缩进,一个 Tab 相当于 4 个空格
-S: 缩进 switch 中的 case 语句块
-w: 对多行宏定义做缩进
-m0: 设定多行条件表达式的最小缩进量为 0(不做缩进)
-p: 在操作符两端加空格
-D: 在函数调用的参数列表两端加空格
-v: 啰嗦模式

  为了在 VC 中使用方便,可以把 AStyle 设置成 Visual Studio IDE 的 tool(下面以 VC 6 为例说明):
1) 把 AStyle.exe 复制到 C:\Windows 下(PATH 设定中一般都包括系统目录,所以复制到这里,也方便把 AStyle 用到其它地方)
2) VC 菜单 Tools -> Customize… -> Tools Tab,新增一项“AStyle”
3) Command 中填“AStyle.exe”;Arguments 中填“-A1 -T4 -S -w -m0 -p -D -v $(FileName)$(FileExt)”;Initial Directory 不用填;选中“Use Output Window”(这样 AStyle 运行时的输出会被捕获到 VC 的 Output 窗口中,就不会出现一个控制台窗口了)
4) VC 菜单 Tools -> Options… -> Editor Tab,选中“Save Options”下面的“Save before running tools”(这样可以避免在没有存盘时使用 AStyle 导致未存盘的修改丢失)
Add AStyle for VC 6

设置好以后,想对当前打开的代码进行重新排版只需要点一下 Tools 菜单下的“AStyle”即可,非常方便!如果想再方便一点点,可以进入 Tools -> Customize… -> Commands Tab,然后把 Category 切换到 Tools,再把 AStyle 对应的图标拖到工具栏上去,以后想重排代码只需要点一下 Toolbar 上的按钮,少了一次点击:D

断点上传程序的一点心得

2009-4-25 21:42 | by 2ndboy

  最近两天花时间把之前做的基于文件的断点上传 Python 程序改成了基于数据块的断点上传,粒度变小之后,上传被打断后可以接着单个文件被打断的地方继续上传,而不用再传被打断之前就已经上传过的部分。心得记录如下:

1) 普通的文件上传用的是 STOR(STORE) 命令,断点上传需要用 APPE(APPEND) 命令:
两者的格式和参数一样,APPE 也没有用来指定 offset 的地方,所以 client 自己必须知道已经上传了多少字节,然后打开文件后把文件读指针设置到相应的地方再进行上传,Server 会把收到的数据附加到指定文件的后面。

2) 不能信赖 storbinary() 回调给出的上传数据 size:
由于上传期间随时可能断线,所以我在程序里维护了一个上传队列,里面记录了每个文件已经上传的 size,这个值是通过 storbinary() 的 callback 拿到的,示例代码如下:

  1. def UpdateSize( total_size, delta_size ):
  2.      total_size[0] = total_size[0] + delta_size
  3.  
  4.  ftp = ftplib.FTP( 'localhost' )
  5.  ftp.login( 'anonymous', '1@1.com' )
  6.  size = [0]
  7.  file = open( sys.path[0] + os.sep + 'test.txt', 'rb' )
  8.  ftp.storbinary( 'STOR /test.txt', file, 8192, lambda data: UpdateSize( size, len( data ) ) )
  9.  file.close()
  10.  print( size[0] )

上面这段示例代码有两个需要注意的地方:
a. lambda 里不能用赋值语句,所以要通过再去调用一个函数来达到计数的目的
b. Python 里 int 类型的变量不能传引用,内容一旦改变就会另建一个新的 int 变量,所以示例里用了一个 list 来保存上传的 size 而没有直接用一个 int 变量
(这里有更优雅的方法来达到同样的目的吗?路过的高人请留言:))

但是经过测试,用 storbinary() 的 callback 记录下来的已上传 size 在断线的情况下几乎都是不准确的,全部大于真正上传的数据 size。所以这个数值不是可信的,为了解决这个问题,我用了 ftplib 里的 FTP.size() 对这个值进行校正,效果不错。

顺便提一句,SIZE 命令不是标准所定义的,所以你在 RFC959 上是找不到这个命令的,但是大多数主流的 FTP Server 都支持这个命令,基本上可以放心使用。

3) 上传时处理文件要用双重 try-except:
《A Byte of Python》的 Chapter 13 当中在介绍不管有无异常发生,文件都能被关闭时用了如下一段代码:

  1. try:
  2.      f = file( 'test.txt' )
  3.      #... ...
  4.  finally:
  5.      f.close()

不管是不是由于 Python 2.x 跟 3.0 的语法差异导致的,总之这段代码在我的 Python 3 环境下是运行不了的,看来 try 跟 finally 块的变量作用域是不同的。倒是在《Dive Into Python》的 6.2.3 找到一段不错的实现:

  1. try:
  2.      fsock = open( filename, 'rb' )
  3.      try:
  4.          #... ...
  5.      finally:
  6.          fsock.close()
  7.  except:
  8.      pass

貌似最近有关技术的 post 很多,所以,没有输入就没有输出:D

50 State Quarters 齐了

2009-4-13 22:23 | by 2ndboy

  从去年三月份飞赴加州起,一直到上周五刚从 US 回来的同事帮我带回 08 年的最后 2 枚,我的 50 州 25 美分收集计划终于圆满结束。耗时将近一年又一个月:D

  目前我手里齐全的是一套 D 版(这里 D 版不是指盗版,指的是硬币反面有大写字母 D 的版本,D 表示 Denver——丹佛造币厂,P 表示 Philadelphia——费城造币厂),另外还有一套 P 版的差 3 枚(但是其中一些也是 D 版的:(),看来在西海岸收集 P 版的硬币是有些困难。

下面展示下 The United States Mint 50 State Quarters Program 的全景图(写程序从 Mint 网站上抓下来后拼合的):
50 State Quarters

  上周五得到的就是上图最后一排的最后 2 枚,分别是最晚加入美国的阿拉斯加(1959/1/3)和夏威夷(1959/8/21)。

  50 个 25 美分的币值是 $12.5,按今天的中行折算价 6.83 来算,一套 50 State Quarters 合人民币 ¥85.375,但是淘宝上居然有人卖 300 多,真不是一般黑啊!

Discuz! 6.1 用户密码的存放和生成方式

2009-4-12 13:00 | by 2ndboy

  自 Discuz! 6.1 以后,装 Discuz! 就必须安装 UCenter。经过分析,事实上自从跟 UCenter 集成以后,Discuz! 的用户密码就不是存放在 Discuz! 自己的 cdb_members 表里了,而是放在 UCenter 库里的 uc_members 表里。

  Discuz! 目录下的 register.php,其中向 cdb_members 表里插数据时密码的生成方式如下:

  1.  $password = md5( random( 10 ) );
  2.  ?>

只是用一个随机数的 MD5 值填充了用户密码字段,这跟 admin/members.inc.php 里的做法是一样的,根本没有使用用户自己输入的密码(可见即便 hacker 拿到了 Discuz! 库中用户表的数据,也没办法用 MD5 碰撞的方法找出一个可以登录的用户密码)。

  真正的用户密码保存在 UCenter 库的 uc_members 表里,代码在 uc_client/model/user.php 里:

  1.  function add_user($username, $password, $email, $uid = 0)
  2.  {
  3.      $salt = substr(uniqid(rand()), -6);
  4.      $password = md5(md5($password).$salt);
  5.      $sqladd = $uid ? 'uid=\''.intval($uid).'\',' : '';
  6.      //$appid = UC_APPID;
  7.      $this->db->query("INSERT INTO ".UC_DBTABLEPRE."members SET $sqladd username='$username', password='$password', email='$email', regip='".$this->base->onlineip."', regdate='".$this->base->time."', salt='$salt'");
  8.      $uid = $this->db->insert_id();
  9.      $this->db->query("INSERT INTO ".UC_DBTABLEPRE."memberfields SET uid='$uid'");
  10.      return $uid;
  11.  }
  12.  ?>

(UC_DBTABLEPRE 的定义是 define(‘UC_DBTABLEPRE’, ‘`ucenter`.uc_’);)可以看出来,这里的密码生成算法跟生成 UCenter 创始人密码的算法是一样的,都是用一个加扰串结合原始密码的 MD5 再做一次 MD5。

  知道了以上原理,就可以手工对用户名密码做一些处理了。

WMF SDK 心得小记

2009-4-3 20:37 | by 2ndboy

  貌似最近都是在写业余研究的东西,今天也偶尔写写工作上的东西:-D

  最近两周因为项目需要都在研究 server 和 Windows Media Format SDK,今天就记录一下研究 wmvcopy 这个示例程序时如何把 wmvcopy 改写成让 reader 读 WMV 文件,decode 后拿到 raw data,然后再让 writer encode 这些数据得到一个新的 WMV 的方法。

  原始的 wmvcopy 为了仅仅实现原样复制 WMV,所以让 reader 从 WMV 中读出 compressed data,然后交给 writer 写入另外一个 WMV 文件中,它旁路了 reader 的 decode 和 writer 的 encode 过程,这是通过下面步骤实现的:

1) 在对 reader 进行设置时,调 IWMReaderAdvanced::SetReceiveStreamSample() 的时候第二个参数给的是 TRUE,指示 reader 应用程序想要接收 stream sample/compressed sample,这就旁路了 reader 的 decoder

2) 在 WMF SDK 中,compressed sample 是通过 IWMReaderCallbackAdvanced::OnStreamSample() 回调得到的,uncompressed sample 是通过 IWMReaderCallback::OnSample() 回调得到的。由于 wmvcopy 只是原样复制,所以它只把核心处理逻辑放到了 OnStreamSample() 里

3) 在对 writer 进行设置时,调 IWMWriter::SetInputProps() 的时候第二个参数是 NULL,指示 writer 不用为输入数据配置编码器,这就旁路了 writer 的 encoder

4) 在收到 sample 后,wmvcopy 通过调用 IWMWriterAdvanced::WriteStreamSample() 把 compressed sample 写至最终去向,而查阅 WMF SDK 文档可知,向 writer 写入 uncopressed sample 需要调用 IWMWriter::WriteSample()

5) 由于是原样复制 WMV audio/video 内容,所以 wmvcopy 需要尽可能快的数据处理速度,为了做到这一点,在配置 reader 的时候,它启用了 user provided clock。如果想让 reader 以播放速度来取数据,只把 IWMReaderAdvanced::SetUserProvidedClock() 这个调用传的值由 TRUE 改成 FALSE 是不够的,IWMReaderAdvanced::SetUserProvidedClock() 的文档里有这样的话:

User-provided clocks are only supported when the source file is a local file.

This method can fail if the current source does not support user-provided clocks.
To drive a clock, an application must call DeliverTime, and then wait for IWMReaderCallbackAdvanced::OnTime to reach the time specified.

果然在 CWMVCopy::OnStatus() 的 case WMT_STARTED 里发现确实这里调用了 IWMReaderAdvanced::DeliverTime(),把这里相关的代码注释掉,不用管 OnTime(),然后就可以让 reader 按播放速度去 decode sample 了。

6) 由于只是想要原样复制 WMV 内容,所以 wmvcopy 给 writer 设置了跟从原始文件中读出后取得的 peofile 一模一样的 peofile。由于使用的 profile 一致,所以 reader 的 output 的 audio stream index 可以对应 writer input 的 audio stream index,reader output 的 video stream index 也可以对应 writer input 的 video stream index。这从 wmvcopy 对 OnStreamSample() 的处理上也能看出来:

  1. HRESULT
  2.  CWMVCopy::OnStreamSample(
  3.      /* [in] */ WORD wStreamNum,
  4.      /* [in] */ QWORD cnsSampleTime,
  5.      /* [in] */ QWORD cnsSampleDuration,
  6.      /* [in] */ DWORD dwFlags,
  7.      /* [in] */ INSSBuffer __RPC_FAR *pSample,
  8.      /* [in] */ void __RPC_FAR *pvContext)
  9.  {
  10.      ... ...
  11.      hr = m_pWriterAdvanced->WriteStreamSample(
  12.          wStreamNum,
  13.          cnsSampleTime,
  14.          0,
  15.          cnsSampleDuration,
  16.          dwFlags,
  17.          pSample );
  18.      ... ...
  19.  }

OnStreamSample() 的 wStreamNum 参数被直接用于对 WriteStreamSample() 的调用。这就表示——比如 reader output 的第 0 个 stream 是 audio stream,那么 writer input 的第 0 个输入也是用来接收 audio stream 的。

但是对于 writer 使用了跟 reader 不一样的 profile 这种情况,writer 的第 0 个 input 不一定是用来接收 audio stream 的,所以需要应用程序自己去检查。

下面是检查 reader output stream index 和 audio/video 对应情况的示例代码:

  1. IWMProfile *pReaderProfile = NULL;
  2.  pReader->QueryInterface( IID_IWMProfile, (void **)&pReaderProfile );
  3.  
  4.  DWORD dwStreamCount = 0;
  5.  pReaderProfile->GetStreamCount( &dwStreamCount );
  6.  
  7.  IWMStreamConfig *pStreamConfig = NULL;
  8.  for( DWORD i = 0; i < dwStreamCount; i++ )
  9.  {
  10.      m_pReaderProfile->GetStream( i, &pStreamConfig );
  11.  
  12.      WORD wStreamNumber = 0;
  13.      pStreamConfig->GetStreamNumber( &wStreamNumber );
  14.      pReaderAdvanced->SetReceiveStreamSamples( wStreamNumber, FALSE )// we want to receive uncompressed sample
  15.  
  16.      IWMMediaProps *pMediaProperty = NULL;
  17.      pStreamConfig->QueryInterface( IID_IWMMediaProps, (void **)&pMediaProperty );
  18.  
  19.      GUID type;
  20.      pMediaProperty->GetType( &type );
  21.      if( WMMEDIATYPE_Audio == type )
  22.      {
  23.          dwReaderOutputStreamIndex_Audio = i;
  24.      }
  25.      else if( WMMEDIATYPE_Video == type )
  26.      {
  27.          dwReaderOutputStreamIndex_Video = i;
  28.      }
  29.  }

下面是检查 writer input stream index 和 audio/video 对应情况的示例代码:

  1. DWORD dwInputCount = 0;
  2.  pWriter->GetInputCount( &dwInputCount );
  3.  
  4.  IWMInputMediaProps *pInputMediaProps = NULL;
  5.  for( DWORD i = 0; i < dwInputNum; ++i )
  6.  {
  7.      hr = pWriter->GetInputProps( i, &pInputMediaProps );
  8.  
  9.      GUID type;
  10.      pInputMediaProps->GetType( &type );
  11.  
  12.      if( WMMEDIATYPE_Audio == type )
  13.      {
  14.          dwWriterInputIndex_Audio = i;
  15.      }
  16.      else if( WMMEDIATYPE_Video == type )
  17.      {
  18.          dwWriterInputIndex_Video = i;
  19.      }
  20.  }

拿到了对应关系之后,在处理 OnSample() 时就可以以这样:

  1. HRESULT
  2.  CWMVCopy::OnSample(
  3.      /* [in] */ DWORD dwOutputNum,
  4.      /* [in] */ QWORD qwSampleTime,
  5.      /* [in] */ QWORD qwSampleDuration,
  6.      /* [in] */ DWORD dwFlags,
  7.      /* [in] */ INSSBuffer __RPC_FAR *pSample,
  8.      /* [in] */ void __RPC_FAR *pvContext )
  9.  {
  10.      DWORD dwInputIndex = 0;
  11.      if( dwReaderOutputStreamIndex_Audio == dwOutputNum )
  12.      {
  13.          dwInputInput = dwWriterInputIndex_Audio;
  14.      }
  15.      else if( dwReaderOutputStreamIndex_Video == dwOutputNum )
  16.      {
  17.          dwInputIndex = dwWriterInputIndex_Video;
  18.      }
  19.      else
  20.      {
  21.          return( S_OK );
  22.      }
  23.  
  24.      m_pWriter->WriteSample( dwInputIndex, qwSampleTime, dwFlags, pSample );
  25.      return( S_OK );
  26.  }

《Windows Media 编程导向》上讲到对接 reader output 和 writer input 时用的方法是比较 input 和 output 的 connection name,但是我在调试中发现 writer input 的 connection name 始终拿不到,只能拿到 reader output 的(类似“0”,“1”这类字串值),所以我就用了上面的方法。

7) 如果想以不同的参数对输入 WMV 重新进行编码,那么就需要给 writer 设置一个跟 reader 不一样的 profile。这个目标的正规做法是创建一个空 profile,然后给这个 profile 添加几个 stream,然后设置 stream 的格式,但是过程非常复杂。其实还有近路,那就是直接使用 system profile 或者其它现成的 profile。

使用 system profile 的方法是调用 IWMWriter::SetProfileByID(),参数是 system profile 的 GUID,比如 WMProfile_V80_56Video。

profile 也可以保存成 .prx 文件,其实就是一个 profile 的 XML 描述,这就方便了我们直接使用现成的 profile 来设置 writer,下面是把一个存在于磁盘上的 .prx 文件加载,获得一个 profile 实例的代码:

  1. HRESULT
  2.  LoadProfileFromFile(
  3.      /* [in] */  IWMProfileManager  *pManager,
  4.      /* [out] */ IWMProfile        **ppProfile,
  5.      /* [in] */  const char         *pFilename )
  6.  {
  7.      HRESULT hr           = S_OK;
  8.      HANDLE  hFile        = INVALID_HANDLE_VALUE;
  9.      LPWSTR  pProfileData = NULL;
  10.     
  11.      do 
  12.      {
  13.          hFile = CreateFile(
  14.              pFilename,
  15.              GENERIC_READ,
  16.              FILE_SHARE_READ,
  17.              NULL,
  18.              OPEN_EXISTING,
  19.              FILE_ATTRIBUTE_NORMAL,
  20.              NULL );
  21.          if( INVALID_HANDLE_VALUE == hFile )
  22.          {
  23.              hr = HRESULT_FROM_WIN32( GetLastError() );
  24.              break;
  25.          }
  26.          if( FILE_TYPE_DISK != GetFileType( hFile ) )
  27.          {
  28.              hr = NS_E_INVALID_NAME;
  29.              break;
  30.          }
  31.          DWORD dwLength = GetFileSize( hFile, NULL );
  32.          if( -1 == dwLength )
  33.          {
  34.              hr = HRESULT_FROM_WIN32( GetLastError() );
  35.              break;
  36.          }
  37.          pProfileData = (WCHAR *)new BYTE[dwLength + sizeof( WCHAR )];
  38.          if( NULL == pProfileData )
  39.          {
  40.              hr = E_OUTOFMEMORY;
  41.              break;
  42.          }
  43.          memset( pProfileData, 0, dwLength + sizeof( WCHAR ) );
  44.          DWORD dwBytesRead = 0;
  45.          if( !ReadFile( hFile, pProfileData, dwLength, &dwBytesRead, NULL ) )
  46.          {
  47.              hr = HRESULT_FROM_WIN32( GetLastError() );
  48.              break;
  49.          }
  50.          hr = pManager->LoadProfileByData( pProfileData, ppProfile );
  51.          if( FAILED( hr ) )
  52.          {
  53.              break;
  54.          }
  55.      }
  56.      while( false );
  57.     
  58.      if( pProfileData )
  59.          delete []pProfileData;
  60.      if( hFile )
  61.          CloseHandle( hFile );
  62.      return( hr );
  63.  }

至于现成的 .prx,可以使用 WMF SDK 的 wmgenprofile,也可以用《Windows Media 编程导向》随书光盘里的一个小工具 ShowProfile。