Using “C# Interactive” aka CSI/CSX for ETL

motivation

interactive C# offers typical REPL benefits ala powershell without the mental context switch required to leave our beloved C# syntax =)
 

notable

  • great MSDN reference article
  • CSX syntax can be executed from either Visual Studio 2015 (as of update 1) > View > Other Windows > C# Interactive
  • -or- C:\Program Files (x86)\MSBuild\14.0\bin\csi.exe
    • then #load file.csx

    Extract

    this sample was based on a low fidelity web page as the raw data source and shows the low overhead convenience of more recent System.Net.WebClient vs previous WebRequest/WebResponse approach.

    Transform

    trivially demonstrative in this case, simply a string.Split call 🙂

    Load

    demonstrates leveraging SQL Server’s Table-Valued-Parameter facility to bulk upload multiple rows at once which are then conveniently manifested as a standard sql rowset for typical DML inside the receiving stored proc.
     

    SQL definition

    CREATE TYPE dbo.SpreaderData_UDT AS TABLE
    (
        [SpreaderDataID] [INT] PRIMARY KEY,
        [SpreaderID] [INT],
        [Speed] [INT],
        [Density] [INT],
        [SpreadQty] [INT],
        [Setting] [VARCHAR](100)
    )
    GO
    
    -- crucial - SQL Server yields a unintuitive error message when this has not been done
    GRANT EXECUTE ON TYPE::dbo.SpreaderData_UDT TO PUBLIC
    GO
    
    CREATE PROCEDURE [dbo].SpreaderData_Table_u
    @SpreaderData dbo.SpreaderData_UDT READONLY -- <= ***** crucial
    AS BEGIN
    
    UPDATE sd SET
      sd.Setting = sd2.Setting
    FROM dbo.SpreaderData sd
    JOIN @SpreaderData sd2 ON sd2.SpreaderDataID = sd.SpreaderDataID
    
    END
    GO
    

    ... READ MORE ...

GreaseMonkey hacking Gmail

motivation

i wanted to see if i could get category bundling working in gmail ala outlook… it’s always been a nice mental flow for me to carve out pending events from littering the “inbox zero” but still see them right there in front so i don’t forget to check up on them vs hidden a click away under a “folder”
 

update – don’t miss the “labs” functionality for doing exactly this kind of category bundling
 

success!

the integration with gmail’s normal behavior isn’t perfect so this is still very experimental stages but it’s a pretty satisfying quick hack…
 

notes

  • i followed a this tutorial for getting the gmail API cooking including OAUTH2…
  • the greasemonkey end of the code has some helper functions to get all the usual libraries loaded up like bootstrap, font-awesome and knockout… currently i’m only leveraging jQuery and Lobibox (love those sexy growls!)
  • tweak the code to call gapi.client.gmail.users.labels.list vs messages to see your label Id’s… they’re not the names we see in the gmail UI

roadmap

obvious next nice to haves rush to mind:

  • bundle by multiple specified categories (aka labels)
  • clicking on message in new section actually does navigate to message since that was an easy gimme but it’s not as slick in the vertical split mode… i tried tracing the code that dynamically populates the side panel with the message body and it’s just to abstract, so it’ll have to be a matter of popping that in myself, but shouldn’t be too tough since we’re already loading the whole message body behind the scenes.
  • flip to real ECMAScript 2016 module loader or whatever’s clever right now… without a build engine to make things happen for script.js it seems like we’re still waiting for a fully native solution… i could do require.js approach but that seems to be on the outs already???

example screenshot

image
 

GmailMonkey.user.js

// ==UserScript==
// @name         GmailMonkey
// @namespace    http://Next-Technologies.com/
// @version      0.1
// @description  try to take over the world!
// @author       Brent Anderson
// @match        https://mail.google.com/mail/u/0/*
// @grant        none
// @run-at        document-end
// ==/UserScript==


//gmail api stuff//////////////////////////////////////////////////////////////////////////

