2011年9月1日星期四

Konsole 里 Session Profile 命令的解析过程

Creative Commons License
本作品采用知识共享署名-非商业性使用 2.5 中国大陆许可协议进行许可。

如果没有强大的动力,我基本是不会主动去读 C++ 的源代码的。事情的起因其实很简单, 就是 Jesse#beihang-osc@freenode.net 上吼,想在 gnome-terminal 里不用 gconf 实现 http://vim.wikia.com/wiki/Change_cursor_shape_in_different_modes 里的效果。他想 看看 Konsole 是怎么实现的。作为伪 KDE 控的我当然不能放过这个机会啦……

第一步是从 anongit.kde.org 上 clone konsole 的源代码。无他,主要是为了打 patch 方便和那个无比好用的 git grep ;P 写这篇文章的时候 git HEAD 是 d6ca64fe4d。

然后这逛那逛的也一直没有头绪。忽然想,那个指令不是 "\<Esc>]50;CursorShape=1\x7""\<Esc>]50;CursorShape=0\x7" 么,干脆 git grep -n ']50' 试试。嘿,还真找到了:

src/Part.cpp:276: buffer.append("\033]50;").append(text.toUtf8()).append('\a');
src/konsoleprofile:3:/bin/echo -e "\033]50;$1\a"
lines 1-2/2 (END)

火速去 src/Part.cpp 第 276 行看:

void Part::changeSessionSettings(const QString& text)
    {
        // send a profile change command, the escape code format
        // is the same as the normal X-Term commands used to change the window title or icon,
        // but with a magic value of '50' for the parameter which specifies what to change
        Q_ASSERT( activeSession() );
        QByteArray buffer;
        buffer.append("\033]50;").append(text.toUtf8()).append('\a');

        activeSession()->emulation()->receiveData(buffer.constData(),buffer.length());
    }

注释说的挺清楚了,而且 033 就是 ASCII 的 ESC,a 是 x7。一切都对应上了。最后它 把指令送给了 activeSession()->emulation()->receiveData 。省略中间痛苦的寻找 过程直接说了,konsole 里每一个窗口/Tab都会有一个 Session, activeSession() 获取的是当前用户正在使用的 Session。每个 Session 里都会有一个 emulation 来模拟 terminal,处理用户的输入输出(src/Session.h 的 136 行)。 Session->emulation() 获取的就是它。具体的东西是 src/Session.cpp 129 行的 _emulation = new Vt102Emulation(); 嗯,跳去 src/Vt102Emulation.cpp 找 receiveData ,木有找 到…… .h 里也没有…… 想起来有可能在鸡肋里, Vt102Emulation 是继承 Emulation 的,于是再跳到 src/Emulation.cpp 看 receiveData

/*
       We are doing code conversion from locale to unicode first.
    TODO: Character composition from the old code.  See #96536
    */

    void Emulation::receiveData(const char* text, int length)
    {
        emit stateSet(NOTIFYACTIVITY);

        bufferedUpdate();

        QString unicodeText = _decoder->toUnicode(text,length);

        //send characters to terminal emulator
        for (int i=0;i<unicodeText.length();i++)
            receiveChar(unicodeText[i].unicode());

        //look for z-modem indicator
        //-- someone who understands more about z-modems that I do may be able to move
        //this check into the above for loop?
        for (int i=0;i<length;i++)
        {
            if (text[i] == '\030')
            {
                if ((length-i-1 > 3) && (strncmp(text+i+1, "B00", 3) == 0))
                    emit zmodemDetected();
            }
        }
    }

略掉编码转换和 z-modem 的过程,这个 receiveData 就是把东西送给了 receiveChar 。再找 receiveChar

// process application unicode input to terminal
    // this is a trivial scanner
    void Emulation::receiveChar(int c)
    {
        c &= 0xff;
        switch (c)
        {
            case '\b'      : _currentScreen->backspace();                 break;
            case '\t'      : _currentScreen->tab();                       break;
            case '\n'      : _currentScreen->newLine();                   break;
            case '\r'      : _currentScreen->toStartOfLine();             break;
            case 0x07      : emit stateSet(NOTIFYBELL);                   break;
            default        : _currentScreen->displayCharacter(c);         break;
        };
    }

