Last minute geek

last minute tech news from around the net

You are here: English WTF

WTF

CodeSOD: Eventful Timing

I once built a system with the job of tracking various laboratory instruments, and sending out notifications when they needed to be calibrated. The rules for when different instruments triggered notifications, and when notifications should be sent, and so on, were very complicated.

An Anonymous reader has a similar problem. They’re tracking “Events”- like seminars and conferences. These multi-day events often have an end date, but some of them are actually open ended events. They need to, given an event, be able to tell you how much it costs. And our Anonymous reader’s co-worker came up with this solution to that problem:

public class RunningEventViewModel : SingleDataViewModel<EventData>
{
    private DateTime _now;

    private readonly Timer _timer;

    public RunningEventViewModel(EventData data)
        : base(data)
    {
        _now = DateTime.Now;

        _timer = new Timer(x =>
        {
            _now = DateTime.Now;
            NotifyOfPropertyChange(() => DurationDays);
            NotifyOfPropertyChange(() => DurationHours);
            NotifyOfPropertyChange(() => DurationMinutes);
            NotifyOfPropertyChange(() => DurationSeconds);
            NotifyOfPropertyChange(() => DurationString);
            NotifyOfPropertyChange(() => TotalCost);
        });

        if (!Stop.HasValue)
        {
            _timer.Change(900, 900);
        }
    }

    public int DurationSeconds
    {
        get
        {
            return ((Stop ?? _now) - Start).Seconds;
        }
    }

    public decimal TotalCost
    {
        get
        {
            var end = Stop ?? _now;
            const int SecsPerDay = 3600 * 24;
            decimal days = ((int)(end - Start).Duration().TotalSeconds) / (decimal)SecsPerDay;

            return (DailyCosts.HasValue ? DailyCosts.Value : 0) * days;
        }
    }

    public int DurationDays
    {
        get
        {
            return ((Stop ?? _now) - Start).Days;
        }
    }

    public int DurationHours
    {
        get
        {
            return ((Stop ?? _now) - Start).Hours;
        }
    }

    public int DurationMinutes
    {
        get
        {
            return ((Stop ?? _now) - Start).Minutes;
        }
    }

    public TimeSpan Duration
    {
        get
        {
            return (Stop ?? _now) - Start;
        }
    }

    public string FromTo
    {
        get
        {
            var res = String.Empty;
            if (Data.EventStart.HasValue)
            {
                res += Data.EventStart.Value.ToShortDateString();

                if (Data.EventStop.HasValue)
                {
                    res += " - " + Data.EventStop.Value.ToShortDateString();
                }
            }

            return res;
        }
    }

    public string DurationString
    {
        get
        {
            var duration = ((Stop ?? _now) - Start).Duration();
            var res = new StringBuilder();
            if (DurationDays > 0)
            {
                res.Append(duration.Days).Append(" days ");
            }

            if (DurationHours > 0)
            {
                res.Append(duration.Hours).Append(" hours ");
            }

            if (DurationMinutes > 0)
            {
                res.Append(duration.Minutes).Append(" minutes");
            }

            return res.ToString().TrimEnd(' ');
        }
    }

    public DateTime Start
    {
        get
        {
            return Get(Data.EventStart.Value, x => x.ToLocalTime().ToUniversalTime());
        }
    }

    public DateTime? Stop
    {
        get { return (DateTime?)null; }
    }

    public decimal? DailyCosts
    {
        get { return Data.DailyCost; }
    }

    private static TResult Get<TItem, TResult>(TItem item, Func<TItem, TResult> resultSelector)
    {
        // this method is actually an extension method from our core framework, it's added here for better understanding the code
        if(Equals(item, null))
        {
            return default(TResult);
        }

        return resultSelector(item);
    }
}

Is null a mistake? Well, it certainly makes this code more complicated. Speaking of more complicated, I like how this class is responsible for tracking a Timer object so that it can periodically emit events. So much for single-responsibility, and good luck during unit testing.

The real WTF here, however, is that .NET has a rich and developer-friendly date-time API, which already has pre-built date difference functions. Code like this block in TotalCost:

            const int SecsPerDay = 3600 * 24;
            decimal days = ((int)(end - Start).Duration().TotalSeconds) / (decimal)SecsPerDay;

