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

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

fixed stop bug in quadratic sieve plugin

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