function objArrayGetByProperty(array, propertyName, value) {
  var result = $.grep(array, function(e){ return e[propertyName] === value; });
  return result.length ? result[0].value : null;
}

function loadMessages() {
  var request = gapi.client.gmail.users.messages.list({
    "userId": "me", //nugget: special userId of me to indicate the currently authenticated user
    "labelIds": "Label_59",
    "maxResults": 200
  });

  request.execute(function(response) {
    if (response.error) {
      Lobibox.notify("error", { size: "mini", delay: false, msg: "[gapi.client.gmail.users.messages.list] " + response.error.message });
      return;
    }

    $.each(response.messages, function() {
      var messageRequest = gapi.client.gmail.users.messages.get({
        "userId": "me",
        "id": this.id
      });

      messageRequest.execute(function (message) {
        //debugger;
        var from = objArrayGetByProperty(message.payload.headers, "name", "From").replace(/"/g,"");
        var subj = objArrayGetByProperty(message.payload.headers, "name", "Subject");
        var date = formatDate(new Date(objArrayGetByProperty(message.payload.headers, "name", "Date")), "MMM DD");

        var onclick = "window.location.href += '/"+message.id+"'";

        mainTable.append(verticalSplit ? '\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" rowspan="3"></td>\
          <td class="yX xY apy">'+from+'</td>\
          <td class="yf xY apt">'+date+'</td>\
          <td class="xY" rowspan="3"></td>\
          </tr>\
          <tr class="zA yO apv" onclick="'+onclick+'">\
          <td colspan="2" class="xY apD" style="font-weight: bold">'+subj+'</td>\
          </tr>\
          <tr class="zA yO apv apw" onclick="'+onclick+'">\
          <td colspan="2" class="xY apA apB y2">'+message.snippet+'</td>\
          </tr>\
          ': '\
          <tr class="zA yO" onclick="window.location.href += \'/'+message.id+'\'">\
          <td colspan="4"></td>\
          <td class="xY">'+from+'</td>\
          <td colspan="2" class="xY" style="overflow: hidden">'+subj+'</td>\
          <td class="xW xY">'+date+'</td>\
          </tr>'
        );
      });
    });
  });
}

//from here: https://www.sitepoint.com/mastering-your-inbox-with-gmail-javascript-api/
function handleAuthClick() {
  gapi.auth.authorize({
    client_id: clientId,
    scope: scopes,
    immediate: false
  }, function (authResult) {
    if(authResult && !authResult.error) {
      gapi.client.load("gmail", "v1", loadMessages); // <<<<<<<<<<<<<<<<<<<<<<<
      /*$("#authorize-button").remove();
      $(".table-inbox").removeClass("hidden");
    } else {
      $("#authorize-button").removeClass("hidden");
      $("#authorize-button").on("click", handleAuthClick);
      */
    }
  });
  return false;
}

function nextTechInit() {
  waitForIt(":2", function(found) {
    $("html, body").css("font-size", "inherit"); //override bootstrap's annoying default

    $(".vh").remove();

    Lobibox.notify.DEFAULTS.soundPath = "//cdn.rawgit.com/arboshiki/lobibox/373d1af467930db68c876e76408bd953198c428e/dist/sounds/";
    Lobibox.notify.DEFAULTS.delay = 3000;
    //Lobibox.notify("error", { size: "mini", delay: false, msg: "lobibox test" });

    mainTable = $(".Cp > div > table");
    verticalSplit = mainTable.find("colgroup > col").length === 5;

    mainTable = $(mainTable).find("tbody");
    mainTable.append(
      '<tr><td colspan="'+(verticalSplit?5:8)+'"><div style="margin-top: 1em" class="">Pending</div></td></tr>');

    gapi.client.setApiKey(apiKey);
    handleAuthClick();
  });
}

function formatDate(date, format) {
  var monthNames = [
    "January", "February", "March",
    "April", "May", "June", "July",
    "August", "September", "October",
    "November", "December"
  ];

  var day = date.getDate();
  var monthIndex = date.getMonth();
  var year = date.getFullYear();

  return format.replace("MMM", monthNames[monthIndex].slice(0,3)).replace("DD", day);
}

//////////////////////////////////////////////////////////////////////////////////

function waitForIt(elementId, then) {
  //console.log("waitForIt: " +elementId);
  var found = document.getElementById(":2");
  if ( !found ) {
    //console.log("not found!"); 
    setTimeout(function() {waitForIt(elementId, then);}, 500);
    return;
  }
  then(found);
}

function loader(refsArray) {
  var element;
  for(var i = 0; i < refsArray.length; i++) {
    url = refsArray[i].toString();

    if (url.indexOf(".js") !== -1 || url.slice(0,8) === "function") {
      element = document.createElement("script");
      if (url.slice(0,8) === "function") { 
        element.innerHTML = url;
        document.getElementsByTagName("head")[0].appendChild(element);
        continue; //nugget! inline script doesn't fire an onload event
      }
      else element.src = url;
      if (i < refsArray.length-1) { //if we're not on the last element already, recurse on the remaining items
        var remaining = refsArray.slice(i+1);
        element.onload = function() {
          loader(remaining);
        };
      }
      document.getElementsByTagName("head")[0].appendChild(element);
      break;
    }

    else if (url.indexOf(".css") !== -1) {
      element = document.createElement("link");
      element.rel = "stylesheet";
      element.type = "text/css";
      element.href = url;
      document.getElementsByTagName("head")[0].appendChild(element);
    }

    else throw("unexpected reference extension, expecting .css or .js or a function, but got: "+url);

  }
}

//by simple convention, list all CSS first, then JS...
//each JS will subsequently load the next as a simple dependency mechanism so specify JS's in appropriate order
//further, including a function(s) will inline <script> it
loader([
  //"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap-theme.min.css",
  //"//cdnjs.cloudflare.com/ajax/libs/twitter-bootstrap/3.3.7/css/bootstrap.min.css",
  //"//cdnjs.cloudflare.com/ajax/libs/font-awesome/4.5.0/css/font-awesome.min.css",
  "//cdn.rawgit.com/arboshiki/lobibox/master/dist/css/lobibox.min.css",
  "//cdnjs.cloudflare.com/ajax/libs/jquery/3.1.0/jquery.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/jquery-validate/1.15.1/jquery.validate.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout/3.4.0/knockout-min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout.mapping/2.4.1/knockout.mapping.min.js",
  //"//cdnjs.cloudflare.com/ajax/libs/knockout-validation/2.0.3/knockout.validation.min.js",
  "//cdn.rawgit.com/arboshiki/lobibox/master/dist/js/lobibox.min.js",
  objArrayGetByProperty.toString()+"\r\n"+
  'var mainTable;\r\n'+
  'var verticalSplit = false;\r\n'+
  'var clientId = "xxxxxxxxxxxxx.apps.googleusercontent.com";\r\n'+
  'var apiKey = "xxxxx";\r\n'+
  'var scopes = "https://www.googleapis.com/auth/gmail.readonly";\r\n\r\n'+
  loadMessages.toString()+"\r\n"+
  handleAuthClick.toString()+"\r\n"+
  waitForIt.toString()+"\r\n"+
  nextTechInit.toString()+"\r\n"+
  formatDate.toString()+"\r\n",
  "//apis.google.com/js/client.js?onload=nextTechInit"
]);

... READ MORE ...

Script to generate dozens of required iOS app icon images

Motivation

there are a number of free icon generating web sites available which begs the question whether a script like this provides any value… but leveraging a robust image manipulation utility like imageMagick to apply unique aesthetics makes a DIY approach like this more compelling… along those lines, in the current script, i’m applying:

  • custom “centering” – for the splash images where the icon sits inside of a larger background, i’ve found positioning the icon 2/7ths of the way down from top to be the most pleasing
  • outer glow
     

Result

image
 

!generate.cmd

@echo off

::keeps all variable definitions local to batch file so no need to cleanup after
setlocal

::*** make sure to start with 1024x1024 original ***
set inFile=!original.png
if not [%1] EQU [] set inFile=%1

rename "%inFile%" "%inFile%.save"
erase Default*.png Icon.ico Icon*.png Icon.ico Splash*.png Itunes*.png
rename "%inFile%.save" "%inFile%"

:: this was a handy page that did most BUT NOT ALL of what i saw in the "Resources" folder generated from the Xamarin project template...
:: http://icon.angrymarmot.org/index.html#c9a51e815eac3df5c54f

:: so i decided to have a crack at generating the remaining "Default" aka splash screen images since they represented an interesting challenge of:
:: 1) resizing onto a larger background
:: 2) applying "outer glow" to mitigate the blue-on-blue...
::    albeit, i could've chosen a more simpatico background color, but i favor sticking with defaults until something really kicks me in a different direction

