Tuesday, September 19, 2017

Gradle 4: Copy War to Directory

This took me 90 minutes to figure out and the internet was no help.

I was trying to copy the output of my gradle war project to a Tomcat webapps directory so I can run it in the debugger. 

I defined "tomcathome" in gradle.properties.  So far so good.

Then after reading a bunch of confusing stuff about how to define gradle tasks, I came up with this:

    task deployTomcat (dependsOn: 'war') {
        doLast {
            mkdir "${tomcathome}/webapps/${project.name}"
            copy {
                from zipTree(war)
                into "${tomcathome}/webapps/${project.name}"

This task got an error:
Execution failed for task ':deployTomcat'.
> Cannot convert the provided notation to a File or URI: task ':war'.
  The following types/formats are supported:
    - A String or CharSequence path, for example 'src/main/java' or '/usr/include'.
    - A String or CharSequence URI, for example 'file:/usr/include'.
    - A File instance.
    - A Path instance.
    - A URI or URL instance.
So it doesn't like 'zipTree(war)'.  I modified the code to print 'war' and 'war.outputs'.  'war.outputs' is an instance of org.gradle.api.internal.tasks.DefaultTaskOutputs

So I googled this, found the source on github, found that DefaultTaskOutputs has a 'files' property of type FileCollection, and FileCollection has a property called 'singleFile'.  So I added more prints and found that war.outputs.files.singleFile gives me the name of the war file.

So this is the working task:

task deployTomcat (dependsOn: 'war') {
    doLast {
        mkdir "${tomcathome}/webapps/${project.name}"
        copy {
            from zipTree(war.outputs.files.singleFile)
            into "${tomcathome}/webapps/${project.name}"

Friday, February 3, 2017

Solution for missing /vagrant file system.

If you are messing around with Vagrant and VirtualBox, you will sometimes end up with a vm where the /vagrant shared file system does not mount properly.  Vagrant complains that the vboxsf kernel module is not loaded properly and gives you a mount command to run manually.  When you try to mount it manually with
sudo mount -t vboxfs -o uid=500,gid=500 vagrant /vagrant
You get this error:
/sbin/mount.vboxsf: mounting failed with the error: No such device
  According to this page, you have to reinstall the VBox guest additions with this command:
cd /opt/VBoxGuestAdditions-*/init
sudo ./vboxadd setup
However, for a generic box, it fails because gcc and the kernel sources are not installed.  So when you run into this problem on a Centos 6 box (specifically, a vm based on the bento/centos-6.8 vagrant box), here is the exact sequence of commands that fixes the problem:
cd /opt/VBoxGuestAdditions-*/init
sudo yum install gcc make kernel-devel
sudo ./vboxadd setup
Then rerun the special mount command to mount the /vagrant file system.

Thursday, September 1, 2016

Changing Custom Field Types in Redmine

At Brick Street, we use Redmine to manage engineering and product management. We host it ourselves on an AWS instance because we're cheap and because we want to integrate it with our other line-of-business systems. We recently upgraded from Redmine 2.1 to 3.3. In Redmine 2.1, we had some custom fields on Issue records that held text to include in our release notes. (We have a program that scans subversion logs for Issues, and then generates release notes by pulling the Redmine text for the issues fixed in the branch.)

In Redmine 2.1, these custom fields are 'text' type, which shows up in the Redmine UI as a single line (an HTML input element). In Redmine 3.3, they added a 'Long Text' type which shows up in the UI as a resizable multiline control (an HTML textarea element).

We wanted to convert the 'text' custom fields to 'long text'. So I played with our preproduction instance and reverse engineered the schema: The custom field definitions are in the CUSTOM_FIELDS table:

MySQL> desc custom_fields;
| Field           | Type         | Null | Key | Default | Extra          |
| id              | int(11)      | NO   | PRI | NULL    | auto_increment |
| type            | varchar(30)  | NO   |     |         |                |
| name            | varchar(30)  | NO   |     |         |                |
| field_format    | varchar(30)  | NO   |     |         |                |
| possible_values | mediumtext   | YES  |     | NULL    |                |
| regexp          | varchar(255) | YES  |     |         |                |
| min_length      | int(11)      | YES  |     | NULL    |                |
| max_length      | int(11)      | YES  |     | NULL    |                |
| is_required     | tinyint(1)   | NO   |     | 0       |                |
| is_for_all      | tinyint(1)   | NO   |     | 0       |                |
| is_filter       | tinyint(1)   | NO   |     | 0       |                |
| position        | int(11)      | YES  |     | NULL    |                |
| searchable      | tinyint(1)   | YES  |     | 0       |                |
| default_value   | mediumtext   | YES  |     | NULL    |                |
| editable        | tinyint(1)   | YES  |     | 1       |                |
| visible         | tinyint(1)   | NO   |     | 1       |                |
| multiple        | tinyint(1)   | YES  |     | 0       |                |
| format_store    | text         | YES  |     | NULL    |                |
| description     | text         | YES  |     | NULL    |                |
19 rows in set (0.00 sec)

The field_format value for text fields is 'string'. The field_format value for long text fields is 'text'.

The actual values of the custom fields are stored in the CUSTOM_VALUES table:

MySQL>; desc custom_values;
| Field           | Type        | Null | Key | Default | Extra          |
| id              | int(11)     | NO   | PRI | NULL    | auto_increment |
| customized_type | varchar(30) | NO   | MUL |         |                |
| customized_id   | int(11)     | NO   |     | 0       |                |
| custom_field_id | int(11)     | NO   | MUL | 0       |                |
| value           | mediumtext  | YES  |     | NULL    |                |
5 rows in set (0.01 sec)

The customized_id field is a foreign key to the record in the ISSUES table. The custom_field_id field is a foreign key to the CUSTOM_FIELDS record. The value field stores the custom field value. This query will show you all the custom field values for Issue 2001:

select * from custom_values where customized_id = 2001;

So it turns out that you can change the type of a custom field value by updating the custom_field_id field of the CUSTOM_VALUES record. In our Redmine, we changed the text fields to long text by following this procedure:
  1. Define a new long text field in the UI.
  2. Look in the CUSTOM_FIELDS table to get the id of the text field (e.g. id 5) and the new long text field (e.g. id 15).
  3. Run this update query to change types of all the custom field values:
    update CUSTOM_VALUES set custom_field_id = 15 where custom_field_id = 5;

  4. Delete the custom field definition of the old field (id 5) in the UI.
This approach works great for Text custom fields. List custom fields are harder because the custom field definition includes a list of possible values (in the CUSTOM_FIELDS.possible_values). We have a number of multiselect fields that contain lists of release version. My next task is to convert these to the new Version custom field type.

Monday, June 24, 2013

Why @OntarioTCU should investigate @BitmakerLabs

BitmakerLabs is a private training school that does a 9 week, $9000 intensive course in Ruby on Rails and related technologies. In Ontario, so-called Private Career Colleges are regulated by the Ontario Ministry of Training, Colleges, and Universities (MTCU) under the Private Career Colleges Act, 2005. This law is basically a consumer protection law that says the MTCU must review and approve Private Career Colleges before they can operate in Ontario. Today, Bitmaker announced they are closing (temporarily) in response to an investigation from the MTCU, and that they will be registering as a Private Career College in order to comply with the 2005 law.

Almost everyone in Toronto with a twitter account seems to be enraged by this blatant attempt to enforce consumer protection laws. But consider this:

BitmakerLabs claims their 9 week program will turn a non-programmer into a web developer. As someone who writes checks to companies who might hire BitmakerLabs' graduates, I am skeptical, to say the least. Apart from my skepticism, however, I wish BitmakerLabs well; if they are doing good work, they should have no problem registering with MTCU. But it's kind of silly to blame MTCU for doing their job.

Furthermore, compare this program with the Humber College Web Development Program, which is full time for three semesters for $8662 and no HST. Or the Udacity / Georgia Tech / AT&T online Computer Science Masters Degree for $6k which promises to offer a fully accredited masters degree. Your money may be better spent on longer programs that provide more time to internalize the skill set.

The background here is that the traditional model of higher education (low volumes at extremely high costs) is ripe for disruption. So we are going to be seeing a lot of interesting experiments in the next few years; experiments like BitmakerLabs, MOOCs, and a general disaggregation of the teaching, evaluation, and certification functions that have traditionally been provided by colleges and universities. What this means for consumers is that they will have to be a lot more careful until this disruption sorts itself out.

  1. Interesting background info on rationale and prior enforcement of the Private Career Colleges Act, 2005: Ontario Ombudsman Report.
  2. The United States has similar laws; e.g. NH Regs for Career Schools

Thursday, March 28, 2013

Friday, August 10, 2012

Influitive and Investing in Toronto Startups

Congratulations to Influitive for closing their $3.75 million round. I joined the Influitive board over a year ago during a convertible debt round; it's been the easiest board seat I've ever held since Mark Organ is such a great CEO. He does all the work and I come to the board meetings and eat sandwiches.

It was also my first investment in a Canadian startup, which has been an interesting experience. If you are an American reader, you may not be aware that Canada is this foreign country on our northern border and, as a foreign country, has a separate currency, banking system, tax regime, and media establishment. Toronto, in particular, is like the New York, Los Angeles, and San Francisco of the country since it is the center for the Canadian financial, media, and technology sectors. So there is a lot going on here, maybe more than in jolly old New England.

Toronto, and Canada in general, is actually a good place to start a company or for established companies to locate R&D and customer-facing business functions. NAFTA has eliminated most cross-border issues and I've found that the talent pool is great, they're in the same time zone, and labor costs are lower than in the US, primarily because employers don't pay for health care. The Canadian government also has a number of refundable R&D tax credits, and Canadian-controlled startups can usually leverage this to get an extra few months of cash at a critical time.

Sunday, June 6, 2010

Boston Angel Bootcamp

Boston Angel Bootcamp was well worth the time to attend.

Sim Simeonov presented an analysis of angel investment outcomes.  The net is that most angels lose money but a small number do very well.  The difference between losers and winners seems to be that winners make a lot of investments; ie portfolio theory applies to angel investments too.  In addition, he recommended that in order to improve deal flow, an angel should get a reputation for making fast decisions and should be able to drag additional investors along for the ride.  I'm sure I'm misrepresenting a lot this, so I'll be waiting for Sim's blog post where he explains it in more detail.

There was also an interesting discussion of the problems with convertible notes.  John Landry of Lead Dog Ventures said that there is a chance that followon investors would not honor the conversion discounts (which prompted audience comment that it was the entrepreneur's responsibility to protect current investors in subsequent rounds) and that the typical 20% conversion discount is inadequate compensation if the note is held for more than a year with no conversion.  John says angels should do the work to put a valuation on the company and should take founders shares.  John also advocates taking a board seat and being very active with one's investments.

Recorded video of the sessions:  http://www.ustream.tv/channel/angelbootcamp

Slides for additional presentations from the #angelbootcamp twitter stream:

David Cancel - http://talks.davidcancel.com/slideshare/
Alexis Ohanian - http://www.slideshare.net/kn0thing/alexis-ohanian-at-angel-bootcamp-boston-june-01-2010

Also: Good summary at xconomy: