Sunset shots in Howarth Park, Everett
History Blog
★、周朝时候流行两句话叫“普天之下,莫非王土,率土之滨,莫非王臣。”流行的程度跟今天我们的一些房地产口号类似。
★、高祖刘邦以前是秦朝的亭长,用今天的话说就是街道居委会治保主任。
★、只有中国人、朝鲜半岛、日本、越南、蒙古、匈牙利这六个地方的人是姓在前,名在后,叫李小二,其他全是小二·李。
★、韩国人说韩国文化不追求宏大,追求的是精巧,你倒想追求宏大,别说你横着修,竖着修也修不来啊,朝鲜号称三千里江山是竖着量的,只能修十分之三,要不然就往海里修。
★、骑兵打步兵,那不跟德国队踢中国队似的吧,我想进几个球就进几个球。北京奥运会我给你留点脸,我5分钟进一个,我不给你留脸半分钟就进一个,180比0。
★、蔡伦是个宦官,宦官一般没什么好人,只有两个不错的,青史留名,蔡伦和郑和。
★、……不造反难道造飞机?
★、我们中原人的打扮是峨冠博带,老高的帽子,老长的大袖子,一走路帽子当避雷针,袖子当拖把,既省电又干净。
★、中国历史上的皇帝,多半嫔妃一大票。其中数量最多的可能是伪天王洪秀全,一百多个,没有名字只有编号,今天从零零一睡到零零七,明天从二百三睡到二百五。
★、朝鲜人写唐诗的水平确实相当高,日本人也能写,但日本人写的没有韩国人纯正,再往下的话越南人也能写,那基本上就是打油诗了。
★、中国没有尚武精神,好男不当兵,好铁不打钉,你一心系国家的秀才去背弓拉箭,那什么玩意儿。
★、有人说中国人轻家国而重乡土,勇于私斗,怯于公战。打架勇敢着呢,你看我媳妇一眼我跟你没完。外敌入侵就胆小,异形打过来了,我躲着。
★、那箭头就是长矛,本来是射马、射城垛子的,这次拿来射辽军主帅萧挞懒,钉地下拔都拔不出来。等于一开战辽国的主帅就战死了,变成了大地标本。
★、西方一哥伦布之后,无数继起之哥伦布,中国一郑和之后,数百年以下,再无郑和。
★、中国人一看傻眼了,但是那会儿是很讲信用的,要搁今天,这事儿不会发生。谁答应你的,有合同吗?有合同拿来一撕,不就完了嘛!
★、你上大学的目的是为祖国?别开玩笑了,这么多年也就周总理说过要为中华之崛起而读书,这还被某些人笑他楞。
高天流云:一篇采访笔记
╭══╮
╭╯ΘΘ║
╰⊙═⊙╯。oо○
中国图书商报有篇采访,借机说出了自己对历史写作的一些看法。试与朋友们勾通,觉得我的看法怎样。
一,媒体把好玩、有趣、诙谐等历史写作方式形容为历史另类写作,您对另类写作的定义是什么?
我个人认为,这不是一种所谓的“另类”,之所以显得和从前的历史书不同,只是多了点代入感,比如用词的现代化、生活化、口语化。让读者觉得平易些。
如果一定要说“另类”,它应该定义在写书的环境与从前不同了。
现在的作家,尤其是草根类写手,他们可以自由地写出自己的感悟,不会像前辈们有诸多的限制。尤其是在改革开放以后成长起来的年青人,他们的价值观,人生观,都和老一辈人截然不同。
不同的观点,造成了对历史事件的不同解读,这样的产物,就是现在市场上出现的鲜活、大胆、入情入理,与平民百姓息息相关,让他们产生共鸣的作品。
至于定义,是没有定义。这正是这一代草根写手们的可贵之处,自由发挥,写出他们所想的东西
没有定义,是最大的定义。
二、选择用这种另类写作的方法来写史,当时的初衷是什么?您的写作标准是什么?
从前我不知道我可以写历史题材,就像卡夫卡不知道什么是小说。我一直记得某一天,在书店里看到易中天等人的作品,实话实说,我觉得书上的内容,甚至写作的风格,都是我和几个历史同好平日里闲聊的东西。
如果这可以成书,那么我想我也可以做到。呵呵。
至于我的标准,就是不要停留在调侃历史的层面上。历史不是故事,它是学问,它是教训。可以轻松地说,但不可以调侃地去写。尤其是宋史,它是中华民族的大转折点,有太多沉重的东西在里面。
轻松平易只是让读者们更容易接受的方式,绝不是我要的结果!并且,一定要注意,历史写作,尤其是现在阶段里,有一种非常恶劣的风气。那就是把刻薄当才气,把调侃当幽默。
我可以点名,就以袁腾飞为例。回顾国破家亡的时代,不管是什么时代的讲师或者写手,都应该有一个起码的态度。我们是回顾,绝不是嘲笑那时的人和那时的事。
三、草根写作以其另作的写作方式受到读者的追捧,而成为历史图书市场的一支生力军,你怎么看待这个现象?
身为这种写法的一员,我可以负责任地说,这只是一时的现象。因为它立足于平易、轻松、幽默等以前所没有一些元素上。
这是种新鲜感,一但它消弱、淡化,之后又会怎样?它一定会因为大众的熟悉,而变成过去。到那时,一个命题就会摆在我们的面前。
“轻松幽默之上会是什么。”这个问题必须要解决。不然中国的历史题材的书就没有前进的方向。
说到这一点,非常遗憾,我个人认为,现在中国的历史学者们任重道远。还处于对读者的扫盲阶段。
大家还对历史上曾经发生过什么都不知道,才有现在众多的普及型读物的出现。这些问题都在我的心里盘旋,我力求在自己的书里解决它们。我想,我找到了些东西。它会在我的宋史书里体现出来。
四、历史类图书火爆的原因是什么?
新鲜、热辣、敢于直言、勇于直面当年的事情,作出合乎情理的解释。这是最重要的几点原因。
五、历史阅读能提供读者什么?
是人写出的东西,带有最平实、最真切的理解,让读者感同身受。
六、您觉得历史书是否可以进行另类写作?
只是相对的另类,与之前的相比。历史是进步的,每一天都有新的资料被发现,它会颠覆、改变之前的结论。我们的价值观念等也在逐步变化中,这对怎样理解同一个历史事件也产生出不同。
所以,每个时代相比于以前,都是另类的。
明天,明年,后年,等等,都会永远的另类下去。这个世界是动态的,历史同样如此。
七、历史书另类写作的底线在哪里?
历史不同于其它任何题材形式,它是一个民族的印迹,它必须是神圣庄严的,不管你要用哪种形式来表现它,必须要尊重它。
所以它必须得有底线。
具体到现在来说,我呼唤读者们的良知和尊严,那是我们先人的事迹,我们要认真地听,就算是作者有意识地调动大家的积极性,尽量写得幽默风趣。也要能分出来哪些是可以接受的,哪些是非常讨厌的。
就像前面所说的,刻薄不同于才气,调侃不是幽默!
现在所谓的另类写作仍然没有成熟,它需要有水平,最重要的是有良知、有同情心的写手们来完善,更需要一大群成熟的,有水平的,富于良知和义愤的读者来维护。
历史属于我们这个民族的共同财富,我们要珍惜它。
Jill Bolte Taylor: Stroke Of Insight (Transcript)
Jill Bolte Taylor had an opportunity few brain scientists would wish for: One morning, she realized she was having a massive stroke. As it happened -- as she felt her brain functions slip away one by one, speech, movement, understanding -- she studied and remembered every moment. This is a powerful story of recovery and awareness -- of how our brains define us and connect us to the world and to one another. (Recorded February 2008 in Monterey, California. Duration: 18:44.)
See source | blog | video | 中文 (翻译).
Stroke Of Insight - Jill Bolte Taylor - TRANSCRIPT
I grew up to study the brain because I have a brother who has been diagnosed with a brain disorder, schizophrenia. And as a sister and as a scientist, I wanted to understand, why is it that I can take my dreams, I can connect them to my reality, and I can make my dreams come true -- what is it about my brother's brain and his schizophrenia that he cannot connect his dreams to a common, shared reality, so they instead become delusions?
So I dedicated my career to research into the severe mental illnesses. And I moved from my home state of Indiana to Boston where I was working in the lab of Dr. Francine Benes, in the Harvard Department of Psychiatry. And in the lab, we were asking the question, What are the biological differences between the brains of individuals who would be diagnosed as normal control, as compared to the brains of individuals diagnosed with schizophrenia, schizoaffective, or bipolar disorder?
So we were essentially mapping the microcircuitry of the brain, which cells are communicating with which cells, with which chemicals, and then with what quantities of those chemicals. So there was a lot of meaning in my life because I was performing this kind of research during the day. But then in the evenings and on the weekends I traveled as an advocate for NAMI, the National Alliance on Mental Illness.
But on the morning of December 10 1996 I woke up to discover that I had a brain disorder of my own. A blood vessel exploded in the left half of my brain. And in the course of four hours I watched my brain completely deteriorate in its ability to process all information. On the morning of the hemorrhage I could not walk, talk, read, write or recall any of my life. I essentially became an infant in a woman's body.
If you've ever seen a human brain, it's obvious that the two hemispheres are completely separate from one another. And I have brought for you a real human brain. [Thanks.] So, this is a real human brain. This is the front of the brain, the back of the brain with a spinal cord hanging down, and this is how it would be positioned inside of my head. And when you look at the brain, it's obvious that the two cerebral cortices are completely separate from one another. For those of you who understand computers, our right hemisphere functions like a parallel processor. While our left hemisphere functions like a serial processor. The two hemispheres do communicate with one another through the corpus collosum, which is made up of some 300 million axonal fibers. But other than that, the two hemispheres are completely separate. Because they process information differently, each hemisphere thinks about different things, they care about different things, and dare I say, they have very different personalities. [Excuse me. Thank you. It's been a joy.]
Our right hemisphere is all about this present moment. It's all about right here right now. Our right hemisphere, it thinks in pictures and it learns kinesthetically through the movement of our bodies. Information in the form of energy streams in simultaneously through all of our sensory systems. And then it explodes into this enormous collage of what this present moment looks like. What this present moment smells like and tastes like, what it feels like and what it sounds like. I am an energy being connected to the energy all around me through the consciousness of my right hemisphere. We are energy beings connected to one another through the consciousness of our right hemispheres as one human family. And right here, right now, all we are brothers and sisters on this planet, here to make the world a better place. And in this moment we are perfect. We are whole. And we are beautiful.
My left hemisphere is a very different place. Our left hemisphere thinks linearly and methodically. Our left hemisphere is all about the past, and it's all about the future. Our left hemisphere is designed to take that enormous collage of the present moment. And start picking details and more details and more details about those details. It then categorizes and organizes all that information. Associates it with everything in the past we've ever learned and projects into the future all of our possibilities. And our left hemisphere thinks in language. It's that ongoing brain chatter that connects me and my internal world to my external world. It's that little voice that says to me, "Hey, you gotta remember to pick up bananas on your way home, and eat 'em in the morning." It's that calculating intelligence that reminds me when I have to do my laundry. But perhaps most important, it's that little voice that says to me, "I am. I am." And as soon as my left hemisphere says to me "I am," I become separate. I become a single solid individual separate from the energy flow around me and separate from you.
And this was the portion of my brain that I lost on the morning of my stroke.
On the morning of the stroke, I woke up to a pounding pain behind my left eye. And it was the kind of pain, caustic pain, that you get when you bite into ice cream. And it just gripped me and then it released me. Then it just gripped me and then released me. And it was very unusual for me to experience any kind of pain, so I thought OK, I'll just start my normal routine. So I got up and I jumped onto my cardio glider, which is a full-body exercise machine. And I'm jamming away on this thing, and I'm realizing that my hands looked like primitive claws grasping onto the bar. I thought "that's very peculiar" and I looked down at my body and I thought, "whoa, I'm a weird-looking thing." And it was as though my consciousness had shifted away from my normal perception of reality, where I'm the person on the machine having the experience, to some esoteric space where I'm witnessing myself having this experience.
And it was all every peculiar and my headache was just getting worse, so I get off the machine, and I'm walking across my living room floor, and I realize that everything inside of my body has slowed way down. And every step is very rigid and very deliberate. There's no fluidity to my pace, and there's this constriction in my area of perceptions so I'm just focused on internal systems. And I'm standing in my bathroom getting ready to step into the shower and I could actually hear the dialog inside of my body. I heard a little voice saying, "OK, you muscles, you gotta contract, you muscles you relax."
And I lost my balance and I'm propped up against the wall. And I look down at my arm and I realize that I can no longer define the boundaries of my body. I can't define where I begin and where I end. Because the atoms and the molecules of my arm blended with the atoms and molecules of the wall. And all I could detect was this energy. Energy. And I'm asking myself, "What is wrong with me, what is going on?" And in that moment, my brain chatter, my left hemisphere brain chatter went totally silent. Just like someone took a remote control and pushed the mute button and -- total silence.
And at first I was shocked to find myself inside of a silent mind. But then I was immediately captivated by the magnificence of energy around me. And because I could no longer identify the boundaries of my body, I felt enormous and expansive. I felt at one with all the energy that was, and it was beautiful there.
Then all of a sudden my left hemisphere comes back online and it says to me, "Hey! we got a problem, we got a problem, we gotta get some help." So it's like, OK, OK, I got a problem, but then I immediately drifted right back out into the consciousness, and I affectionately referred to this space as La La Land. But it was beautiful there. Imagine what it would be like to be totally disconnected from your brain chatter that connects you to the external world. So here I am in this space and any stress related to my, to my job, it was gone. And I felt lighter in my body. And imagine all of the relationships in the external world and the many stressors related to any of those, they were gone. I felt a sense of peacefulness. And imagine what it would feel like to lose 37 years of emotional baggage! I felt euphoria. Euphoria was beautiful -- and then my left hemisphere comes online and it says "Hey! you've got to pay attention, we've got to get help," and I'm thinking, "I got to get help, I gotta focus." So I get out of the shower and I mechanically dress and I'm walking around my apartment, and I'm thinking, "I gotta get to work, I gotta get to work, can I drive? can I drive?"
And in that moment my right arm went totally paralyzed by my side. And I realized, "Oh my gosh! I'm having a stroke! I'm having a stroke!" And the next thing my brain says to me is, "Wow! This is so cool. This is so cool. How many brain scientists have the opportunity to study their own brain from the inside out?"
And then it crosses my mind: "But I'm a very busy woman. I don't have time for a stroke!" So I'm like, "OK, I can't stop the stroke from happening so I'll do this for a week or two, and then I'll get back to my routine, OK."
So I gotta call help, I gotta call work. I couldn't remember the number at work, so I remembered, in my office I had a business card with my number on it. So I go in my business room, I pull out a 3-inch stack of business cards. And I'm looking at the card on top, and even though I could see clearly in my mind's eye what my business card looked like, I couldn't tell if this was my card or not, because all I could see were pixels. And the pixels of the words blended with the pixels of the background and the pixels of the symbols, and I just couldn't tell. And I would wait for what I call a wave of clarity. And in that moment, I would be able to reattach to normal reality and I could tell, that's not the card, that's not the card, that's not the card. It took me 45 minutes to get one inch down inside of that stack of cards.
In the meantime, for 45 minutes the hemorrhage is getting bigger in my left hemisphere. I do not understand numbers, I do not understand the telephone, but it's the only plan I have. So I take the phone pad and I put it right here, I'd take the business card, I'd put it right here, and I'm matching the shape of the squiggles on the card to the shape of the squiggles on the phone pad. But then I would drift back out into La La Land, and not remember when I come back if I'd already dialed those numbers.
So I had to wield my paralyzed arm like a stump, and cover the numbers as I went along and pushed them, so that as I would come back to normal reality I'd be able to tell, yes, I've already dialed that number. Eventually the whole number gets dialed, and I'm listening to the phone, and my colleague picks up the phone and he says to me, "Whoo woo wooo woo woo." [laughter] And I think to myself, "Oh my gosh, he sounds like a golden retriever!" And so I say to him, clear in my mind I say to him. "This is Jill! I need help!" And what comes out of my voice is, "Whoo woo wooo woo woo." I'm thinking, "Oh my gosh, I sound like a golden retriever." So I couldn't know, I didn't know that I couldn't speak or understand language until I tried.
So he recognizes that I need help, and he gets me help. And a little while later, I am riding in an ambulance from one hospital across Boston to Mass General Hospital. And I curl up into a little fetal ball. And just like a balloon with the last bit of air just, just right out of the balloon I felt my energy lift and I felt my spirit surrender. And in that moment I knew that I was no longer the choreographer of my life. And either the doctors rescue my body and give me a second chance at life or this was perhaps my moment of transition.
When I awoke later that afternoon I was shocked to discover that I was still alive. When I felt my spirit surrender, I said goodbye to my life, and my mind is now suspended between two very opposite planes of reality. Stimulation coming in through my sensory systems felt like pure pain. Light burned my brain like wildfire and sounds were so loud and chaotic that I could not pick a voice out from the background noise and I just wanted to escape. Because I could not identify the position of my body in space, I felt enormous and expensive, like a genie just liberated from her bottle. And my spirit soared free like a great whale gliding through the sea of silent euphoria. Harmonic. I remember thinking there's no way I would ever be able to squeeze the enormousness of myself back inside this tiny little body.
But I realized "But I'm still alive! I'm still alive and I have found Nirvana. And if I have found Nirvana and I'm still alive, then everyone who is alive can find Nirvana." I picture a world filled with beautiful, peaceful, compassionate, loving people who knew that they could come to this space at any time. And that they could purposely choose to step to the right of their left hemispheres and find this peace. And then I realized what a tremendous gift this experience could be, what a stroke of insight this could be to how we live our lives. And it motivated my to recover.
Two and a half weeks after the hemorrhage, the surgeons went in and they removed a blood clot the size of a golf ball that was pushing on my language centers. Here I am with my mama, who's a true angel in my life. It took me eight years to completely recover.
So who are we? We are the life force power of the universe, with manual dexterity and two cognitive minds. And we have the power to choose, moment by moment, who and how we want to be in the world. Right here right now, I can step into the consciousness of my right hemisphere where we are -- I am -- the life force power of the universe, and the life force power of the 50 trillion beautiful molecular geniuses that make up my form. At one with all that is. Or I can choose to step into the consciousness of my left hemisphere. where I become a single individual, a solid, separate from the flow, separate from you. I am Dr. Jill Bolte Taylor, intellectual, neuroanatomist. These are the "we" inside of me.
Which would you choose? Which do you choose? And when? I believe that the more time we spend choosing to run the deep inner peace circuitry of our right hemispheres, the more peace we will project into the world and the more peaceful our planet will be. And I thought that was an idea worth spreading.
Local Restaurants
Name | Phone | Address |
Bamboo Garden (四季飘香) | (425) 688-7991 | 202 106th Pl NE, Bellevue, WA 98004-5723 |
Chengdu Chinese Buffet (蓉园) | (425) 451-8389 | 14625 NE 24th St., Bellevue, WA 98007 |
Chiang's Gourmet (敘香園) | (206) 527-8888 | 7845 Lake City Way NE Seattle, WA 98125 |
China Gate (龙门酒家) | (206) 624-1730 | 516 7th Ave. South,Seattle WA 98104 |
China Harbor (海景樓) | (206) 286-1688 | 2040 Westlake Avenue N. Seattle, WA 98109 |
House of Hong (康樂酒家) | (206) 622-7997 | 409 8th Avenue S. Seattle, WA 98104 |
Jade Garden (翠园) | (206) 622-8181 | 424 7th Ave S, Seattle, WA 98104 |
Jeem Asian (醉翁楼) | (425) 883-8858 | 14850 Ne 24th St Redmond, WA 98052 |
Kung-Ho Gourmet (更好小館) | (425) 643-2268 | 3640 128th Avenue SE Bellevue, WA 98006 |
Maple Garden (枫林) | (425) 644-9038 | 14725 NE 20th St, Bellevue, WA |
Ming's Chinese Seafood (明苑) | (425) 378-8009 | 13200 NE 20th St, Bellevue, WA |
New Kowloon Seafood (新九龍) | (206) 223-7999 | 900 S. Jackson St. #203 Seattle, WA 98104 |
New Star (新星酒家) | (206) 622-8801 | 516 S. Jackson St. Seattle, WA 98104 |
Noble Court (豪苑) | (425) 641-6011 | 1644 140th Ave NE Bellevue, WA 98005 |
Regent Bakery & Café (丽晶) | (425) 378-1498 | 15159 NE 24th St, Redmond, WA 98052 |
Shanghai Garden (滬江春) | (206) 625-1688 | 524 6th Avenue S. Seattle, WA 98104 |
Sichuanese Cuisine (老四川) | (425) 562-1552 | 15005 NE 24th St, Redmond, WA 98052 |
Spiced (过桥园) | (425) 644-8888 | 1299 156th Ave NE Suite #135, Bellevue, WA 98007 |
Szechuan Chef (川霸王) | (425) 746-9008 | 15015 Main St. Ste 107 Bellevue WA 98007 |
Szechuan Express (小四川) | (425) 746-4764 | 2245 140th Ave NE, Bellevue, WA 98005 |
Top Gun Seafood (半島海鮮酒家) | (425) 641-3386 | 12450 SE 38th St. Bellevue, WA 98006 |
V-Garden (顺利得) | (206) 622-2686 | 310 4th Ave S, Seattle, WA |
Drama, Life, and Review
美国电影断档了,最近看的最多的就是大陆的电视连续剧。过去中国的影视剧一直是个弱项,难得见到和大国相符的水平。随着岁月的增长,反倒是儿时看多了的《红色娘子军》、《小兵张嘎》、《永不消逝的电波》、《党的女儿》、《红日》、《林海雪原》、《英雄儿女》、等等变成了经典。近些年国产电影仍然是胡编烂造的多,钱花的越多越丢人现眼。除了个别几部,象《云水谣》和每年的贺岁片,大多数的质量总是徘徊在二三流的水准。
也不知是传统文化的原因还是市场的问题,和电影相比,中国的电视剧一直在稳步发展。即使不算早期港台剧的精品,各内陆省市和央视的作品都是越拍越好,似乎编剧和导演的素质都有飞跃,技术手法越来越成熟。之所以强调编剧和导演,主要是发现大多数电视剧的故事比电影曲折精彩,比早期的内容严谨多了;另外,一个好剧能捧红一个演员,而同样的一个演员在其他剧中的表现却差之千里,这应该是导演的功力。
和欧美的电影相比,有一个奇怪的现象:许多经过名著改编的美国电影,即使不是奥斯卡的水平,也不会太一般;可国产影剧,越是名著改编的影视,质量越差。象一些历史剧和武侠剧,都有一种倒胃口的感觉。或者编剧自作主张画蛇添足,或者导演和演员象没文化根本就没读过原作似的,不仅没表现原著的精髓,还加了不少糟粕,生生毁了许多经典的素材。这些往往都是戏外的功夫和市场炒作太多,象靠名气和绯闻挣票房的结果。
其实只要踏踏实实把心思放在本职上,以国人的聪慧,辅以国内外同行的借鉴,低成本、没有明星的片子一样可以拍的很好。国产电视和欧美相比还是有长处的。比如时下美国的电视更趋向快餐经营,除了“脱口兽”、“真人兽”、“生活兽”这种纯粹绑定收视率的怪胎,电视剧几乎完全以系列剧为主,这样各集独立,既缩短编排周期,又减少成本和风险。未来的大陆影视可千万不要走这个方向。看看《二十四小时》就知道,一个好的故事还是有市场的。
除了故事的完整连续,大陆影视的另一个突出看点就是对爱情的描写。欧美的电影在这方面是先驱,也是大碗,可在电视剧方面却显得很粗糙。过去大陆由于政治因素,爱情故事写的很勉强,脱离生活实际,对白老套无味虚假。后来应该是受到香港电视剧的影响,加之近年网络文学的累积,新近的许多连续剧非常贴近生活或者设定的历史背景,让人身临其境、追看至终。特别是在情感营造上的细腻委婉,非中国文化莫属。
例如,象《死亡日记-沉默的证人》这样一部以刑侦探案和心理变态杀人狂为主线的警匪片,其中不着斧痕顺其自然地插入了一些情感故事,竟然能在冷冰冰的解剖室外看到那种青涩凄美的初恋描写,其细微婉转之处恰好和警察制服的干练严肃形成强烈对比。再如《士兵突击》中纯粹兄弟情谊,《天道》和《半路夫妻》中看似不协调的感情纠葛,都是源于生活,又有艺术加工。更不用说象《我们的八十年代》《不能没有她》这种纯爱情的故事,其复杂曲折更胜以往的台湾言情小说。
由于中国历史的重大变迁,尤其是近代人文在每个十字路口的快速转向,给每一个时代都留下了深刻的烙印。这种变化以现在的视角来看,真有恍如隔世的感叹。如《一年又一年》用时间的节奏和社会现象循序推进家庭生活中的大事。其他一些类似家族史跨越从清末至近代传奇的电视剧也很流行。还有一些故事,虽然设定在特殊的年代,但在剧情描写上却只专注于一个特定的层面,而把其他内容如时代背景、政治特色故意模糊。比如《王贵和安娜》的故事虽然开始于文革后期,但已经不再局限于当时的背景,甚至完全可以作为一部婚姻指南。
国产的生活电视剧从《渴望》开始就已经很有特色了。喜剧不够多,但质量还可以,当然众多期盼的《地下交通站》不能续播是非常遗憾的。其他新的成就主要表现在警匪片(如海岩的系列作品)、谍战片(《潜伏》)、和军旅片(《士兵突击》)。这些优秀的电视剧都来源于好的文学作品,连历史剧也是这样───大家看腻了“辫子戏”,可是以二月河的小说为蓝本的《雍正王朝》就显示出极高的水平。这可能是商业化的戏剧创作仍然没有入门。
顺带检讨一个题外话:国内的互联网发展势头是很猛的了,可对资料的组织实在是太差了。这可能就是长久以来一直诟病的软件水平远远落后于硬件发展的问题。目前美国无论象“亚马逊”这样的商业网站,电影数据库,还是图书馆,都可以快速简捷地查到一个影剧的详细资料。可国内的电视剧,很多时候连哪一年首播或出品的信息都找不到,这还是受千万人追捧的剧目。这方面国家没人管,也没有商业文化公司愿意长期投资这样的项目。
总之,中国影视仍然缺乏气势磅礴的鸿篇巨制,文化和历史的底蕴不足。这和中国几千年的现成遗产是相背的,令人不可思议。细致严谨不足还可以是受经验、资金等的影响,总是可以学习的。但自家的东西不懂,就不是抄袭外国的“大片”手法或用什么电脑制作可以弥补的了。只希望随着经济发展和生活富裕,影视产品能有更长足的底气和自我建设,焕发出本源和新曲。
这里顺手罗列一些佳作,以备存念:
《我的兄弟叫顺溜》
《死亡日记──沉默的证人》
《天道》(2009.02.03)、《墓道》
《潜伏》
《我的青春谁做主》、《奋斗》
《我们的八十年代》、《王贵与安娜》、《北風那個吹》
《不能没有她》、《一言為定》
《纸醉金迷》(2008.11.22)
《地下交通站》、《我爱我家》(1993-1994)、《编辑部的故事》
《大染坊》(2009.09.26)、《闯关东》、《大宅门》
《士兵突击》(2006.01.01)
《雍正王朝》、《雍正皇帝》
《便衣警察》、《一场风花雪夜的事》、《永不瞑目》、《玉观音》
《一年又一年》、《过把瘾》
《大时代》、《流氓大亨》、《人在边缘》、《我本善良》……
《含羞草》、《星星知我心》
CCNet vs. CruiseControl
Set once and let it go, this is how Continuous Integration works for automated build process along with development cycle. For a large number of projects, the maintenance for such continously integrated build environment could be complicate and cumbersome. This article will provide and discuss sample config files from practice experience to make easy using CruiseControl.NET and CruiseControl with subversion repository. For comparison of all other similar products, see ThoughtWorks CI Feature Matrix.
Contents
- CruiseControl.NET - [see online documentation]
- CruiseControl - [see configuration reference]
- Comparison
CruiseControl.NET
CCNET (CruiseControl.NET) starts with ccnet.config:
<!--ccnet.config-->
<?xml version="1.0" encoding="utf-8" ?>
<!DOCTYPE cruisecontrol SYSTEM "file:ccnet_definitions.dtd">
<cruisecontrol xmlns:cb="urn:ccnet.config.builder">
<cb:include href="ccnet_definitions.xml" xmlns:cb="urn:ccnet.config.builder"/>
<queue name="$(computername)" duplicates="ApplyForceBuildsReAdd" />
<cb:include href="ccnet_Project.Standard_Solution.xml" xmlns:cb="urn:ccnet.config.builder"/>
<cb:include href="ccnet_MyProject.xml" xmlns:cb="urn:ccnet.config.builder"/>
</cruisecontrol>
In order to maintain CCNET config and projects in clear XML code, the
ccnet.config
in above example takes advantage of CCNET powerful preprocessor features. There are three types of CCNET configuration preprocessors: constant (including text/variable and nodeset XML fragment), nested expansion (defining a class whose instance can take parameters), and include file (<cb:include />
).Including external config files helps putting constant/variable definitions, project definitions, and reusable XML pieces together in an organized structure, so that
ccnet.config
can be read in a from-top-to-down order while presenting high-level view in the main config. Another advanced use of include files is to allow editing definition or project file individually without touching ccnet.config. CCNET has the ability to notice any include file changed and reload ccnet.config automatically.Like writing a program, the first thing in design is to define constants, variables, and reusable functions (or templates) that can be shared across the projects. These are all implemented by
<cb:define />
in CCNET. Constants and XML fragments can be defined by DTD, such as in XHTML DTDs and following sample of ccnet_definitions.dtd
included in ccnet.config
.
<!--ccnet_definitions.dtd-->
<!ENTITY dir_ccnet "$(ProgramFiles)\CruiseControl.NET">
<!ENTITY dir_ccnet_server "$(ProgramFiles)\CruiseControl.NET\server">
<!ENTITY dir_ccnet_artifacts "$(ProgramFiles)\CruiseControl.NET\server\projects">
<!ENTITY dir_svn "$(ProgramFiles)\CollabNet Subversion Server">
<!ENTITY dir_svn_target "\\10.0.1.100\svnbuilds">
<!ENTITY dir_dotnet "$(SystemRoot)\Microsoft.NET\Framework\v3.5">
<!ENTITY dir_msvs "$(ProgramFiles)\Microsoft Visual Studio 9.0">
<!ENTITY svn_server "svn://192.168.0.100">
<!ENTITY auth_svn "<username>svn_user</username><password>svn_password</password>" >
<!ENTITY exec_devenv "<executable>&dir_msvs;\Common7\IDE\devenv.com</executable>" >
<!ENTITY exec_nmake "<executable>&dir_msvs;\VC\bin\nmake.exe</executable>" >
<!ENTITY exec_msbuild "<executable>&dir_dotnet;\MSBuild.exe</executable>" >
<!ENTITY exec_svn "<executable>&dir_svn;\svn.exe</executable>" >
<!ENTITY url_ccnet "http://localhost/ccnet">
However, entity definition has its limitation in use (such as in string value of a property) and modifying a system file included by
<!DOCTYPE >
cannot trigger ccnet.config
to reload. In CCNET, <cb:define />
is recommended to perform the same and even better job. The following sample illustrates syntax of defining text constant (variable) and nodeset (xml fragment). The third usage of <cb:define />
, nested expansion, is for reusable class that can take parameters by instance, such as defining a project template.The use of definitions in CCNET is easy to understand from above sample config file. First of all, any system environment variables can be referenced by
<!--ccnet_definitions.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<!--# Preprocessor: Text Constants -->
<cb:define const_name="value" /> <-- defines $(const_name), or <cb:const_name/> -->
<cb:define dir_ccnet="$(ProgramFiles)\CruiseControl.NET" />
<cb:define dir_ccnet_server="$(dir_ccnet)\server" />
<cb:define dir_ccnet_artifacts="$(dir_ccnet_server)\projects" />
<cb:define dir_ccnet_buildlogger="$(dir_ccnet)\server\ThoughtWorks.CruiseControl.MsBuild.dll" />
<cb:define dir_dotnet="$(SystemRoot)\Microsoft.NET\Framework\v3.5" />
<cb:define dir_svn="$(ProgramFiles)\CollabNet Subversion Server" />
<cb:define dir_svn_source="c:\svn_checkout" />
<cb:define dir_svn_builds="d:\svn_builds" />
<cb:define dir_svn_target="\\10.0.1.100\svnbuilds" />
<cb:define dir_system32="$(SystemRoot)\system32" />
<cb:define dir_msvs="$(ProgramFiles)\Microsoft Visual Studio 9.0" />
<cb:define svn_username="svn_username" />
<cb:define svn_password="svn_password" />
<cb:define url_svn_target="file://///10.0.1.100/svnbuilds" />
<cb:define url_ccnet="http://localhost/ccnet" />
<!--# Preprocessor: Nodeset Constants -->
<cb:define name="xml_default_extlink">
<externalLink name="CCNET Builds [Ctrl+Click to open in Explorer]" url="$(url_svn_target)" />
</cb:define>
<cb:define name="xml_logger">
<xmllogger logDir="$(dir_ccnet_server)\projects\$(project)" />
</cb:define>
<cb:define name="auth_svn">
<username>$(svn_username)</username>
<password>$(svn_password)</password>
</cb:define>
<cb:define name="exec_devenv">
<executable>$(dir_msvs)\Common7\IDE\devenv.com</executable>
</cb:define>
<cb:define name="exec_nmake">
<executable>$(dir_msvs)\VC\bin\nmake.exe</executable>
</cb:define>
<cb:define name="exec_vcvars">
<executable>$(dir_msvs)\VC\vcvarsall.bat</executable>
<baseDirectory>$(dir_msvs)\VC</baseDirectory>
<buildArgs>x86</buildArgs>
</cb:define>
<cb:define name="exec_msbuild">
<executable>$(dir_dotnet)\MSBuild.exe</executable>
</cb:define>
<cb:define name="exec_svn">
<executable>$(dir_svn)\svn.exe</executable>
</cb:define>
<!--# Preprocessor: Nested Expansions (see other ccnet_*.xml)
<cb:define name="xml_element"><some_element property="$(var1)" /><more>$(var2)</more></cb:define>
<cb:xml_element var1="value1" var2="value2" />
-->
</cb:config-template>
$(env_var)
, where $(env_var)
is an environment variable accessible in CCNET runtime context. For example, if CCNET is started by ccnet.exe
manually in a Command Prompt window, all system variables and logon user variables should be available; otherwise, if CCNET is started in service.msc
, only system variables can be used. Once a CCNET constant/variable (
<cb:define var_name="text" />
) or XML fragment (<cb:define name="element_name" >...</cb:define>
) is defined, it can be referenced immediately afterward thru whole CCNET runtime environment. The constant/variable can be referenced as a string by $(var_name)
, or <cb:var_name />
. The XML fragment must be referenced by <cb:element />
. All <cb:define />
are global definitions. To control the scope of a preprocessor definition, use <cb:scope />
. See configuration preprocessors.Next, let's take a look at how to use CCNET nested expansions and parameters of preprocessor definition to create a project template. This template can be used to meet the following conditions:
- All project properties can be initiated by an instance of the template
- There is only one build target path needed to be published
- There is only one developer and who will be in notification for all build states
- The project can be built by a MS Visual Studio 2008 solution file
ccnet_definitions.xml
, ccnet_Project.Standard_Solution.xml
uses <cb:config-template />
but only defines one XML fragment named "project_template_solution
".
<!--ccnet_Project.Standard_Solution.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:define name="project_template_solution">
<project name="$(project_name)" queue="$(project_queue)" queuePriority="0">
<category>$(project_category)</category>
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<artifactDirectory>$(dir_ccnet_artifacts)\$(project_name)</artifactDirectory>
<webURL>$(url_ccnet)</webURL>
<labeller type="svnRevisionLabeller">
<prefix>$(project_name)-r</prefix>
<major>0</major>
<minor>0</minor>
<url>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</url>
<cb:auth_svn />
</labeller>
<!--
<labeller type="lastChangeLabeller"><prefix>$(project_name)-</prefix></labeller>
-->
<triggers>
<!--NOTE: build machine must sync clock with svn server!!!-->
<intervalTrigger buildCondition="IfModificationExists" seconds="$(project_checkTime)" initialSeconds="60" />
</triggers>
<sourcecontrol type="svn">
<trunkUrl>svn://$(project_svnServer)$/(project_Root)$(project_Path)/$(project_Branch)</trunkUrl>
<cb:auth_svn />
<cb:exec_svn />
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<timeout units="seconds">$(project_checkTime)</timeout>
</sourcecontrol>
<tasks>
<msbuild>
<cb:exec_msbuild />
<projectFile>$(project_solution)</projectFile>
<workingDirectory>$(dir_svn_source)\$(project_name)</workingDirectory>
<buildArgs>/noconsolelogger /p:Configuration=Release</buildArgs>
<logger>$(dir_ccnet_buildlogger)</logger>
</msbuild>
</tasks>
<publishers>
<cb:xml_logger project="$(project_name)" />
<buildpublisher>
<sourceDir>$(dir_svn_source)\$(project_name)\$(project_buildTarget)</sourceDir>
<publishDir>$(dir_svn_target)</publishDir>
<useLabelSubDirectory>true</useLabelSubDirectory>
<alwaysPublish>false</alwaysPublish>
</buildpublisher>
<cb:include href="ccnet_email.xml" xmlns:cb="urn:ccnet.config.builder" />
<cb:ccnet_email
developer_name="Developer" developer_alias="$(project_developer)"
notification="Always"
/>
</publishers>
</project>
</cb:define>
</cb:config-template>
Different from
ccnet_definitions.xml
, ccnet_Project.Standard_Solution.xml
also uses some variables that have never been defined, such as $(project_name)
, $(project_path)
, and etc., basically anything with "project_
" prefix in this example. This is the exclusive feature of CCNET preprocessor that allow all these variable dynamically bound to an instance of the template when it is used. In ccnet.config
, after ccnet_Project.Standard_Solution.xml
is loaded, a project file ccnet_MyProject.xml
comes after to initiate a project configuration based on the template. Here is code:
<!--ccnet_MyProject.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:project_template_solution
project_name="My Project"
project_alias="myproject"
project_category="My Category"
project_path="MyProject"
project_root=""
project_branch="trunk"
project_svnServer="192.168.10.100"
project_svnPath="svn://192.168.10.100/Software/MyProject/trunk"
project_solution="MyProject.sln"
project_buildTarget="Setup\Release"
project_buildTime="1800"
project_checkTime="600"
project_developer="myalias"
project_queue="$(computername)"
/>
</cb:config-template>
The properties defined in "My Project" configuration makes the template used in complete. Actually, the property names after
cb:project_template_solution
may not be necessary matching what have been defined in the template. At least CCNET does not check if a referenced constant or variable is predefined. Undefined constant/variable will be replaced by empty string (- be caution that this could cause exception at the runtime). And any property defined in the instance but not used in the template will be ignored (e.g. project_svnPath
).CCNET preprocessor config template can be nested. Here is the file of
ccnet_email.xml
that has been included in ccnet_Project.Standard_Solution.xml
:For label variables used in
<!--ccnet_email.xml-->
<cb:config-template xmlns:cb="urn:ccnet.config.builder">
<cb:define name="ccnet_email">
<email from="Builder@mycompany.com"
mailhost="192.168.10.25" includeDetails="TRUE" useSSL="FALSE"
subjectPrefix="CCNET:" description="CruiseControl.NET E-mail Notification ">
<users>
<user name="Builder" group="Builder" address="builder@mycompany.com"/>
<user name="$(developer_name)" group="Developer" address="$(developer_alias)@mycompany.com"/>
<user name="Build Request" group="Supervisor" address="buildrequest@mycompany.com"/>
<user name="Admin" group="Admin" address="admin@mycompany.com"/>
</users>
<groups>
<group name="Admin" notification="Failed"/>
<group name="Builder" notification="Success"/>
<group name="Developer" notification="$(notification)"/>
</groups>
<modifierNotificationTypes>
<NotificationType>Always</NotificationType>
<NotificationType>Failed</NotificationType>
<NotificationType>Fixed</NotificationType>
<NotificationType>Success</NotificationType>
<NotificationType>Change</NotificationType>
</modifierNotificationTypes>
<subjectSettings>
<subject buildResult="Broken" value="Build Failed - ${CCNetProject} - [Broken] :: ${CCNetBuildCondition}" />
<subject buildResult="StillBroken" value="Build Failed - ${CCNetProject} - [StillBroken] :: ${CCNetBuildCondition}" />
<subject buildResult="Fixed" value="Build Success - ${CCNetProject} - [Fixed] :: ${CCNetBuildCondition}" />
<subject buildResult="Exception" value="Build Failed - ${CCNetProject} - [Exception] :: ${CCNetBuildCondition}" />
<subject buildResult="Success" value="Build Success - ${CCNetProject} - [Success] :: ${CCNetBuildCondition}" />
</subjectSettings>
</email>
</cb:define>
<!--# after <cb:include href="ccnet_email.xml" />, define following properties:
developer_name - developer name
developer_alias - developer email alias
notification - notification type: Always|Change|Failed|Success|Fixed
<cb:ccnet_email
developer_name=""
developer_alias=""
notfication=""
/>
==-->
</cb:config-template>
<subjectSettings />
, please refer to CCNET Email Publisher.The last thing left uncovered in the project template is "svnRevisionLabeller". This is a modified plugin based on original svnrevisionlabeller. The code (
SvnRevisonLabeller.cs
) is attached below:
/**
****************************************
* Class Name: SvnRevisionLabeller
* Description: Subversion functions used in NAnt
* Requisite: Reference to NAnt.Core.dll
* Notes: See http://code.google.com/p/svnrevisionlabeller/
****************************************
*/
using System;
using System.Xml;
using Exortech.NetReflector;
using ThoughtWorks.CruiseControl.Core;
using ThoughtWorks.CruiseControl.Core.Util;
namespace ccnet.SvnRevisionLabeller.plugin
{
/// <summary>
/// Generates label numbers using the Subversion revision number.
/// </summary>
/// <remarks>
/// This class was inspired by Jonathan Malek's post on his blog
/// (<a href="http://www.jonathanmalek.com/blog/CruiseControlNETAndSubversionRevisionNumbersUsingNAnt.aspx">CruiseControl.NET and Subversion Revision Numbers using NAnt</a>),
/// which used NAnt together with Subversion to retrieve the latest revision number. This plug-in moves it up into
/// CruiseControl.NET itself, so that you can see the latest revision number appearing in CCTray. The label can
/// then be retrieved from within NAnt by accessing the property <c>${CCNetLabel}</c>.
/// </remarks>
[ReflectorType("svnRevisionLabeller")]
public class SvnRevisionLabeller : ILabeller
{
#region Private members
private int major;
private int minor;
private string _url;
private string executable;
private string prefix;
private string username;
private string password;
private const string RevisionXPath = "/log/logentry/@revision";
#endregion
#region Constructors
/// <summary>
/// Initializes a new instance of the <see cref="SvnRevisionLabeller"/> class.
/// </summary>
public SvnRevisionLabeller()
{
major = 1;
minor = 0;
executable = "svn.exe";
prefix = String.Empty;
}
#endregion
#region Public methods
/// <summary>
/// Returns the label to use for the current build.
/// </summary>
/// <param name="resultFromLastBuild">IntegrationResult from last build used to determine the next label</param>
/// <returns>the label for the new build</returns>
public string Generate(IIntegrationResult resultFromLastBuild)
{
// Get the last revision from the Subversion repository
int svnRevision = GetRevision();
// Get the last revision from CruiseControl
Version version = ParseVersion(svnRevision, resultFromLastBuild);
// If the revision number hasn't changed (because no new check-ins have been made), increment the build number;
// Otherwise, reset the build number to 0
int revision = (svnRevision == version.Build) ? version.Revision + 1 : 0;
// Construct a new version number, adding any specified prefix
Version newVersion = new Version(major, minor, svnRevision, revision);
if (major == 0 && minor == 0)
{
// <major>0</major><minor>0</minor> must be explicitly defined
// assume we only care about subversion revision - 2008-11-18 jzhu@zetron.com
return prefix + svnRevision;
}
else // keep original usage for a 4-field version format
{
return prefix + newVersion;
}
}
/// <summary>
/// Runs the task, given the specified <see cref="IIntegrationResult"/>, in the specified <see cref="IProject"/>.
/// </summary>
/// <param name="result"></param>
public void Run(IIntegrationResult result)
{
result.Label = Generate(result);
}
#endregion
#region Public properties
/// <summary>
/// Gets or sets the major build number.
/// </summary>
/// <value>The major build number.</value>
[ReflectorProperty("major", Required=false)]
public int Major
{
get
{
return major;
}
set
{
major = value;
}
}
/// <summary>
/// Gets or sets the minor build number.
/// </summary>
/// <value>The minor build number.</value>
[ReflectorProperty("minor", Required=false)]
public int Minor
{
get
{
return minor;
}
set
{
minor = value;
}
}
/// <summary>
/// Gets or sets the repository URL from which the <c>svn log</c> command will be run.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("url", Required = true)]
public string Url
{
get
{
return _url;
}
set
{
_url = value;
}
}
/// <summary>
/// Gets or sets the Subversion client executable.
/// </summary>
/// <value>The path to the executable.</value>
/// <remarks>
/// If the value is not supplied, the task will expect to find <c>svn.exe</c> in the <c>PATH</c> environment variable.
/// </remarks>
[ReflectorProperty("executable", Required=false)]
public string Executable
{
get
{
return executable;
}
set
{
executable = value;
}
}
/// <summary>
/// Gets or sets an optional prefix for the build label.
/// </summary>
/// <value>A string to prefix the version number with.</value>
[ReflectorProperty("prefix", Required=false)]
public string Prefix
{
get
{
return prefix;
}
set
{
prefix = value;
}
}
/// <summary>
/// Gets or sets the username to access SVN repository.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("username", Required = false)]
public string Username
{
get
{
return username;
}
set
{
username = value;
}
}
/// <summary>
/// Gets or sets the password to access SVN repository.
/// </summary>
/// <value>The repository.</value>
[ReflectorProperty("password", Required = false)]
public string Password
{
get
{
return password;
}
set
{
password = value;
}
}
#endregion
#region Private methods
/// <summary>
/// Parses the version.
/// </summary>
/// <param name="revision">The revision.</param>
/// <param name="resultFromLastBuild">The result from last build.</param>
private Version ParseVersion(int revision, IIntegrationResult resultFromLastBuild)
{
try
{
string label = resultFromLastBuild.LastSuccessfulIntegrationLabel;
if (prefix.Length > 0)
{
label = label.Replace(prefix, String.Empty).TrimStart('_');
}
return new Version(label);
}
catch (SystemException)
{
return new Version(major, minor, revision, 0);
}
}
/// <summary>
/// Gets the latest Subversion revision by checking the last log entry.
/// </summary>
private int GetRevision()
{
// Set up the command-line arguments required
ProcessArgumentBuilder argBuilder = new ProcessArgumentBuilder();
argBuilder.AppendArgument("log");
argBuilder.AppendArgument("--xml");
argBuilder.AppendArgument("--limit 1");
argBuilder.AppendArgument(Url);
if (Username != null && Username.Length > 0 && Password != null && Password.Length > 0)
{
AppendCommonSwitches(argBuilder);
}
// Run the svn log command and capture the results
ProcessResult result = RunProcess(argBuilder);
Log.Debug("Received XML : " + result.StandardOutput);
// Load the results into an XML document
XmlDocument xml = new XmlDocument();
xml.LoadXml(result.StandardOutput);
// Retrieve the revision number from the XML
XmlNode node = xml.SelectSingleNode(RevisionXPath);
return Convert.ToInt32(node.InnerText);
}
/// <summary>
/// Appends the arguments required to authenticate against Subversion.
/// </summary>
/// <param name="buffer"><The argument builder./param>
private void AppendCommonSwitches(ProcessArgumentBuilder buffer)
{
buffer.AddArgument("--username", Username);
buffer.AddArgument("--password", Password);
buffer.AddArgument("--non-interactive");
buffer.AddArgument("--no-auth-cache");
}
/// <summary>
/// Runs the Subversion process using the specified arguments.
/// </summary>
/// <param name="arguments">The Subversion client arguments.</param>
/// <returns>The results of running the process, including captured output.</returns>
private ProcessResult RunProcess(ProcessArgumentBuilder arguments)
{
ProcessInfo info = new ProcessInfo(executable, arguments.ToString(), null);
Log.Debug("Running Subversion with arguments : " + info.Arguments);
ProcessExecutor executor = new ProcessExecutor();
ProcessResult result = executor.Execute(info);
return result;
}
#endregion
}
}
CruiseControl
Comparing to CCNET, CruiseControl does not provide very powerful preprocessor and template feature. There is no syntax to define XML fragment (except using DTD). The main config file,config.xml
, cannot be automatically reloaded if any configuration changed.
<!--config.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--# Always deploy ccnet.config with following files under ${env.CCDIR}
config.properties
cc_antpublisher_copy.xml
prj_*.xml
also place cruisecontrol.jar under "${env.CCDIR}\lib" to overwrite
original one for a customized new plugin: <ZSVNLabeller />
==-->
<cruisecontrol>
<property file="config.properties" />
<property environment="env" toupper="true" />
<property name="url_cruisecontrol" value="${url_cchost}/cruisecontrol" />
<property name="url_buildresults" value="${url_cruisecontrol}/buildresults" />
<property name="url_ccdashboard" value="${url_cchost}/dashboard" />
<property name="url_ccdoc" value="${url_cchost}/documentation" />
<include.projects file="prj_MyProject.xml" />
</cruisecontrol>
As in above sample of
config.xml
, property definition is either by <property name="var" value="text" />
element or an external file (e.g. config.properties
).
###############################################################################
# Filename: config.properties
# Usage: called by cruisecontrol element, i.e.:
# <property file="config.properties" />
# Syntax: defined by the class java.util.Properties, with the same rules
# about how non-ISO8859-1 characters must be escaped.
###############################################################################
### default value predefinitions
def_hostip=10.0.1.194
def_hostname=hostname
def_project_buildTime=1800
def_project_checkTime=600
### dir/path predefinitions
dir_cruisecontrol=%ProgramFiles%\CruiseControl
dir_svn_target=\\10.0.1.100\svnbuilds
pwd_cruisecontrol=/srv/cruisecontrol-bin-2.8.2
smb_svn_target=/svnbuilds
### svn settings predefinitions
svn_server=svn://192.168.10.100
svn_username=svn_username
svn_password=svn_password
### user id predefinitions
uid_admin=admin
uid_builder=builder
### url predefinitions
url_cchost=http://hostname:8080
url_ccdashboard=http://hostname:8080/dashboard
url_ccdoc=http://hostname:8080/documentation
url_cruisecontrol=http://hostname:8080/cruisecontrol
url_buildresults=http://hostname:8080/cruisecontrol/buildresults
url_ccnet=http://localhost/ccnet
Certainly, a project file (e.g.
prj_MyProject.xml
) can be included in config.xml
so that the configuration is well organized by each individual config file. Here only provides a project template file that a real project config (e.g. prj_MyProject.xml
) will be based on.
<!--prj_project.template.xml-->
<?xml version="1.0" encoding="utf-8" ?>
<!--#
For each project, use following steps to create a project file from this template by
copying this file to a new project file, in convention of "prj_$[project_name].xml":
(1) replace "$[project_name]" with a project name
(2) replace any place holder definition, like in format of $[place_holder], in property definitions
(3) optionally use ${def_project_buildTime} for ${project_buildTime}
(4) optionally add more ${project_buildTarget[n]} properties and publishers
(5) choose one of the build methods in <project><schedule></schedule></project> block
(6) replace "\\" to "/" on Linux system
(7) replace ${dir_svn_target} with ${smb_svn_target} on a Linux build system
(8) define properties ${eid_*} for build notification; ${uid_builder} has been notified on success
(9) refer to any property definitions in config.properties
==-->
<cruisecontrol>
<project name="$[project_name]">
<property name="project_name" value="${project.name}" />
<property name="project_alias" value="$[project_alias]" />
<property name="project_category" value="$[project_category]" />
<property name="project_root" value="$[project_root]" />
<property name="project_path" value="$[project_path]" />
<property name="project_branch" value="$[project_branch]" />
<property name="project_svnServer" value="$[project_svnServer]" />
<property name="project_svnPath" value="svn://${project_svnServer}/${project_root}${project_path}/${project_branch}" />
<property name="project_source" value="${env.CCDIR}\\projects\\${project.name}" />
<property name="project_buildTarget" value="$[project_buildTarget]" />
<property name="project_buildTime" value="${def_project_buildTime}" />
<property name="project_buildTime" value="$[project_buildTime]" />
<property name="project_checkTime" value="30" />
<property name="url_cruisecontrol" value="${url_cruisecontrol}" />
<property name="eid_failure" value="builder" />
<property name="eid_success" value="builder" />
<property name="eid_always" value="developer_alias" />
<plugin name="svn" classname="net.sourceforge.cruisecontrol.sourcecontrols.SVN" />
<plugin name="ZSVNLabeller" classname="net.sourceforge.cruisecontrol.labelincrementers.ZSVNLabeller" />
<ZSVNLabeller separator="r" labelprefix="r" workingcopypath="${project_source}"
/>
<!--#
<listeners>
<currentbuildstatuslistener file="logs/${project.name}/status.txt"/>
</listeners>
==-->
<bootstrappers>
<!--#
<antbootstrapper anthome="apache-ant-1.7.0"
buildfile="projects/${project.name}/build.xml"
target="clean" />
<execbootstrapper command=""
args="" workingdir="projects/${project.name}"
errorstr="" showProgress=""
timeout=""
/>
==-->
<svnbootstrapper localWorkingCopy="${project_source}"
username="${svn_username}"
password="${svn_password}"
/>
</bootstrappers>
<modificationset quietperiod="60">
<!--# <filesystem/> triggers a build if any file in ${project.name} project is touched
<filesystem folder="${project_source}" includedirectories="false" />
==-->
<!--# <svn/> sets property ${svnrevision}
==-->
<svn localWorkingCopy="${project_source}"
useLocalRevision="false"
username="${svn_username}"
password="${svn_password}"
property=""
/>
</modificationset>
<schedule interval="{project_checkTime}">
<!--#<ant />
<ant anthome="apache-ant-1.7.0" buildfile="projects/${project.name}/build.xml"/>
==-->
<!--#<exec/>
==-->
<exec command=".\\build.cmd"
args='"${project_source}"' workingdir="${project_source}"
errorstr="Build Failed" showProgress="true"
timeout="${project_buildTime}"
/>
</schedule>
<log>
<!--# merge from log files with specified pattern from specified dir
<merge dir="${project_source}\\log" pattern="*.log" />
==-->
<deleteartifacts every="30" unit="DAY" />
<delete every="30" unit="DAY" />
</log>
<publishers>
<htmlemail buildresultsurl="${url_cruisecontrol}/buildresults/${project_name}"
defaultsuffix=".com" mailhost="192.168.10.66" mailport="25" usessl="false"
returnaddress="builder@mycompany.com" returnname="Builder"
subjectprefix="CruiseControl:"
>
<success address="${uid_builder}@mycompany.com" /><!--send to builder success notification-->
<success address="${eid_success}@mycompany.com" />
<failure address="${eid_failure}@mycompany.com" />
<always address="${eid_always}@mycompany.com" />
</htmlemail>
<onsuccess>
<antpublisher anthome="apache-ant-1.7.0"
buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
target="publish_dir"
>
<property name="publish_source" value="${project_source}\\${project_buildTarget}" />
<property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
</antpublisher>
<!--
<antpublisher anthome="apache-ant-1.7.0"
buildfile="${env.CCDIR}\\cc_antpublisher_copy.xml"
target="publish_file"
>
<property name="publish_source" value="${project_source}\\${project_buildTarget}" />
<property name="publish_target" value="${dir_svn_target}\\${project_alias}" />
</antpublisher>
==-->
</onsuccess>
</publishers>
</project>
</cruisecontrol>
The template cannot be used as nicely as in CCNET. But it gives a start for most projects. Please refer to usage comments at beginning of the template. Also remember since CruiseControl is running on both Windows and Linux systems, backslash or forward-slash need to be carefully used wherever a path is typed.
One of the difference in CruiseControl than CCNET is the sequence of continuous integration cycle. In CCNET, the
<sourcecontrol />
compares last build state with repository server, then checkout (for the first time of check) or update source code on local working directory before starting build tasks. But in CruiseControl, the bootstrapper is always called at the beginning to update local working directory from server before using <modificationset />
to detect if tasks in <schedule />
need to be executed. Such difference implies that the local working directory and source code in CruiseControl project must be manually checked out from the repository (such as
`svn co`
) at the time when the project is setup, which is not necessary in CCNET - where the local working directory can be created automatically if it does not exist. This also explains why LocalWorkingCopy
(rather than RepositoryLocation
) property should be used in <svn />
source control under <modificationset />
to detect new commit for a CruiseControl project. Note: There could be a chance that local working copy has not been updated yet when a new change is committed on repository server after
bootstrapper
but before modificationset
. The race condition will end up building old source against a newer revision label. Modification of subversion in CruiseControl is parsed from
`svn log -r`
by checking between last build time and current time. If useLocalRevision
property is used in <svn />
source control, the local revision number will be in place of current time.At the end of the template, artifacts publishing is implemented by the following include file.
<!--cc_antpublisher_coppy.xml-->
<project name="antpublisher_copy" default="publish">
<target name="publish">
<echo>Copying ${publish_source} to ${publish_target} ...</echo>
<copy todir="${publish_target}"><fileset dir="${publish_source}" /></copy>
</target>
<target name="publish_dir">
<echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
<copy todir="${publish_target}-${label}"><fileset dir="${publish_source}" /></copy>
</target>
<target name="publish_file">
<echo>Copying ${publish_source} to ${publish_target}-${label} ...</echo>
<copy todir="${publish_target}-${label}" file="${publish_source}"></copy>
</target>
<target name="publish_artifacts">
<echo>Copying ${publish_source_dir} to ${publish_target} ...</echo>
<copy todir="${publish_target}">
<fileset dir="${publish_source_dir}">
<include name="${publish_source_files}" />
</fileset>
</copy>
</target>
</project>
The Java source code of "ZSVNLabeller" plugin (based on
SVNLabelIncrementer
, implements LabelIncrementer
) is attached as below. The file "ZSVNLabeller.java
" should be place under source code folder "net\sourceforge\cruisecontrol\labelincrementers
". After rebuilt, copy "cruisecontrol.jar
" to ${env.CCDIR}
, where CruiseControl installed.
package net.sourceforge.cruisecontrol.labelincrementers;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import net.sourceforge.cruisecontrol.LabelIncrementer;
import org.apache.log4j.Logger;
import org.jdom.Element;
/**
* This class provides a label incrementation based on svn revision numbers.
* This class expects the label format to be "x<sep>y[<sep>z]",
* where x is any String and y is an integer and <sep> a separator, the
* last part z, is optional, and gets generated and later incremented in case a
* build is forced. The default separator is "." and can be modified using
* {@link #setSeparator}.
*
* @author Ketan Padegaonkar < KetanPadegaonkar gmail >
*/
/// UPDATE: return revisionNumber if separator is as same as prefix - Boathill
public class ZSVNLabeller implements LabelIncrementer {
private static final Logger LOG = Logger.getLogger(ZSVNLabeller.class);
private String workingCopyPath = ".";
private String labelPrefix = "svn";
private String separator = ".";
public boolean isPreBuildIncrementer() {
return true;
}
public String incrementLabel(String oldLabel, Element buildLog) {
String revisionNumber = "";
String result = oldLabel;
try {
revisionNumber = getSvnRevision();
if (getSeparator().equals(getLabelPrefix())) {
return labelPrefix + revisionNumber; // return svn revison only
}
if (revisionNumber == null || revisionNumber.equals("")) {
return labelPrefix;
}
result = labelPrefix + getSeparator() + revisionNumber;
if (oldLabel.indexOf(result) > -1) {
int lastSeparator = oldLabel.lastIndexOf(getSeparator());
int firstSeparator = oldLabel.indexOf(getSeparator());
int lastPart = 1;
if (lastSeparator != firstSeparator) {
String suffix = oldLabel.substring(lastSeparator + 1);
lastPart = Integer.parseInt(suffix) + 1;
}
result += getSeparator() + lastPart;
}
LOG.debug("Incrementing label from " + oldLabel + " to " + result);
} catch (IOException e) {
LOG.error("could not execute svn binary", e);
} catch (NumberFormatException e) {
LOG.error("could not increment label. Old label was " + oldLabel + ". svn revision was " + revisionNumber,
e);
}
return result;
}
protected String getSvnRevision() throws IOException {
String rev;
Process p = null;
try {
p = Runtime.getRuntime().exec(new String[]{"svnversion", workingCopyPath});
BufferedReader stdInput = new BufferedReader(new InputStreamReader(p.getInputStream()));
rev = stdInput.readLine();
} finally {
if (p != null) {
p.destroy();
}
}
LOG.debug("SVN revision is: " + rev);
return rev;
}
public boolean isValidLabel(String label) {
// we don't mind what the previous label is,
// when the next label is built, then parsing is performed to add / increment a suffix.
return true;
}
public void setWorkingCopyPath(String path) {
LOG.debug("Working Path is: " + path);
workingCopyPath = path;
}
public String getLabelPrefix() {
return this.labelPrefix;
}
public void setLabelPrefix(String labelPrefix) {
this.labelPrefix = labelPrefix;
}
public String getDefaultLabel() {
return getLabelPrefix() + getSeparator() + "0";
}
public String getSeparator() {
return this.separator;
}
public void setSeparator(String separator) {
this.separator = separator;
}
}
Comparison
Some other Continuous Integration products, e.g. Cruise and Hudson, have much nicer web interface, while CruiseControl.NET and CruiseControl provides better flexibility and can be greatly customized. However, the flexibility sometimes means difficulty. For example, CruiseControl has three web sites - dashboard (:8080), project cruisecontrol (:8000), and JMS console - but not be seen on one page; CruiseControl supports CCNET CCTray but could not integrate with CCNET projects or dashboard (even if both developed by ThoughtWorks).The build publisher supports using build label in both CCNET (by
<labeller />
) and CruiseControl (${label}
in ant config). And in CCNET, the build publisher has to copy all contents in a specified source to another location although ant or nant can be used to customize such task. CruiseControl web interfaces do have a little more advanced features than CCNET. As an instance, the build target files (artifacts) can be published on CruiseControl dashboard so that can be downloaded. But the artifact in CCNET is just a local folder not accessible from the web. On the other hand, CruiseControl does not display the runtime log on web interfaces. The build log can only be reviewed after the build is done. In CCNET dashboard, this is provided by a server log link. Although the server log in CCNET is not automatically updated (as nicely as a real time console display in Hudson), it at least gives user some hint before knowing the build succeeded or failed. Having better integration with Windows, CCNET can be run in either command line or service mode. For CruiseControl, you have to write a
/etc/init.d/cruisecontrol
.Since CruiseControl.NET derived from CruiseControl, both work the same way and the cofiguration shares similarity. As open source projects, it is convenient to develop or modify plugin to suite need during implementation. CruiseControl is cross-platform by Java technology. CCNET has improved on web integration and preprocessor configuration so that project file can be well structured via nodeset expansion template.
Note: Source code samples are formatted by SyntaxHighlighter.
Epigram
拟旧文学诗一首──驾车于高速公路戏作
我们是──
寄生在车轮上的腐朽阶级,
不爱工作,也不爱学习。
整日好吃好喝,每天昏睡不起,
沉迷了思想,又虚淘了身体。
有时也曾想振作自己,
可懒惰总是最大的天敌。
让舒适摧残了健康、
和用金钱也买不到的毅力;
才知道没有艰苦的付出,
不明白宝葫芦的秘密。
挥霍人生作游戏,
似在虚拟的空间里逃避。
坐井城市的天地,
分不清麦子和稻米。
如果和劳动人民比一比,
这样的生活有何意义?
2009.07.12 Seattle