::***install fantastically handy ImageMagick tool from => http://www.imagemagick.org/script/binary-releases.php
::main options docs: http://www.imagemagick.org/script/command-line-options.php#compose

::clarify options used:
::-channel portion yields more desireable hard edge on shadow, see: http://www.imagemagick.org/Usage/blur/#shadow_outline
::+swap is necessary because we need the input image to initially come first in order to render it's corresponding shadow but then we want the shadow behind... and the + sign is handy shortcut to swap the last 2 layers in pipeline w/o having to explicitly specify
::-gravity North centers horizontally and at top vertically and then the -extent sets the outer "canvas" size with the negative offset bringing the icon down from the top
::the double %% on -level is required to escape the normal processing of "%" as a batch variable

@echo on
goto pause

magick "%inFile%" -define icon:auto-resize="256,128,96,64,48,32,16" ( +clone -shadow 100x80-0-0 -channel A -level 0,50%% +channel ) +swap -background none -layers merge -gravity center "Icon.ico"

:Icons
call :IconSub 29 Icon-Small
call :IconSub 40 Icon-Small-40
call :IconSub 50 Icon-Small-50
call :IconSub 57 Icon
call :IconSub 58 Icon-Small@2x
call :IconSub 60 Icon-60 ::including this one only because it's referenced in LaunchScreen.storyboard, even though i'm not using LaunchScreen because i want the app to Splash with the big background style image
call :IconSub 72 Icon-72
call :IconSub 76 Icon-76
call :IconSub 80 Icon-Small-40@2x
call :IconSub 100 Icon-Small-50@2x
call :IconSub 114 Icon@2x
call :IconSub 120 Icon-60@2x
call :IconSub 144 Icon-72@2x
call :IconSub 152 Icon-76@2x
call :IconSub 512 iTunesArtwork
call :IconSub 1024 iTunesArtwork@2x