… is completely unnecessary. Similarly, the FromTo and DurationString functions could benefit from a little use of the TimeSpan object, and a little String.format. And it’s clear that the developer knows these exist, because they’ve used them, yet in TotalCost, it’s apparently too much to bear.

But the real topper, on this, is the Stop property. Once you look at that, most of the code in this file becomes downright stupid.

[Advertisement] Release! is a light card game about software and the people who make it. Play with 2-5 people, or up to 10 with two copies - only $9.95 shipped!

Read all

Error'd: Banking on the Information Super Highway

"Good to see Santander finally embracing modern technology!" writes Sam B.

 

"I imagine the text could read 'Welcome user! Launch the game since you have no friends anyway and are beyond help'. Yay," writes Ruff.

 

Alister wrote, "Getting on the wifi with the 'network' cable was a snap but I found the range to be very limited."

 

"Seriously guys? WTF. They all look defined to me," B.J. wrote.

 

"Apparently my package had to travel back in time before it could get to me. Did I order a TARDIS by mistake?" writes Patrick.

 

While standing in line at customer service at Walmart, I spotted this on the customer facing screens on all of the registers at customer service. I wonder if someone wanted to buy tickets, would they be allowed?

 

Betsy R. writes "I once heard IBM's documentation described as sounding as if it had been translated from a foreign language by a bored high-school student. Maybe that's what happened here?"

 

[Advertisement] Universal Package Manager - ProGet easily integrates with your favorite Continuous Integration and Build Tools, acting as the central hub to all your essential components. Learn more today!

Read all

CodeSOD: Extended Conditions

Every programming language embodies in it a philosophy about how problems should be solved. C reduces all problems to manipulations of memory addresses. Java turns every problem into a set of interacting objects. JavaScript summons Shub-Niggurath, the black goat of the woods with a thousand young, to eat the eyes of developers.

Just following the logic of a language can send you a long way to getting good results. Popular languages were designed by smart people, who work through many of the problems you might encounter when building a program with their tools. That doesn’t mean that you can’t take things a bit too far and misapply that philosophy, though.

Take this code, sent to us by “Kogad”. Their co-worker understood that objects and interfaces were fundamental to Java programming, so when presented with the challenge of three conditional statements, they created this:

package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;

public interface ProcesSpecification {
        boolean isSatisfiedBy(CustomerRequest req);
}
//--------------
package com.initrode.account.framework.process.specification;

public abstract class CompositeProcesSpecification implements ProcesSpecification {

        public ProcesSpecification and(ProcesSpecification specification){
                return new AndProcesSpecification(this, specification);
        }

        public ProcesSpecification or(ProcesSpecification specification){
                return new OrProcesSpecification(this, specification);
        }

        public ProcesSpecification not(ProcesSpecification specification){
                return new NotProcesSpecification(specification);
        }

}
//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;

public class NotProcesSpecification extends CompositeProcesSpecification {

        private ProcesSpecification spec;

        public NotProcesSpecification(ProcesSpecification specification) {
                spec = specification;
        }

        @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return !spec.isSatisfiedBy(req);
        }

}

//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;

public class AndProcesSpecification extends CompositeProcesSpecification {
        private ProcesSpecification specOne;
        private ProcesSpecification specTwo;

        public AndProcesSpecification(ProcesSpecification specificationOne, ProcesSpecification specificationTwo) {
                specOne = specificationOne;
                specTwo = specificationTwo;
        }

        @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return specOne.isSatisfiedBy(req) && specTwo.isSatisfiedBy(req);
        }

}
//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;


public class OrProcesSpecification extends CompositeProcesSpecification {
        private ProcesSpecification specOne;
        private ProcesSpecification specTwo;

        public OrProcesSpecification(ProcesSpecification specificationOne, ProcesSpecification specificationTwo) {
                specOne = specificationOne;
                specTwo = specificationTwo;
        }

        @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return specOne.isSatisfiedBy(req) || specTwo.isSatisfiedBy(req);
        }

}
//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;
import com.initrode.account.framework.process.CustomerType;

