source: trunk/CrypPlugins/QuadraticSieve/PeerToPeer.cs @ 1854

Last change on this file since 1854 was 1854, checked in by Sven Rech, 11 years ago

quadratic sieve fixes

File size: 27.7 KB
Line 
1/*                             
2   Copyright 2010 Sven Rech, Uni Duisburg-Essen
3
4   Licensed under the Apache License, Version 2.0 (the "License");
5   you may not use this file except in compliance with the License.
6   You may obtain a copy of the License at
7
8       http://www.apache.org/licenses/LICENSE-2.0
9
10   Unless required by applicable law or agreed to in writing, software
11   distributed under the License is distributed on an "AS IS" BASIS,
12   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13   See the License for the specific language governing permissions and
14   limitations under the License.
15*/
16
17using System;
18using System.Collections.Generic;
19using System.Linq;
20using System.Text;
21using System.IO.Compression;
22using System.IO;
23using Cryptool.P2P;
24using System.Numerics;
25using System.Diagnostics;
26using System.Collections;
27using System.Threading;
28using System.Windows.Threading;
29using System.Runtime.Serialization.Formatters.Binary;
30using System.Management;
31using System.Security.Principal;
32using System.Security.Cryptography;
33using Cryptool.P2P.Internal;
34
35namespace Cryptool.Plugins.QuadraticSieve
36{
37    class PeerToPeer
38    {
39        /// <summary>
40        /// This structure is used to hold all important peer performance and alive informations in a queue
41        /// </summary>
42        private struct PeerPerformanceInformations
43        {
44            public DateTime lastChecked;
45            public int peerID;
46            public double performance;
47            public int lastAlive;           
48
49            public PeerPerformanceInformations(DateTime lastChecked, int peerID, double performance, int lastAlive)
50            {
51                this.lastAlive = lastAlive;
52                this.lastChecked = lastChecked;
53                this.peerID = peerID;
54                this.performance = performance;
55            }
56        }
57
58        private string channel;
59        private BigInteger number;
60        private BigInteger factor;
61        private int head;
62        private Queue storequeue;   //relation packages to store in the DHT
63        private Queue loadqueue;    //relation packages that have been loaded from the DHT
64        private Thread loadStoreThread;
65        private bool stopLoadStoreThread;
66        private QuadraticSievePresentation quadraticSieveQuickWatchPresentation;
67        private AutoResetEvent newRelationPackageEvent;
68        private int ourID;           //Our ID
69        private Dictionary<int, string> nameCache;  //associates the ids with the names
70        private Queue<PeerPerformanceInformations> peerPerformances;      //A queue of performances from the different peers ordered by the date last checked.
71        private HashSet<int> activePeers;                                 //A set of active peers
72        private double ourPerformance = 0;
73        private int aliveCounter = 0;       //This is stored together with the performance in the DHT
74        private string ourName;
75        private uint downloaded = 0;
76        private uint uploaded = 0;
77        private HashSet<int> ourIndices;
78        private Queue<KeyValuePair<int, DateTime>> lostIndices;
79        private int loadIndex;
80        private AutoResetEvent waitForConnection = new AutoResetEvent(false);
81
82        public delegate void P2PWarningHandler(String warning);
83        public event P2PWarningHandler P2PWarning;       
84
85        public PeerToPeer(QuadraticSievePresentation presentation, AutoResetEvent newRelationPackageEvent)
86        {
87            quadraticSieveQuickWatchPresentation = presentation;
88            this.newRelationPackageEvent = newRelationPackageEvent;
89            SetOurID();
90        }
91
92        public int getActivePeers()
93        {
94            if (activePeers == null)
95                return 0;
96            return activePeers.Count;
97        }
98
99        private void SetOurID()
100        {
101            string username = WindowsIdentity.GetCurrent().Name;
102            string mac = GetMacIdentifier();
103
104            MD5 md5 = new MD5CryptoServiceProvider();
105            byte[] idBytes = md5.ComputeHash(System.Text.ASCIIEncoding.ASCII.GetBytes(username + mac));
106
107            ourID = BitConverter.ToInt32(idBytes, 3);
108            quadraticSieveQuickWatchPresentation.ProgressRelationPackages.setOurID(ourID);
109
110            ourName = System.Net.Dns.GetHostName();
111        }
112
113        /// <summary>
114        /// Reads relation package at position "index" from DHT and returns the ownerID and the decompressed packet itself.
115        /// </summary>
116        private byte[] ReadRelationPackage(int index, out int ownerID)
117        {
118            ownerID = 0;
119            byte[] relationPackage = P2PManager.Retrieve(RelationPackageIdentifier(index)).Data;
120            if (relationPackage == null)
121                return null;
122            downloaded += (uint)relationPackage.Length;
123
124            byte[] decompressedRelationPackage = DecompressRelationPackage(relationPackage);
125
126            byte[] idbytes = new byte[4];
127            Array.Copy(decompressedRelationPackage, idbytes, 4);
128            ownerID = BitConverter.ToInt32(idbytes, 0);
129            byte[] y = new byte[decompressedRelationPackage.Length - 4];
130            Array.Copy(decompressedRelationPackage, 4, y, 0, y.Length);
131           
132            return y;
133        }
134
135        private static byte[] DecompressRelationPackage(byte[] relationPackage)
136        {
137            MemoryStream memStream = new MemoryStream();
138            DeflateStream defStream = new DeflateStream(memStream, CompressionMode.Decompress);
139            memStream.Write(relationPackage, 0, relationPackage.Length);
140            memStream.Position = 0;
141            MemoryStream memStream2 = new MemoryStream();
142            defStream.CopyTo(memStream2);
143            defStream.Close();
144            byte[] decompressedRelationPackage = memStream2.ToArray();
145            return decompressedRelationPackage;
146        }
147
148        private bool ReadAndEnqueueRelationPackage(int loadIndex, bool checkAlive)
149        {
150            int ownerID;
151            byte[] relationPackage = ReadRelationPackage(loadIndex, out ownerID);
152            if (relationPackage != null)
153            {
154                ShowTransfered(downloaded, uploaded);
155                loadqueue.Enqueue(relationPackage);
156
157                string name = null;
158
159                if (ownerID != ourID)
160                {
161                    //Progress relation package:
162                    if (!nameCache.ContainsKey(ownerID))
163                    {
164                        byte[] n = P2PManager.Retrieve(NameIdentifier(ownerID)).Data;
165                        if (n != null)
166                            nameCache.Add(ownerID, System.Text.ASCIIEncoding.ASCII.GetString(n));
167                    }
168                    if (nameCache.ContainsKey(ownerID))
169                        name = nameCache[ownerID];
170                                       
171                    if (checkAlive && !activePeers.Contains(ownerID))
172                    {
173                        //check performance and alive informations:
174                        byte[] performancebytes = P2PManager.Retrieve(PerformanceIdentifier(ownerID)).Data;
175                        if (performancebytes != null)
176                        {
177                            double performance = BitConverter.ToDouble(performancebytes, 0);
178                            int peerAliveCounter = BitConverter.ToInt32(performancebytes, 8);
179                            peerPerformances.Enqueue(new PeerPerformanceInformations(DateTime.Now, ownerID, performance, peerAliveCounter));
180                        }
181                        activePeers.Add(ownerID);
182                        UpdateActivePeerInformation();
183                    }
184                }
185
186                //Set progress info:
187                SetProgressRelationPackage(loadIndex, ownerID, name);
188
189                newRelationPackageEvent.Set();
190                return true;
191            }
192            return false;
193        }
194
195        /// <summary>
196        /// Tries to read and enqueue relation package on position "loadIndex".
197        /// If it fails, it stores the index in lostIndices queue.
198        /// </summary>
199        private void TryReadAndEnqueueRelationPackage(int index, bool checkAlive, Queue<KeyValuePair<int, DateTime>> lostIndices)
200        {
201            bool succ = ReadAndEnqueueRelationPackage(index, checkAlive);
202            if (!succ)               
203            {
204                var e = new KeyValuePair<int, DateTime>(index, DateTime.Now);
205                lostIndices.Enqueue(e);
206                SetProgressRelationPackage(index, -1, null);
207            }
208        }
209
210        private void LoadStoreThreadProc()
211        {
212            loadIndex = 0;
213            downloaded = 0;
214            uploaded = 0;
215            ourIndices = new HashSet<int>();   //Stores all the indices which belong to our packets
216            //Stores all the indices (together with there check date) which belong to lost packets (i.e. packets that can't be load anymore):
217            lostIndices = new Queue<KeyValuePair<int, DateTime>>();
218            double lastPerformance = 0;
219            DateTime performanceLastPut = new DateTime();
220
221            try
222            {
223                WaitTillConnected();
224
225                //store our name:
226                P2PManager.Retrieve(NameIdentifier(ourID));     //just to outsmart the versioning system
227                P2PManager.Store(NameIdentifier(ourID), System.Text.ASCIIEncoding.ASCII.GetBytes(ourName.ToCharArray()));
228
229                head = 0;
230                SynchronizeHead();
231                int startHead = head;
232
233                while (!stopLoadStoreThread)
234                {
235                    try
236                    {
237                        //Store our performance and our alive counter in the DHT, either if the performance changed or when the last write was more than 1 minute ago:
238                        if (ourPerformance != lastPerformance || performanceLastPut.CompareTo(DateTime.Now.Subtract(new TimeSpan(0, 1, 0))) < 0)
239                        {
240                            P2PManager.Retrieve(PerformanceIdentifier(ourID));      //just to outsmart the versioning system
241                            P2PManager.Store(PerformanceIdentifier(ourID), concat(BitConverter.GetBytes(ourPerformance), BitConverter.GetBytes(aliveCounter++)));
242                            lastPerformance = ourPerformance;
243                            performanceLastPut = DateTime.Now;
244                        }
245
246                        //updates all peer performances which have last been checked more than 1 minutes and 20 seconds ago and check if they are still alive:
247                        while (peerPerformances.Count != 0 && peerPerformances.Peek().lastChecked.CompareTo(DateTime.Now.Subtract(new TimeSpan(0, 1, 20))) < 0)
248                        {
249                            var e = peerPerformances.Dequeue();
250
251                            byte[] performancebytes = P2PManager.Retrieve(PerformanceIdentifier(e.peerID)).Data;
252                            if (performancebytes != null)
253                            {
254                                double performance = BitConverter.ToDouble(performancebytes, 0);
255                                int peerAliveCounter = BitConverter.ToInt32(performancebytes, 8);
256                                if (peerAliveCounter <= e.lastAlive)
257                                {
258                                    activePeers.Remove(e.peerID);
259                                    UpdateActivePeerInformation();
260                                }
261                                else
262                                    peerPerformances.Enqueue(new PeerPerformanceInformations(DateTime.Now, e.peerID, performance, peerAliveCounter));
263                            }
264                            else
265                            {
266                                activePeers.Remove(e.peerID);
267                                UpdateActivePeerInformation();
268                            }                       
269                        }
270                                       
271                        SynchronizeHead();
272                        UpdateStoreLoadQueueInformation();
273
274                        bool busy = false;
275
276                        if (storequeue.Count != 0)  //store our packages
277                        {
278                            byte[] relationPackage = (byte[])storequeue.Dequeue();
279                            bool success = P2PManager.Store(RelationPackageIdentifier(head), relationPackage).IsSuccessful();
280                            while (!success)
281                            {
282                                SynchronizeHead();
283                                success = P2PManager.Store(RelationPackageIdentifier(head), relationPackage).IsSuccessful();
284                            }
285                            SetProgressRelationPackage(head, ourID, null);
286                            ourIndices.Add(head);
287                       
288                            head++;
289
290                            //show informations about the uploaded relation package:
291                            uploaded += (uint)relationPackage.Length;
292                            ShowTransfered(downloaded, uploaded);
293                            UpdateStoreLoadQueueInformation();
294                            busy = true;
295                        }
296
297                        //load the other relation packages:
298
299                        //skip all indices which are uploaded by us:
300                        while (ourIndices.Contains(loadIndex))
301                            loadIndex++;
302
303                        if (loadIndex < head)
304                        {
305                            bool checkAlive = loadIndex >= startHead;       //we don't check the peer alive informations of the packages that existed before we joined
306                            TryReadAndEnqueueRelationPackage(loadIndex, checkAlive, lostIndices);
307                            loadIndex++;
308                            UpdateStoreLoadQueueInformation();
309                            busy = true;
310                        }
311                   
312                        //check lost indices which are last checked longer than 2 minutes ago (but only if we have nothing else to do):
313                        if (!busy)
314                        {
315                            //TODO: Maybe we should throw away those indices, which have been checked more than several times.
316                            if (lostIndices.Count != 0 && lostIndices.Peek().Value.CompareTo(DateTime.Now.Subtract(new TimeSpan(0, 2, 0))) < 0)
317                            {
318                                var e = lostIndices.Dequeue();
319                                TryReadAndEnqueueRelationPackage(loadIndex, true, lostIndices);
320                                UpdateStoreLoadQueueInformation();                           
321                            }
322                            else
323                                Thread.Sleep(5000);    //Wait 5 seconds
324                        }
325
326                    }
327                    catch (NotConnectedException)
328                    {
329                        WaitTillConnected();
330                    }
331                }
332            }
333            catch (ThreadInterruptedException)
334            {
335                return;
336            }
337            catch (NotConnectedException)
338            {
339                WaitTillConnected();
340                LoadStoreThreadProc();
341            }
342        }
343
344        private void WaitTillConnected()
345        {
346            if (!P2PManager.IsConnected)
347            {
348                P2PManager.ConnectionManager.OnP2PConnectionStateChangeOccurred += HandleConnectionStateChange;
349                waitForConnection.WaitOne();
350                P2PManager.ConnectionManager.OnP2PConnectionStateChangeOccurred -= HandleConnectionStateChange;
351            }
352        }
353
354        private void HandleConnectionStateChange(object sender, bool newState)
355        {
356            if (newState)
357                waitForConnection.Set();
358        }
359
360        private void UpdateActivePeerInformation()
361        {
362            quadraticSieveQuickWatchPresentation.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
363            {
364                quadraticSieveQuickWatchPresentation.amountOfPeers.Content = "" + activePeers.Count + " other peer" + (activePeers.Count!=1 ? "s" : "") + " active!";
365            }, null);
366        }
367
368        private void UpdateStoreLoadQueueInformation()
369        {
370            if (storequeue != null && ourIndices != null && lostIndices != null)
371            {
372                quadraticSieveQuickWatchPresentation.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
373                {
374                    int upload = storequeue.Count;
375                    int download = head - loadIndex - ourIndices.Count(x => x > loadIndex);
376                    int lost = this.lostIndices.Count;
377                    quadraticSieveQuickWatchPresentation.queueInformation.Content = "Queue: Upload " + upload + "! Download " + download + "! Lost " + lost + "!";
378                }, null);
379            }
380        }
381
382        /// <summary>
383        /// Loads head from DHT. If ours is greater (or there is no entry yet), we store ours.
384        /// </summary>
385        private void SynchronizeHead()
386        {
387            byte[] h = P2PManager.Retrieve(HeadIdentifier()).Data;
388            if (h != null)
389            {
390                int dhthead = int.Parse(System.Text.ASCIIEncoding.ASCII.GetString(h));
391                if (head > dhthead)
392                {
393                    bool success = P2PManager.Store(HeadIdentifier(), System.Text.ASCIIEncoding.ASCII.GetBytes(head.ToString())).IsSuccessful();
394                    if (!success)
395                        SynchronizeHead();
396                }
397                else if (head < dhthead)
398                {
399                    head = dhthead;
400                    if (head != 0)
401                        SetProgressRelationPackage(head - 1, 0, null);
402                }
403            }
404            else
405            {
406                bool success = P2PManager.Store(HeadIdentifier(), System.Text.ASCIIEncoding.ASCII.GetBytes(head.ToString())).IsSuccessful();
407                if (!success)
408                    SynchronizeHead();
409            }           
410        }
411
412        private void ShowTransfered(uint downloaded, uint uploaded)
413        {
414            string s1 = ((downloaded / 1024.0) / 1024).ToString();
415            string size1 = s1.Substring(0, (s1.Length < 3) ? s1.Length : 3);
416            string s2 = ((uploaded / 1024.0) / 1024).ToString();
417            string size2 = s2.Substring(0, (s2.Length < 3) ? s2.Length : 3);
418            quadraticSieveQuickWatchPresentation.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
419            {
420                quadraticSieveQuickWatchPresentation.relationsInfo.Content = "Downloaded " + size1 + " MB! Uploaded " + size2 + " MB!";
421            }, null);
422        }
423
424        private void SetProgressRelationPackage(int index, int id, string name)
425        {
426            quadraticSieveQuickWatchPresentation.Dispatcher.BeginInvoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
427            {
428                quadraticSieveQuickWatchPresentation.ProgressRelationPackages.Set(index, id, name);
429            }, null);
430        }
431
432        private void ClearProgressRelationPackages()
433        {
434            quadraticSieveQuickWatchPresentation.Dispatcher.Invoke(DispatcherPriority.Normal, (SendOrPostCallback)delegate
435            {
436                quadraticSieveQuickWatchPresentation.ProgressRelationPackages.Clear();
437            }, null);
438        }
439
440        private string HeadIdentifier()
441        {
442            return channel + "#" + number + "-" + factor + "HEAD";
443        }
444
445        private string FactorListIdentifier()
446        {
447            return channel + "#" + number + "FACTORLIST";
448        }
449
450        private string RelationPackageIdentifier(int index)
451        {
452            return channel + "#" + number + "-" + factor + "!" + index;
453        }
454
455        private string NameIdentifier(int ID)
456        {
457            return channel + "#" + number + "NAME" + ID.ToString();
458        }
459
460        private string PerformanceIdentifier(int ID)
461        {
462            return channel + "#" + number + "-" + factor + "PERFORMANCE" + ID.ToString();
463        }
464
465        public string StatusKey()
466        {
467            return channel + "#" + number + "-status";
468        }
469
470        private void StartLoadStoreThread()
471        {
472            storequeue = Queue.Synchronized(new Queue());
473            loadqueue = Queue.Synchronized(new Queue());
474
475            stopLoadStoreThread = false;
476            loadStoreThread = new Thread(LoadStoreThreadProc);
477            loadStoreThread.Start();
478        }
479
480        public void StopLoadStoreThread()
481        {
482            stopLoadStoreThread = true;
483            if (loadStoreThread != null)
484            {
485                loadStoreThread.Interrupt();
486                loadStoreThread.Join();
487                loadStoreThread = null;
488            }
489        }
490
491        /// <summary>
492        /// Concatenates the two byte arrays a1 and a2 an returns the result array.
493        /// </summary>
494        private byte[] concat(byte[] a1, byte[] a2)
495        {
496            byte[] res = new byte[a1.Length + a2.Length];
497            System.Buffer.BlockCopy(a1, 0, res, 0, a1.Length);
498            System.Buffer.BlockCopy(a2, 0, res, a1.Length, a2.Length);
499            return res;
500        }
501       
502        /// <summary>
503        /// Returns an identifier that depends on the MAC addresses of this system
504        /// </summary>       
505        private string GetMacIdentifier()
506        {
507            string MacID = "";
508            ManagementClass MC = new ManagementClass("Win32_NetworkAdapter");
509            ManagementObjectCollection MOCol = MC.GetInstances();
510            foreach (ManagementObject MO in MOCol)
511                if (MO != null)
512                    if (MO["MacAddress"] != null)
513                        MacID += MO["MACAddress"].ToString();
514            return MacID;
515        }
516
517        public Queue GetLoadedRelationPackagesQueue()
518        {
519            return loadqueue;
520        }
521
522        /// <summary>
523        /// Compresses the relation package and puts it in the DHT.
524        /// </summary>
525        public void Put(byte[] serializedRelationPackage)
526        {
527            //Add our ID:
528            byte[] idbytes = BitConverter.GetBytes(ourID);
529            Debug.Assert(idbytes.Length == 4);
530            serializedRelationPackage = concat(idbytes, serializedRelationPackage); ;
531
532            //Compress:
533            MemoryStream memStream = new MemoryStream();
534            DeflateStream defStream = new DeflateStream(memStream, CompressionMode.Compress);
535            defStream.Write(serializedRelationPackage, 0, serializedRelationPackage.Length);
536            defStream.Close();
537            byte[] compressedRelationPackage = memStream.ToArray();
538
539            //Debug stuff:
540            byte[] decompr = DecompressRelationPackage(compressedRelationPackage);
541            Debug.Assert(decompr.Length == serializedRelationPackage.Length);
542
543            //store in queue, so the LoadStoreThread can store it in the DHT later:
544            storequeue.Enqueue(compressedRelationPackage);
545
546            UpdateStoreLoadQueueInformation();
547        }
548
549        /// <summary>
550        /// Sets the channel in which we want to sieve
551        /// </summary>
552        public void SetChannel(string channel)
553        {
554            this.channel = channel;
555        }
556
557        /// <summary>
558        /// Sets the number to sieve
559        /// </summary>
560        public void SetNumber(BigInteger number)
561        {
562            this.number = number;
563        }
564
565        /// <summary>
566        /// Sets the factor to sieve next and starts reading informations from the DHT.
567        /// </summary>
568        public void SetFactor(BigInteger factor)
569        {
570            this.factor = factor;
571            Debug.Assert(this.number % this.factor == 0);
572
573            ClearProgressRelationPackages();
574            nameCache = new Dictionary<int, string>();
575            peerPerformances = new Queue<PeerPerformanceInformations>();
576            activePeers = new HashSet<int>();
577           
578            if (loadStoreThread != null)
579                throw new Exception("LoadStoreThread already started");
580            StartLoadStoreThread();
581        }
582
583        /// <summary>
584        /// Synchronizes the factorManager with the DHT.
585        /// Return false if this.factor is not a composite factor in the DHT (which means, that another peer already finished sieving this.factor).
586        /// </summary>
587        public bool SyncFactorManager(FactorManager factorManager)
588        {
589            FactorManager dhtFactorManager = null;
590            //load DHT Factor Manager:
591            byte[] dhtFactorManagerBytes = P2PManager.Retrieve(FactorListIdentifier()).Data;
592            if (dhtFactorManagerBytes != null)
593            {
594                MemoryStream memstream = new MemoryStream();
595                memstream.Write(dhtFactorManagerBytes, 0, dhtFactorManagerBytes.Length);
596                memstream.Position = 0;
597                BinaryFormatter bformatter = new BinaryFormatter();
598                bformatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
599                try
600                {
601                    dhtFactorManager = (FactorManager)bformatter.Deserialize(memstream);
602                }
603                catch (System.Runtime.Serialization.SerializationException)
604                {
605                    P2PWarning("DHT factor list is broken!");
606                    P2PManager.Remove(FactorListIdentifier());
607                    return SyncFactorManager(factorManager);
608                }               
609            }
610
611            //Synchronize DHT Factor Manager with our Factor List
612            if (dhtFactorManager == null || factorManager.Synchronize(dhtFactorManager))
613            {
614                //Our Factor Manager has more informations, so let's store it in the DHT:
615                MemoryStream memstream = new MemoryStream();
616                BinaryFormatter bformatter = new BinaryFormatter();
617                bformatter.AssemblyFormat = System.Runtime.Serialization.Formatters.FormatterAssemblyStyle.Simple;
618                bformatter.Serialize(memstream, factorManager);
619                bool success = P2PManager.Store(FactorListIdentifier(), memstream.ToArray()).IsSuccessful();
620                if (!success)
621                {
622                    Thread.Sleep(1000);
623                    return SyncFactorManager(factorManager);       //Just try again
624                }
625            }
626
627            return factorManager.ContainsComposite(this.factor);
628        }
629
630        /// <summary>
631        /// Sets the performance of this machine, so that this class can write it to the DHT later.
632        /// The performance is meassured in relations per ms.
633        /// </summary>
634        public void SetOurPerformance(double[] relationsPerMS)
635        {
636            double globalPerformance = 0;
637            foreach (double r in relationsPerMS)
638                globalPerformance += r;
639
640            ourPerformance = globalPerformance;
641        }
642
643        /// <summary>
644        /// Returns the performance of all peers (excluding ourselve) in relations per ms.
645        /// </summary>
646        public double GetP2PPerformance()
647        {
648            double perf = 0;
649
650            foreach (var p in peerPerformances)
651                perf += p.performance;
652
653            return perf;
654        }
655    }
656}
Note: See TracBrowser for help on using the repository browser.