:Splashes
call :SplashSub 320 480 Default
call :SplashSub 640 960 Default@2x
call :SplashSub 640 1136 Default
call :SplashSub 768 1004 Default-Portrait
call :SplashSub 1536 2008 Default-Portrait@2x
call :SplashSub 1024 748 Default-Landscape
call :SplashSub 2048 1496 Default-Landscape

@echo off
echo  ____                                               _   _ 
echo / ___^|   _   _    ___    ___    ___   ___   ___    ^| ^| ^| ^|
echo \___ \  ^| ^| ^| ^|  / __^|  / __^|  / _ \ / __^| / __^|   ^| ^| ^| ^|
echo  ___) ^| ^| ^|_^| ^| ^| (__  ^| (__  ^|  __/ \__ \ \__ \   ^|_^| ^|_^|
echo ^|____/   \__,_^|  \___^|  \___^|  \___^| ^|___/ ^|___/   (_) (_)

@ping localhost -n 10 >nul:
exit

:IconSub
@echo off
:: %1 = desired size
:: %2 = file name
SETLOCAL
set /a shadowSize=%1*8/100
if %shadowSize% EQU 0 set shadowSize=1
magick "%inFile%" -resize %1 ( +clone -shadow 100x%shadowSize%-0-0 -channel A -level 0,50%% +channel ) +swap -background none -layers merge -gravity center -resize %1 %2.png
@echo on
@goto :EOF


:SplashSub
@echo off
:: %1 = width
:: %2 = height
:: %3 = file name
SETLOCAL
set smallestDimension=%2
if %1 LSS %2 set smallestDimension=%1
::the shadow creates a natural "padding" effect, so start out with icon same as smallest dimension
set /a iconSize=%smallestDimension%
set /a shadowSize=%smallestDimension%*8/100
::center the icon 1/3 of the whitespace available down from top
set /a verticalOffset=(%2-%iconSize%)*2/7

::default Xamarin project blue background #3498DB
:: handy script to generate following commands if you already have a folder with the desired resulting images:
:: >magick identify -format "magick \"\%inFile\%\" -resize %w %f\n" Icon*.png iTunes*.png
magick "%inFile%" -resize %iconSize% ( +clone -shadow 100x%shadowSize%-0-0 -channel A -level 0,50%% +channel ) +swap -background "#3498DB" -layers merge -resize %iconSize% -gravity North -extent %1x%2+0-%verticalOffset% %3.png

@echo on
@goto :EOF

... READ MORE ...

[Solved] Greasemonkey/Tampermonkey jQuery sideload and setInterval

i was having a heck of a time keeping a reliable handle on jQuery in the Pandora page… it would be there upon initial Greasemonkey script fire but then upon subsequent setInterval executions, jQuery was nowhere to be found… facinating…
 

notable: as i was debugging, i started to see that Chrome was cycling through four ( 4 ! ) different VMxxxx “copies” of the greasemonkey script upon each setInterval execution… questions like why? and why 4? abound if anyone cares to enlighten me
 

so it struck me that i just need to make sure jQuery is available in each one of those “sessions”…
 

noteable: the “sideload” is accomplished via jQuery’s native “noConflict” facility… this post explains how it works… the gist is that each load of jQuery does indeed replace “$” BUT it also saves the previous into _$, such that $.noConflict can restore “$” to the previous version… this is what allows Pandora’s copy of jQuery to remain as-is… crucial in this case because Pandora depends on additional add-ons that it loads as expando properties on its instance of jQuery.
 

after that was in the bag, i couldn’t help dwelling on what else might be possible and had another aha moment… from tracing the pandora js execution i learned that there were pretty obvious variables getting set for allowed features (e.g. “allowSkipTrackWithoutLimit”)… i’d banged my head pretty deap trying to replace the main pandora.js script with one where those values were tweaked… blocking the original via AdBlockPlus was easy as well as loading the tweaked pandora.js inline <script> but that approach ran aground on not being able to precisely sequence the alternative script in the same order with it’s dependencies… Chrome doesn’t implement the crucial window.beforescriptexecute event which would probably make this feasible… the main pandora.js is wrappered in a self contained function call so we can’t monkey patch its innards… but then it struck me, jQuery is global… and what if they’re getting these values via jQuery.ajax… such that i could override and tweak… sure enough, that approach panned all the way out!
 

update – after that last round, i realized the whole thing about sideloading jQuery was unecessary, i just needed to use the inline script approach to make sure my code executed on the page context vs whatever weird context TamperMonkey normally does… so the following script now reflects the cleaner evolved approach

// ==UserScript==
// @name          Pandora - "still listening" click
// @author        Brent Anderson
// @homepage      http://www.beejblog.com/2016/08/solved-greasemonkey-jquery-sideload-and-setinterval.html
// @match         http://www.pandora.com/*
// @grants        none
// @run-at        document-end
// ==/UserScript==

function recurringTweaks() {
  //this click, remove, click sequence skips embedded video ads and gets the tunes playing again
  var stillListeningButton = $("#still_listening_ignore");
  if (stillListeningButton.is(":visible")) {
    stillListeningButton.click();
    $("#videoPlayerContainer").remove();
    stillListeningButton.click();
    $(".playButton").click();
    //above brute force video ad skip leaves player controls disabled, this resolves that side effect
    $(".disabled").removeClass("disabled");
  }

  var adContainer = $("#ad_container");
  if (adContainer.length) {
    //remove right side ad section...
    $("#ad_container").remove();
    //and allow the album covers area to fill the space
    $(".contentContainer").css("width", "100%");
    $("#adLayout").css("width", "80%");

    //remove some other "upgrade" bits
    $(".registeredUser").remove();
    $("#rightColumnDivider").remove();
    $(".audioAdInfo").remove();
  }
}


// monkey patch jQuery.ajax so we can override some nice stuff =)
var hijax = function() {
  if (typeof $ !== 'undefined') {
    var oldAjax = $.ajax;
    var newAjax = function(a, b) {
      var oldSuccess = a.success;
      a.success = function(data, textStatus, jqHXR) {

        // infinite skip! =)
        $(data).find('name:contains(allowSkipTrackWithoutLimit) + value > boolean').replaceWith('<boolean>1</boolean>');

        //auto skip ads
        if (a.url.indexOf("method=registerImpression") !== -1) {
          $(".skipButton a").click();
        }

        //debug: console.log('url: ' + a.url + ', data: '+(''+data === '[object XMLDocument]' ? data.children[0].innerHTML : data));
        oldSuccess(data, textStatus, jqHXR);
      };
      oldAjax(a, b);
    };
    $.ajax = newAjax;

    setInterval(recurringTweaks, 2000);

  }

};

// load <script> inline to the page so it has access to jQuery "$" global vs TamperMonkey's alternative context
if (!document.getElementById("hijax")) {
  var hijaxScript = document.createElement("script");
  hijaxScript.setAttribute("id", "hijax");
  hijaxScript.innerHTML = recurringTweaks.toString() + "\r\n" + hijax.toString().replace(/^function.*{|}$/g, "");
  document.head.appendChild(hijaxScript);
}



///////////////////////////////////////////////////////////////////////////////////////////////////////
//sorry, turning this post into a catch all for stuff that might come in handy elsewhere
/*

//the original jquery "sideload" code
function loadJq() {
    if (!window.jq) {
        script = document.createElement("script");
        script.src = "http://cdnjs.cloudflare.com/ajax/libs/jquery/1.11.1/jquery.min.js";
        script.onload = function() { window.jq = $.noConflict(true); cosmetics(); };
        document.getElementsByTagName("head")[0].appendChild(script);
    }
    else cosmetics();
}

    //helpful: http://userscripts-mirror.org/scripts/show/125936

  window.addEventListener('beforescriptexecute', function(e) {
      if (e.target.src.indexOf("pandora.js") != -1) {
          e.preventDefault();
          //e.stopPropagation(); //??
          e.target.src = ''; //??
          e.target.innerHTML = "patched script";
      }
  }, true);

var a = document.getElementsByTagName("script");
for each (var e in a) {
  if (!e) continue; // oddly, this does sometimes grab null elements.
  var b = e.getAttribute("src");
  if (b && b.indexOf("pandora.js") != -1) {
    e.parentNode.removeChild(e);
    debugger;
    break;
  }
}
*/

// @grants         GM_xmlhttpRequest
/*GM_xmlhttpRequest({
  method: "GET",
  url: "http://rawgit.com/Beej126/567a36f2dd1e3ce613ad8ec5846a40d4/raw/fac20b4ab17681b5da41b07c2549676ff3571fc9/dorPanda.js", //"http://www.pandora.com/pandora.js?v=440211416",
  onload: function(response) {
    debugger;

    //here's the beef!
    //var tweaked = response.responseText.replace("this.PC=b.allowSkipTrackWithoutLimit", "this.PC = true;");
    //$("script[src*='/pandora.js'").af

    var tweaked = response.responseText;
    document.head.appendChild(document.createElement('script')).innerHTML = tweaked;
  }
});*

*/

... READ MORE ...

[SOLVED] Bare CNAME with MX record

just thought i’d throw this out into the interwebz since it seems to be such a well known no-can-do that is actually a very handy can-do (where applicable)…
 

Background

DNS admin warnings advise against doing a “bare” (no prefix) CNAME along with an MX record, example

 

Disclaimer

What i’m successfully demonstrating here is clearly non-standard according to the specs
however, i’ve proven it does work for SOME servers implementation of the standards (including major provider Office 365 on the MX side) so it’s worth trying with your servers if this provides a convenient solution for your needs… and it will be immediately verifiably working or not; no “sometimes” ambiguity to worry about.
 

... READ MORE ...

ESXi = Win+Mac Xamarin iOS dev nirvana

Overview

  • i was looking for a single machine Mac + Win solution… working from one primary desktop and remoting to the other…
  • this is of course how VMware Workstation product sits naturally but that arrangement gave me heartburn (see Motivation)
  • ESXi requires some relatively esoteric configurations of HDD, Video & USB to yield the same single machine convenience, hence these notes

Motivation:

Hosting Mac VM side under Windows VMware Workstation i ran into very unreliable connection from Visual Studio 2015 to Xamarin’s Mac Build Agent (believe me, tried all latest VS2015 update bits as well as Xamarin alpha channel)… only after MANY MANY frustrating retries would it eventually connect as well as surprising Xamarin Studio NuGet package gallery connectivity roadblock with virtual Mac’s network interface in NAT mode which seemed to be the only way Build Agent would ever connect… NuGet worked under Bridged but then Visual Studio couldn’t connect… arrrrg going the other way with Windows virtualized under Mac host via either Parallels or Fusion always took an unacceptable hit on Windows / Visual Studio performance… year after year both mainstream commercial Mac hosted virtualization products have been riddled with issues and chronically dead ended support forum posts

... READ MORE ...

PowerShell WinForms interactive Prototyping/Debugging

Credits:

Motivation

  • PowerShell’s interactive nature lends it to be a handy way to experiment with .Net objects… including UI elements like WinForms/WPF…
  • Yet firing up UI’s classically take over the active thread to service the user interaction (e.g. mouse events etc)…
  • Without the extra gravy below, our otherwise handy interactive powershell locks up tight until we close down the Windows Forms application thus releasing the main thread back to the command line
  • The following gravy throws a Windows Form onto a background thread such that we can continue to manipulate the UI objects WHILE THEY’RE RUNNING, yay!

The Gravy

create RunSpaceWinForm.ps1 as such

function RunSpaceWinForm {
    param($frm)
    if (!$frm) {return}

    #RunSpace is a PowerShell thread
    [System.Management.Automation.Runspaces.Runspace]$rs = [Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace()
    $rs.ApartmentState = "STA"
    $rs.ThreadOptions = "ReuseThread"
    $rs.Open()

    # make the WinForm object instantiated in current scope also available inside the runspace
    $rs.SessionStateProxy.SetVariable("frm", $frm)

    $ps = [System.Management.Automation.PowerShell]::Create()
    $ps.Runspace = $rs
    $rs.SessionStateProxy.SetVariable("ps", $ps)

    [System.IAsyncResult]$invokeHandle = $null
    $rs.SessionStateProxy.SetVariable("invokeHandle", $invokeHandle)

    # nugget: here's basically where the magic sauce kicks in
    # create the script that will run on the background thread, this lets WinForm have it's WndProc message pump while freeing our current PowerShell thread to further manipulate WinForm objects
    $ps.AddScript({
      #this call will take over the thread until the application is shut down by closing the main form
      [System.Windows.Forms.Application]::Run($frm)

      # clean up the powershell thread objects
      $ps.Runspace.Close()

      #honestly not sure if these commands work and are beneficial
      $ps.EndInvoke($invokeHandle)
      $ps.Runspace.Dispose() #this will block the runspace state on "closing" until you close the interactive powershell window
      $ps.Dispose()
    }) | Out-Null
    $invokeHandle = $ps.BeginInvoke()
    return "Use Debug-Runspace -id $($rs.Id) to activate breakpoints"
}

... READ MORE ...

Migrate from Delicious to Diigo

Delicious set the standard but they’ve been a bit of a bumpy ride lately with reliability… according to their blog blog they recently moved back to old code base and Del.icio.us domain (ah memories :)… and then just as of today just got their settings page operational such that I could successfully export my bookmarks and hop to another free link lily pad… the export yields a simple html file full of links.
 

I’ve initially setted on Diigo… it’s pretty slick… there’s a nicely robust Chrome plugin.
 

Below is a little jQuery i threw together to truncate my Delicious links at a certain cutoff date so I’m not loading a bunch of old junk.
Edit the exported html file and add jquery in the head like so and F5 refresh the page.

<head>
  <script type="text/javascript" src="http://cdnjs.cloudflare.com/ajax/libs/jquery/2.2.3/jquery.min.js"></script>
</head>

then run this JS straight from the F12 developer tools console in Chrome, or whatever you prefer

// this approach works on "bookmark file" format. e.g. what Delicious.com exports
// basically just an html file full of <a> tags wrapped with <dt>'s inside one big <dl>
// key attributes: ADD_DATE="1461441710" TAGS="BestStuffEver"
// the date format is in seconds... to make it JS Date compatible just multiply value by 1000 (i.e. milliseconds)

// find the "add_date" of the oldest entry you want to keep
var cutoffDate = new Date(1345307846000)

// this then deletes all the links older than cutoffDate
$("a").each(function(idx, el) {
  if( new Date($(el).attr("add_date")*1000) < cutoffDate ) { var par = $(el).parent(); par.next("dd").remove(); par.remove(); }
});

// then just save-as that page

// and import to Diigo :)
// https://www.diigo.com/tools/import_all

... READ MORE ...

SQL Server PDO PHP7

  1. get the DLL – grab latest x64.zip
  2. add to your php.ini extension list:
    [ExtensionList]
    extension=php_pdo_sqlsrv_7_nts.dll
    
  3. here’s sample call code:
    <?php
    
    try {
         $conn = new PDO( "sqlsrv:Server= ip_address; Database = mydb ", $user, $pwd);
         $conn->setAttribute( PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION );
    }
    catch(Exception $e){
         die(var_dump($e));
    }
    
    $stmt = $conn->query($qry);
    $result = $stmt->fetchAll();
    $row = $result[0];
    $colval = $row["fieldname"];
    ?>
    

... READ MORE ...

PHP7 Debugging under Visual Studio 2015

I suggest installing PHP7 through the IIS Web Platform Installer so it does the Handler Mapping vs having to mess with that manually (i.e. assigning PHP extension to php-cgi.exe) – but there’s a lot of guides out there for doing that yourself if you want. That’ll probably leave you with a slightly older version so then just go install latest PHP7 bits over the top – we want the non-thread-safe (NTS) builds when running under IIS FastCGI (supposedly the most performant approach) Ideally you don’t mind springing for PHP Tools for Visual Studio… this package provides comforts like Intellisense and PHP project templates… and I also noticed that PHP Tools automatically configures XDebug debugging bits for us which is nice vs figuring that out manually (at least the first time)… turns out its just some php.ini settings (see below). so basically you just launch site in Visual Studio debug (F5) and PHP Tools will ask you if you want it to configure debugging for you… the brief wrinkle here is that it then went off and installed/configured PHP5.x… hence the main reason I’m posting this – to affirm we can then indeed just go copy the pertinent settings from the PHP5.ini to our PHP7.ini… if you go look at the php5.ini that PHP Tools set up you’ll see the following settings added at the bottom (for me it went under: C:\Program Files (x86)\iis express\PHP\v5.6): [XDEBUG] zend_extension=”C:\Program Files (x86)\IIS Express\PHP\v5.6\ext\php_xdebug.dll” xdebug.remote_enable = on xdebug.remote_handler = dbgp xdebug.remote_host = 127.0.0.1 xdebug.remote_port = 9000 xdebug.remote_mode = req so then we need to go get xdebug for php7

... READ MORE ...