public class TypeOneProcesSpecification extends CompositeProcesSpecification {

        @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return null != req && CustomerType.ONE == req.getType();
        }

}

//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;
import com.initrode.account.framework.process.CustomerType;

public class TypeTwoProcesSpecification extends CompositeProcesSpecification {

        @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return null != req && CustomerType.TWO == req.getType();
        }

}
//--------------
package com.initrode.account.framework.process.specification;

import com.initrode.account.framework.process.CustomerRequest;

public class VerifyProcessSpecification extends CompositeSpecification
    @Override
        public boolean isSatisfiedBy(CustomerRequest req) {
                return req.hasVerificationCode();
        }
}

//--------------
// Usage:

public class ActionOne {
        private ProcesSpecification procesSpec;

        @PostConstruct
        protected void postInitialize() {
                setProcesSpecification(new VerifyProcessSpecification().and(new TypeOneProcesSpecification()));
        }

    @Override public boolean canHandle(CustomerRequest req) {
        return procesSpec.isSatisfiedBy(req);
    }

    @Override
    void doAction(...);
}

public class ActionTwo {
        private ProcesSpecification procesSpec;

        @PostConstruct
        protected void postInitialize() {
                setProcesSpecification(new VerifyProcessSpecification().and(new TypeTwoProcesSpecification()));
        }

    @Override public boolean canHandle(CustomerRequest req) {
        return procesSpec.isSatisfiedBy(req);
    }

    @Override
    void doAction(...);
}

public class ActionThree {
        private ProcesSpecification procesSpec;

        @PostConstruct
        public void postInitialize() {
                procesSpec = new NotProcesSpecification(new VerifyProcessSpecification().and(
                                new TypeOneProcesSpecification().or(new TypeTwoProcesSpecification())));
        }

    @Override public boolean canHandle(CustomerRequest req) {
        return procesSpec.isSatisfiedBy(req);
    }

    @Override
    void doAction(...);
}

This is certainly Peak Java™. It’s… extensible, at least. Not that you’d want to. “Kogad” replaced this masterpiece with a much simpler, if less extensible, chain of conditionals that were also more closely mapped to their actual requirements.

[Advertisement] Otter enables DevOps best practices by providing a visual, dynamic, and intuitive UI that shows, at-a-glance, the configuration state of all your servers. Find out more and download today!

Read all

A Case of Denial

RGB color wheel 72

On his first day at his new job, Sebastian wasn't particularly excited. He'd been around the block enough times to have grown a thick skin of indifference and pessimism. This job was destined to be like any other, full of annoying coworkers, poorly thought out requirements, legacy codebases full of spaghetti. But it paid well, and he was tired of his old group, weary in his soul of the same faces he'd grown accustomed to. So he prepared himself for a new flavor of the same office politics and menial tasks.

It didn't faze him much when he walked into the IT office to pick up his credentials and heard the telltale buzzing and clicking of old Packard Bell servers. He simply adjusted his expectations for his own developer machine downward a few notches and walked back to his new office. Yes, this job came with a private office, and pay to match. For that, he could put up with a lot of BS.

His login worked on the first try, which was pleasantly surprising. He expected Windows XP; when Vista loaded, he wasn't sure if he should be pleased that the OS was newer, or horrified that it was Vista. He could pretend it was 7 for a while at least, once he finished getting admin privileges and nerfing UAC. It'll take more than that to scare me off, he thought to himself as he fired up Outlook.

Already, he had mail: a few welcome messages with new employee information, as well as his first assignment from his manager. Impressed with the efficiency in assigning work, if nothing else, he opened the message from his new boss.

That first email went a little something like this:

Hi Sebastian, welcome to our super-clean environment. We do everything right here. You will use Bonk-Word (a IBM documentation web app) for design documents. Remember to save your work often! If Bonk-Word crashes, you'll need to send an email to IT to have it restarted.

We do design documentation right here. Be sure to write everything in passive voice, use Purple for chapter headings and Green for section headings. We have document review with the company president every day at 9AM so be ready for that. It's a black mark on your permanent record to have any headings wrong.

Please start designing how you are going to fix our 4-year old Macintosh font issues. We need a six-page design document by 9AM tomorrow. Thanks.