不会这么简单吧!又忽然想起来, Emulation::receiveData 可能会调用 Vt102Emulation::receiveChar 的吧…… 在 src/Emulation.h 的 427 行,这货果然是 virtual 的。于是再返回 src/Vt102Emulation.cpp 找 receiveChar 。这回终于 找到处理字符序列的地方了。不过因为整个函数有101行,还要加上前面18行注释和15行宏 ,就不贴在这里了。那个函数主要是把用户输入 tokenize ,并且对 token 进行处理。这 整个过程我还不是完全理解,但是大概的内容可以猜的出来。对于本文起作用的主要是第 316 行

if (Xte         ) { processWindowAttributeChange(); resetTokenizer(); return; }

Xte 是个判断 \033] 的宏。(好吧,它其实只判断了 token 的位置和 ']') processWindowAttributeChange 就在 receiveChar 的下面

void Vt102Emulation::processWindowAttributeChange()
    {
      // Describes the window or terminal session attribute to change
      // See Session::UserTitleChange for possible values
      int attributeToChange = 0;
      int i;
      for (i = 2; i < tokenBufferPos     && 
                  tokenBuffer[i] >= '0'  && 
                  tokenBuffer[i] <= '9'; i++)
      {
        attributeToChange = 10 * attributeToChange + (tokenBuffer[i]-'0');
      }

      if (tokenBuffer[i] != ';') 
      { 
        reportDecodingError(); 
        return; 
      }
      
      QString newValue;
      newValue.reserve(tokenBufferPos-i-2);
      for (int j = 0; j < tokenBufferPos-i-2; j++)
        newValue[j] = tokenBuffer[i+1+j];
     
      _pendingTitleUpdates[attributeToChange] = newValue;
      _titleUpdateTimer->start(20);
    }

前半部分是提取 \033';' 中间的数字,然后把剩下的字串放到 _pendingTitleUpdates 里给别人处理。这里作者启动了一个 20ms 的计时器,计时器到时 间之后才会更新。这可以压缩更新的次数,避免频繁更新吧。计时器的 callback 就在下 面

void Vt102Emulation::updateTitle()
    {
        QListIterator<int> iter( _pendingTitleUpdates.keys() );
        while (iter.hasNext()) {
            int arg = iter.next();
            emit titleChanged( arg , _pendingTitleUpdates[arg] );    
        }
        _pendingTitleUpdates.clear();    
    }

简单的函数,它又发出了 titleChanged 这个信号。这个信号是在哪处理的呢?(中间 省略N多 git grep 之类的过程)是在 src/Session.cpp 的 Session::setUserTitle

void Session::setUserTitle( int what, const QString &caption )
    {
    ....
        if (what == ProfileChange) 
        {
            emit profileChangeCommandReceived(caption);
            return;
        }
    ....
    }

这个 ProfileChange 就等于我们所要的 50(src/Session.h, 341 行) ……再追踪 profileChangeCommandReceived 这个信号。(别急,快完啦)处理它的是 src/SessionManager.cpp 里的 SessionManager::sessionProfileCommandReceived

void SessionManager::sessionProfileCommandReceived(const QString& text)
    {
        // FIXME: This is inefficient, it creates a new profile instance for
        // each set of changes applied.  Instead a new profile should be created
        // only the first time changes are applied to a session

        Session* session = qobject_cast<Session*>(sender());
        Q_ASSERT( session );

        ProfileCommandParser parser;
        QHash<Profile::Property,QVariant> changes = parser.parse(text);

        Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
        
        QHashIterator<Profile::Property,QVariant> iter(changes);
        while ( iter.hasNext() )
        {
            iter.next();
            newProfile->setProperty(iter.key(),iter.value());
        } 

        _sessionProfiles[session] = newProfile;
        applyProfile(newProfile,true);
        emit sessionUpdated(session);
    }