Six pages by tomorrow? worried Sebastian. Maybe I rejoiced too soon on that efficiency thing. Well, at least I won't be bored. He cracked his knuckles, opened Bonk-Word, and set about examining these so-called font issues.

The first thing he learned was that his manager wasn't kidding when he said to save often. By the end of the day, he was mentally betting with himself on which would crash first: Bonk-Word or Vista itself. They both crashed approximately every half hour. But it was somehow soothing to keep count, making tally marks on a post-it note. It reminded him that something in this world still worked. Basic math wasn't impressive, but it was reliable. Steady. Solid.

Maybe he was a little lonely in his office. But it was quiet, and private, and even if the crashing was frustrating, he made progress. He stayed late to turn out his treatise on "Delving into the diverse and varied literature that exists on the subject of font rendering, including but not limited to the Postscript specification, accompanying literature indicating the best practices for the use thereof, and extant informational centers within the World Wide Web that have been created to gather wisdom from the best minds the industry has to offer in a familiar and comforting question-and-answer format." Lather, rinse, and repeat for "Writing a Python program to render each character." He took two pages to explain the fact that he would, essentially, eyeball the results.

If they want six pages, they're getting six pages, he thought.

A strange first day, but Sebastian could see himself sticking this out for a few years at least. He took his time as he walked through the building (which smelled suspiciously of old leather undergarments) to his "free assigned ramp parking spot" (another perk he told himself made the job worth it). Walking slowly was a good idea anyway, as the ramp had terminal rust-rot and there were many places where the concrete had completely fallen off, exposing the rebar in the floors and columns.

The next morning, at 9:00 sharp, Sebastian found himself in his manager's office for his first design review with the company president, held via conference call. Sebastian was uneasy about meeting with the president directly, given the company had sixty employees, but he took it in stride.

I did as they asked, wordy as it was. Probably this is a formality and then I can get to work.

A humiliated, exhausted Sebastian crawled back to his office an hour later, his ears ringing from the nonsensical yet harsh critique he'd received. According to the president, his headings were merely "greenish" instead of the company-mandated Green, and his chapter headings were unforgivably "reddish" rather than the expected Purple. Furthermore, he'd been informed in no uncertain terms that it was "impossible" to debug the font using Python. Instead, he was to work in C++, using the company's "marvelous" software libraries. Sebastian's manager had praised the document while they were waiting for the president, but had failed to utter a single word once the review began, his eyes fixed firmly on the brick wall behind his desk.

Sebastian closed the door to his office, blocking out the rest of the company. He sat in his plush leather chair, staring at the machine that barely worked. He opened his document again, then rebooted his machine once Vista decided to crash. When the machine came back up again, he checked his bank balance, thought of his mortgage, and gritted his teeth.

"All right," he said aloud to his empty office. "Let's see about those libraries."

The first thing he looked for was documentation. Surely, in a company as document-focused as this one, the documentation for the "marvelous" libraries would be exactly the right shade of exactly the right font, with exactly the right chapter headings and section names. Instead, it appeared to be ... missing. There were design documents galore, and their greens were more green and their purples showed far less red. But they only spelled out the methodology behind the development of the library, and said nothing of its proper usage.

Am I going mad? Sebastian asked himself as his machine restarted for the third time. Maybe the code is self-documenting ...

To his horror, but not particularly his surprise, the libraries simply consisted of poorly thought out wrappers around basic string functions from the standard library.

Sebastian gave it his all despite the setbacks. Every day, he was summoned for another round of verbal browbeating. The company had made no progress in the past four years with this font issue, and yet, nothing he did was good enough for the president. Sebastian gave up on the custom library, sticking with the Python he knew; after all, if he was going to be berated anyway, why bother trying to do as he was told? But no matter whether he used his own font tester in Python, or Microsoft's tester, or Apple's, or Adobe's, the font was an absolute mess. 488 intrinsic, unfixable, unkludgable design errors.

The president flatly denied the truth before him. It had to be Sebastian's fault for not using the wonderful C++ libraries.

Out of options, Sebastian left the key to the rusting, collapsing wreck of a garage on his manager's desk along with a letter of resignation. He kissed his lovely office with its decrepit pile of crap they called a machine goodbye. He took a deep breath, letting that disturbing leather smell permeate his nostrils one last time. Then he left, never to return.

Somehow, he doubted he'd miss the place.

[Advertisement] Application Release Automation for DevOps – integrating with best of breed development tools. Free for teams with up to 5 users. Download and learn more today!

Read all

CodeSOD: Mapping Every Possibility

Capture all

Today, Aaron L. shares the tale of an innocent little network mapping program that killed itself with its own thoroughness:

I was hired to take over development on a network topology mapper that came from an acquisition. The product did not work except in small test environments. Every customer demo was a failure.

The code below was used to determine if two ports on two different switches are connected. This process was repeated for every switch in the network. As the number of switches, ports, and MAC addresses increased the run time of the product went up exponentially and typically crashed with an array index out of bounds exception. The code below is neatly presented, the actual code took me over a day of repeatedly saying "WTF?" before I realized the original programmer had no idea what a Map or Set or List was. But after eliminating the arrays the flawed matching algorithm was still there and so shortly all of the acquired code was thrown away and the mapper was re-written from scratch with more efficient ways of connecting switches.


public class Switch {
    Array[] allMACs = new Array[numMACs];
    Array[] portIndexes = new Array[numPorts];
    Array[] ports = new Array[numPorts];

    public void load() {
        // load allMACs by reading switch via SNMP
        // psuedo code to avoid lots of irrelevant SNMP code
        int portCounter = 0;
        int macCounter = 0;
        for each port {
            ports[portCounter] = port;
            portIndexes[portCounter] = macCounter;
            for each MAC on port {
                 allMACs[macCounter++] = MAC;
            }
        }
    }

    public Array[] getMACsForPort(int port) {
        int startIndex;
        int endIndex;
        for (int ictr = 0; ictr < ports.length; ictr++) {
            if (ports[ictr] == port) {
                startIndex = portIndexes[ictr];
                endIndex = portIndexes[ictr + 1];
            }
        }
        Array[] portMACS = new Array[endIndex - startIndex];
        int pctr = 0;
        for (int ictr = startIndex; ictr < endIndex - 1; ictr++) {
            portMACS[pctr++] = allMACs[ictr];
        }
        return(portMACS);
    }
}

...
for every switch in the network {
    for every other switch in the network {
        for every port on switch {
            Array[] switchPortMACs = Switch.getMACsForPort(port);
            for every port on other switch {
                Array[] otherSwitchPortMACs = OtherSwitch.getMACsForPort(other port);
                if (intersect switchPortMACs with otherSwitchPortMACs == true) {
                   connect switch.port with otherSwitch.port;
                }
            }
        }
    }
}

[Advertisement] Onsite, remote, bare-metal or cloud – create, configure and orchestrate 1,000s of servers, all from the same dashboard while continually monitoring for drift and allowing for instantaneous remediation. Download Otter today!

Read all

Healthcare Can Make You Sick

Every industry has information that needs to be moved back and forth between disparate systems. If you've lived a wholesome life, those systems are just different applications on the same platform. If you've strayed from the Holy Path, those systems are written using different languages on different platforms running different operating systems on different hardware with different endian-ness. Imagine some Java app on Safari under some version of Mac OS needing to talk to some version of .NET under some version of Windows needing to talk to some EBCIDIC-speaking version of COBOL running on some mainframe.

Long before anyone envisioned the above nightmare, we used to work with SGML, which devolved into XML, which was supposed to be a trivial tolerable way to define the format and fields contained in a document, with parsers on every platform, so that information could be exchanged without either end needing to know anything more than the DTD and/or schema for purposes of validation and parsing.

In a hopelessful attempt at making this somewhat easier, wrapper libraries were written on top of XML.

Sadly, they failed.

A hand holding a large pile of pills, in front of a background of pills

In the health care industry, some open-source folks created the (H)ealthcare (API), or HAPI project, which is basically an object oriented parser for text-based healthcare industry messages. Unfortunately, it appears they suffered from Don't-Know-When-To-Stop-Syndrome™.