还是一个挂着 FIXME 的函数呢…… 不过逻辑还是比较简单的,基本上就是把当前的Profile 作为父 Profile 新建一个 Profile。然后根据命令的内容修改 Profile 的属性。也就是说 ,理论上讲,只要是 Profile 里可以改的,就可以通过 \<ESC>50;x1=y1;x2=y2\x7 来 修改。后来我又把 vim 里的 t_{S,E}I 修改成

if $TERM =~ 'xterm'
        let &t_SI = "\<Esc>]50;CursorShape=1;BlinkingCursorEnabled=true\x7"
        let &t_EI = "\<Esc>]50;CursorShape=0;BlinkingCursorEnabled=false\x7"
endif

然后在插入模式下,光标果然编程一闪一闪的竖线了。哈哈。不过需要小注意的是,用这个 方式修改的 Profile 是临时的,不会保存,新建的标签也不会继承这个 Profile。

现在就拨云见日,回顾一下整个调用过程吧

Emulation::receiveData
    ||
    \/
Vt102Emulation::receiveChar
    || tokenize/process token
    \/
Vt102Emulation::processWindowAttributeChange
    || 提取 \<ESC>] 后面的 code 和 cmd
    \/ 20ms 延迟,聚集变更
Vt102Emulation::updateTitle
    || emit titleChanged(code, cmd)
    \/
Session::setUserTitle(int, const QString &)
    || emit profileChangeCommandReceived(cmd)
    \/
SessionManager::sessionProfileCommandReceived(const QString)
{
    ...
    Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
    ...
    newProfile->setProperty
    ...
    _sessionProfiles[session] = newProfile;
    applyProfile(newProfile,true);
    emit sessionUpdated(session);
}

回头来看,一步一步的到还挺清晰的。

多谢各位能够读到最后。作为奖励,贴一个解决上面 FIXME 的补丁吧,哈哈

commit 5fb452e51ac1a9d18952fdd26f8bfa55438aedf3
Author: Grissiom <chaos.proton@gmail.com>
Date:   Thu Sep 1 01:17:24 2011 +0800

    use a static _sessionRuntimeProfiles to store runtime profiles

diff --git a/src/SessionManager.cpp b/src/SessionManager.cpp
index 028b76f..697589c 100644
--- a/src/SessionManager.cpp
+++ b/src/SessionManager.cpp
@@ -758,9 +758,7 @@ Profile::Ptr SessionManager::findByShortcut(const QKeySequence& shortcut)

 void SessionManager::sessionProfileCommandReceived(const QString& text)
 {
-    // FIXME: This is inefficient, it creates a new profile instance for
-    // each set of changes applied.  Instead a new profile should be created
-    // only the first time changes are applied to a session
+    static QHash<Session*,Profile::Ptr> _sessionRuntimeProfiles;

     Session* session = qobject_cast<Session*>(sender());
     Q_ASSERT( session );
@@ -768,14 +766,23 @@ void SessionManager::sessionProfileCommandReceived(const QString& text)
     ProfileCommandParser parser;
     QHash<Profile::Property,QVariant> changes = parser.parse(text);

-    Profile::Ptr newProfile = Profile::Ptr(new Profile(_sessionProfiles[session]));
-
+    Profile::Ptr newProfile;
+    if (!_sessionRuntimeProfiles.contains(session))
+    {
+        newProfile = new Profile(_sessionProfiles[session]);
+        _sessionRuntimeProfiles.insert(session,newProfile);
+    }
+    else
+    {
+        newProfile = _sessionRuntimeProfiles[session];
+    }
+
     QHashIterator<Profile::Property,QVariant> iter(changes);
     while ( iter.hasNext() )
     {
         iter.next();
         newProfile->setProperty(iter.key(),iter.value());
-    }
+    }

     _sessionProfiles[session] = newProfile;
     applyProfile(newProfile,true);

C++ 代码看的比较少,Konsoel 的代码也是刚看。有什么不对的地方还请指教~;P