Rather than implementing a generic parser that simply splits a delimited or fixed-format string into a list of text-field-values, the latest version implements 1205 different parsers, each for its own top-level data structure. Most top level structures have dozens of sub-structures. Each parser has one or more accessor methods for each field. Sometimes, a field can be a single instance, or a list of instances, in which case you must programmatically figure out which accessor to use.

That's an API with approximately 15,000 method calls! WTF were these developers thinking?

For example, the class: EHC_E15_PAYMENT_REMITTANCE_DETAIL_INFO can have zero or more product service sections. So right away, I'm thinking some sort of array or list. Thus, instead of something like:

    EHC_E15_PAYMENT_REMITTANCE_DETAIL_INFO info = ...;
    List<EHC_E15_PRODUCT_SERVICE_SECTION> prodServices = info.getProductServices();
    // iterate

... you need to do one of these:

    // Get sub-structure
    EHC_E15_PAYMENT_REMITTANCE_DETAIL_INFO info = ...;
	
    // Get embedded product-services from sub-structure

    // ...if you know for certain that there will be exactly one in the message:
    EHC_E15_PRODUCT_SERVICE_SECTION prodSvc = info.getPRODUCT_SERVICE_SECTION();
	
    // ...if you don't know how many there will be:
    int n = infos.getPRODUCT_SERVICE_SECTIONReps();
    for (int i=0; i<n; i++) {
        EHC_E15_PRODUCT_SERVICE_SECTION prodSvc = info.getPRODUCT_SERVICE_SECTION(i);
        // use it
    }

    // ...or you can just grab them all and iterate
    List<EHC_E15_PRODUCT_SERVICE_SECTION> allSvcs = info.getPRODUCT_SERVICE_SECTIONAll();

...and you need to call the correct one, or risk an exception. But having multiple ways of accomplishing the same thing via the API leads to multiple ways of doing the same thing in the code that is using the API, which invariably leads to problems.

So you might say, OK, that's not SO bad; you just use what you need. Until you realize that some of these data structures are embedded ten+ levels deep, each with dozens of sub-structures and/or fields, each with multiple accessors. With those really long names. Then you realize that the developers of the HAPI got tired of typing and just started using acronyms for everything, with such descriptive data structure names as: LA1, ILT and PCR.

The API does attempt to be helpful in that if it doesn't find what it's expecting in the field you ask it to parse, it throws an exception and it's up to you to figure out what went wrong. Of course, this implies that you already know what is being sent to you in the data stream.

Anonymous worked in the healthcare industry and was charged with maintaining a library that had been wrapped around HAPI. He was routinely assigned tasks (with durations of several weeks) to simply parse one additional field. After spending far too much time choking down the volumes of documentation on the API, he wrote a generic single-class 300 line parser with some split's, substring's, parseDate's and parseInt's to replace the whole thing.

Now adding an additional field takes all of ten minutes.

[Advertisement] Release! is a light card game about software and the people who make it. Play with 2-5 people, or up to 10 with two copies - only $9.95 shipped!

Read all

Error'd: Errors for Everyone!

"All I wanted to do was to unsubscribe from Credit Sesame emails, but instead I got more than I bargained for," writes Shawn A.

 

Mike R. wrote, "Sure, Simon Rewards, I'll click the button to update GlassWire, but only if there's a reward in it for me."

 

"This blue screen ad campaign is really convincing. I'm sold!" writes Roger K.

 

"I hope it's just a quick coffee run," wrote Jeremy E.

 

Ben writes, "It's good to see Windows XP reliably running ticket machines in the Southern Rail region of the UK ( picture credit to Gil Tompkinson from Brighton)"

 

"Jerusalem Central bus station has installed new automatic ticket machines which is nice," Eugene F. wrote, "I certainly would like the interface to be at least a little bit more descriptive though."

 

Phil wriktes, "A colleague was driving behind a van labelled with farmersmeats.co.uk - so we decided to check out their site. However based on the 'Latin' descriptions alone it is difficult to tell each meat apart."

 

[Advertisement] Universal Package Manager – store all your Maven, NuGet, Chocolatey, npm, Bower, TFS, TeamCity, Jenkins packages in one central location. Learn more today!

Read all