Now that we’ve introduced how the CRX will be loaded by AutoCAD I/O – via an Autoloader bundle – we’re going to take a look at the code needed to create and test our Activity using it.
As a starting point – and as mentioned last time – you should get hold of the code in this sample on GitHub and copy & paste the (C# & XML) code we’ve seen in the last two posts into their respective files.
The code we’re going to see in today’s post belongs in Client\Program.cs. We’re very much tailoring the existing implementation, making sure it works specifically with our project. You’ll notice from the code that I’ve also made some changes to the name of the CrxApp project – you’ll want to do the same, for your own.
This project is basically intended to administer your Activity and the AppPackage it depends upon. It has a service reference to the AutoCAD I/O REST API, accessible via the AIO namespace. The code embeds the ConsumerKey and ConsumerSecret – that you can get in a few clicks via our developer portal – which is really ill-advised for a client-side application. You should only ever embed this information in a server-side application (which we’ll see in the next post in this series), but as this is strictly admin-only – and not being distributed to customers – it’s OK.
As part of the tool’s upload process, we’re also going to create a WorkItem with some dummy parameters (we’re specifying a size of 20x15 and about 100 pieces for our puzzle, we’re not providing any engraving layer) to exercise the Activity and make sure it works.
Here’s the C# code:
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net.Http;
namespace Client
{
class Credentials
{
// Get your ConsumerKey/ConsumerSecret at http://developer.autodesk.com
public static string ConsumerKey = "th15Isn0tAk3y";
public static string ConsumerSecret = "n0rI5th15aS3cr3t";
}
class Program
{
static readonly string PackageName = "JigsawPackage";
static readonly string ActivityName = "JigsawActivity";
static AIO.Container container;
static void Main(string[] args)
{
// Instruct client side library to insert token as Authorization value
// into each request
container =
new AIO.Container(
new Uri("https://developer.api.autodesk.com/autocad.io/v1/")
);
var token = GetToken();
container.SendingRequest2 +=
(sender, e) => e.RequestMessage.SetHeader("Authorization", token);
// Check if our app package exists
var package =
container.AppPackages.Where(a => a.Id == PackageName).FirstOrDefault();
string res = null;
if (package != null)
res =
Prompts.PromptForKeyword(
string.Format(
"AppPackage '{0}' already exists. What do you want to do? " +
"[Delete/Recreate/Update/Leave]<Update>", PackageName
)
);
if (res == "Delete")
{
container.DeleteObject(package);
container.SaveChanges();
package = null;
var activity2 =
container.Activities.Where(a => a.Id == ActivityName).FirstOrDefault();
if (activity2 != null)
{
container.DeleteObject(activity2);
container.SaveChanges();
activity2 = null;
}
return;
}
if (res == "Recreate")
{
container.DeleteObject(package);
container.SaveChanges();
package = null;
}
if (res != "Leave")
{
package = CreateOrUpdatePackage(CreateZip(), package);
}
// Check if our activity already exist
var activity =
container.Activities.Where(a => a.Id == ActivityName).FirstOrDefault();
if (activity != null)
{
if (
Prompts.PromptForKeyword(
string.Format(
"Activity '{0}' already exists. Do you want to recreate it? " +
"[Yes/No]<No>", ActivityName
)
) == "Yes")
{
container.DeleteObject(activity);
container.SaveChanges();
activity = null;
}
}
if (activity == null)
{
activity = CreateActivity(package);
}
// Save outstanding changes if any
container.SaveChanges(
System.Data.Services.Client.SaveChangesOptions.PatchOnUpdate
);
// Finally submit workitem against our activity
SubmitWorkItem(activity);
}
static string GetToken()
{
Console.WriteLine("Getting authorization token...");
using (var client = new HttpClient())
{
var values = new List<KeyValuePair<string, string>>();
values.Add(
new KeyValuePair<string, string>(
"client_id", Credentials.ConsumerKey
)
);
values.Add(
new KeyValuePair<string, string>(
"client_secret", Credentials.ConsumerSecret
)
);
values.Add(
new KeyValuePair<string, string>("grant_type", "client_credentials")
);
var requestContent = new FormUrlEncodedContent(values);
var response =
client.PostAsync(
"https://developer.api.autodesk.com/authentication/v1/authenticate",
requestContent
).Result;
var responseContent = response.Content.ReadAsStringAsync().Result;
var resValues =
JsonConvert.DeserializeObject<Dictionary<string, string>>(
responseContent
);
return resValues["token_type"] + " " + resValues["access_token"];
}
}
static string CreateZip()
{
Console.WriteLine("Generating autoloader zip...");
string zip = "package.zip";
if (File.Exists(zip))
File.Delete(zip);
using (var archive = ZipFile.Open(zip, ZipArchiveMode.Create))
{
string bundle = PackageName + ".bundle";
string name = "PackageContents.xml";
archive.CreateEntryFromFile(name, Path.Combine(bundle, name));
name = "Jigsaw.dll";
archive.CreateEntryFromFile(
name, Path.Combine(bundle, "Contents", name)
);
name = "Newtonsoft.Json.dll";
archive.CreateEntryFromFile(
name, Path.Combine(bundle, "Contents", name)
);
}
return zip;
}
static AIO.AppPackage CreateOrUpdatePackage(
string zip, AIO.AppPackage package
)
{
Console.WriteLine("Creating/Updating AppPackage...");
// First step -- query for the url to upload the AppPackage file
UriBuilder builder = new UriBuilder(container.BaseUri);
builder.Path += "AppPackages/GenerateUploadUrl";
var url =
container.Execute<string>(builder.Uri, "POST", true, null).First();
// Second step -- upload AppPackage file
UploadObject(url, zip);
if (package == null)
{
// Third step -- after upload, create the AppPackage entity
package = new AIO.AppPackage()
{
UserId = "",
Id = PackageName,
Version = 1,
RequiredEngineVersion = "20.0",
Resource = url
};
container.AddToAppPackages(package);
}
else
{
//Or update the existing one with the new url
package.Resource = url;
container.UpdateObject(package);
}
container.SaveChanges(
System.Data.Services.Client.SaveChangesOptions.PatchOnUpdate
);
return package;
}
static void UploadObject(string url, string filePath)
{
Console.WriteLine("Uploading autoloader zip...");
var client = new HttpClient();
client.PutAsync(
url,
new StreamContent(File.OpenRead(filePath))
).Result.EnsureSuccessStatusCode();
}
// Creates an activity with 2 inputs and variable number of outputs.
// All outputs are placed in a folder 'outputs'
static AIO.Activity CreateActivity(AIO.AppPackage package)
{
Console.WriteLine("Creating/Updating Activity...");
var activity = new AIO.Activity()
{
UserId = "",
Id = ActivityName,
Version = 1,
Instruction = new AIO.Instruction()
{
Script = "_erase _all _jigio params.json outputs\n"
},
Parameters = new AIO.Parameters()
{
InputParameters =
{
new AIO.Parameter()
{
Name = "HostDwg", LocalFileName = "$(HostDwg)"
},
new AIO.Parameter()
{
Name = "Params", LocalFileName = "params.json"
},
},
OutputParameters = {
new AIO.Parameter()
{
Name = "Results", LocalFileName = "outputs"
}
}
},
RequiredEngineVersion = "20.0"
};
container.AddToActivities(activity);
container.SaveChanges(
System.Data.Services.Client.SaveChangesOptions.PatchOnUpdate
);
// Establish link to package
container.AddLink(activity, "AppPackages", package);
container.SaveChanges();
return activity;
}
static void SubmitWorkItem(AIO.Activity activity)
{
Console.WriteLine("Submitting workitem...");
// Create a workitem
var wi = new AIO.WorkItem()
{
UserId = "", // Must be set to empty
Id = "", // Must be set to empty
Arguments = new AIO.Arguments(),
Version = 1, // Should always be 1
ActivityId =
new AIO.EntityId { Id = activity.Id, UserId = activity.UserId }
};
wi.Arguments.InputArguments.Add(new AIO.Argument()
{
Name = "HostDwg", // Must match the input parameter in activity
Resource =
"http://download.autodesk.com/us/samplefiles/acad/title_block-iso.dwg",
StorageProvider = "Generic" // Generic HTTP download (vs A360)
});
wi.Arguments.InputArguments.Add(new AIO.Argument()
{
Name = "Params", // Must match the input parameter in activity
// Use data URL to send json parameters without having to upload
// them to storage
ResourceKind = "Embedded",
Resource =
@"data:application/json, " +
JsonConvert.SerializeObject(
new JigsawGenerator.Parameters
{
Width = 20, Height = 15, Pieces = 100
}
),
StorageProvider = "Generic" // Generic HTTP download (vs A360)
});
wi.Arguments.OutputArguments.Add(new AIO.Argument()
{
Name = "Results", // Must match the output parameter in activity
StorageProvider = "Generic", // Generic HTTP upload (vs A360)
HttpVerb = "POST", // Use HTTP POST when delivering result
Resource = null, // Use storage provided by AutoCAD.IO
ResourceKind = "ZipPackage" // Upload files as zip to output directory
});
container.AddToWorkItems(wi);
container.SaveChanges();
// Polling loop
do
{
Console.WriteLine("Sleeping for 2 sec...");
System.Threading.Thread.Sleep(2000);
container.LoadProperty(wi, "Status"); // HTTP request is made here
Console.WriteLine("WorkItem status: {0}", wi.Status);
}
while (wi.Status == "Pending" || wi.Status == "InProgress");
// Re-query the service so that we can look at the details provided
// by the service
container.MergeOption =
System.Data.Services.Client.MergeOption.OverwriteChanges;
wi =
container.WorkItems.Where(
p => p.UserId == wi.UserId && p.Id == wi.Id
).First();
// Resource property of the output argument "Results" will have
// the output url
var url =
wi.Arguments.OutputArguments.First(
a => a.Name == "Results"
).Resource;
DownloadToDocs(url, "AIO.zip");
// Download the status report
url = wi.StatusDetails.Report;
DownloadToDocs(url, "AIO-report.txt");
}
static void DownloadToDocs(string url, string localFile)
{
var client = new HttpClient();
var content = (StreamContent)client.GetAsync(url).Result.Content;
var fname =
Path.Combine(
Environment.GetFolderPath(Environment.SpecialFolder.MyDocuments),
localFile
);
Console.WriteLine("Downloading to {0}.", fname);
using (var output = System.IO.File.Create(fname))
{
content.ReadAsStreamAsync().Result.CopyTo(output);
output.Close();
}
}
}
}
When we execute it for the first time, it should just upload and exercise the Activity. For subsequent runs there’s an “Update” options, although I’ve found it more effective to use “Delete” (an option I added) and then run it again.
Here’s the output we see in the command prompt when it executes properly:
The output from the CRX app – which has been updated to create output, more details in the original post showing it – should have been Zipped up and downloaded to your “My Documents” folder. There should be an execution report there, too, showing exactly what went on on the command-line.
Here’s the PNG that was generated by the test WorkItem:
The DWG is also there, of course, which will drive our imaginary laser cutter. Here are the contents of AIO-report.txt, showing what went on:
[06/17/2015 14:09:07] Starting work item 34365e590f80433ebe4a75d4c3a9b8bc
[06/17/2015 14:09:07] Start download phase.
[06/17/2015 14:09:07] Start downloading file http://download.autodesk.com/us/samplefiles/acad/title_block-iso.dwg.
[06/17/2015 14:09:07] End downloading file http://download.autodesk.com/us/samplefiles/acad/title_block-iso.dwg. 128233 bytes have been written to C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\title_block-iso.dwg.
[06/17/2015 14:09:07] Embedded resource data:application/json, {"Width":20.0,"Height":15.0,"Pieces":100,"XRes":0,"YRes":0,"Pixels":null} is saved as file: C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\params.json.
[06/17/2015 14:09:07] End download phase.
[06/17/2015 14:09:07] Start preparing script and command line parameters.
[06/17/2015 14:09:07] Folder "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\outputs" has been created.
[06/17/2015 14:09:07] Start preparing AppPackage JigsawPackage.
[06/17/2015 14:09:07] Downloading bits to local cache.
[06/17/2015 14:09:07] Done preparing AppPackage JigsawPackage.
[06/17/2015 14:09:07] Start script content.
[06/17/2015 14:09:07] _erase _all _jigio params.json outputs
[06/17/2015 14:09:07] End script content.
[06/17/2015 14:09:07] Command line: /i "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\title_block-iso.dwg" /sandbox /isolate job_34365e590f80433ebe4a75d4c3a9b8bc "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\userdata" /al "Z:\Aces\AcesRoot\20.0\coreEngine\AppCache\JigsawPackage.package" /s "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\script.scr"
[06/17/2015 14:09:07] End preparing script and command line parameters.
[06/17/2015 14:09:07] Start script phase.
[06/17/2015 14:09:07] Start AutoCAD Core Console output.
[06/17/2015 14:09:09] Redirect stdout (file: C:\Users\ACESWO~1\AppData\Local\Temp\accc284452).
[06/17/2015 14:09:09] AutoCAD Core Engine Console - Copyright 2014 by Autodesk, Inc. (J.48.C.62)
[06/17/2015 14:09:09] Isolating to userId=job_34365e590f80433ebe4a75d4c3a9b8bc, userDataFolder=C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\userdata.
[06/17/2015 14:09:09] Launching sandbox process: ["Z:\Aces\AcesRoot\20.0\coreEngine\Exe\accoreconsole.exe" /i "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\title_block-iso.dwg" /sandbox /isolate job_34365e590f80433ebe4a75d4c3a9b8bc "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\userdata" /al "Z:\Aces\AcesRoot\20.0\coreEngine\AppCache\JigsawPackage.package" /s "C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\script.scr"]
[06/17/2015 14:09:09] Redirect stdout (file: C:\Users\acesworker\AppData\LocalLow\temp\accc108871).
[06/17/2015 14:09:09] AutoCAD Core Engine Console - Copyright 2014 by Autodesk, Inc. (J.48.C.62)
[06/17/2015 14:09:09] Running at low integrity.
[06/17/2015 14:09:09] Regenerating model.
[06/17/2015 14:09:09] Command:
[06/17/2015 14:09:09] Command:
[06/17/2015 14:09:09] Command:
[06/17/2015 14:09:09] Command: _erase
[06/17/2015 14:09:09] Select objects: _all 2 found
[06/17/2015 14:09:09] 1 was not in current space.
[06/17/2015 14:09:09] Select objects:
[06/17/2015 14:09:10] Command: _jigio
[06/17/2015 14:09:10] Specify parameter file: params.json
[06/17/2015 14:09:10] Specify output folder: outputs
[06/17/2015 14:09:11] Puzzle will be 12 x 8 (96 in total)._zoom
[06/17/2015 14:09:11] Specify corner of window, enter a scale factor (nX or nXP), or
[06/17/2015 14:09:11] [All/Center/Dynamic/Extents/Previous/Scale/Window/Object] <real time>: _extents _pngout Enter file name <C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\title_block-iso.png>: outputs\jigsaw.png Select objects or <all objects and viewports>:
[06/17/2015 14:09:11] Command: _quit
[06/17/2015 14:09:11] End AutoCAD Core Console output
[06/17/2015 14:09:11] End script phase.
[06/17/2015 14:09:11] Start upload phase.
[06/17/2015 14:09:11] Zipping directory C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\outputs as C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\outputs.zip.
[06/17/2015 14:09:11] Start uploading C:\Users\acesworker\AppData\LocalLow\jobs\34365e590f80433ebe4a75d4c3a9b8bc\outputs.zip
to https://acadio.s3.amazonaws.com/aces-workitem-outputs/34365e590f80433ebe4a75d4c3a9b8bc/outputs.zip?
AWSAccessKeyId=ASIAI2C57P7J27QHKLFQ&Expires=1434553752&x-amz-security-token=AQoDYXdzEC4a4APZUiKTndtDnbB
jLIBMYhX1hCvyhmosYwYpoks0mmbOn4wbdDYB4kMEkTv8x3SlG7v3Rqd3mLgiT24mzq9FFHhI3Gt25Yix8ZYrGYFONo3BigltsE%2Fk
XuF5ddYea3GiuEaL9prC5RBo2BEl2xIfEFjATy9EH1MsrpnJl%2B3gzOuYp5WASaJuIpgqSt4z36Hwc6s1uVq0bqvHFEQP2iqCibB%2
FL9HmQZOH%2FrCirvGLMin0GysAIH7ZGNrKJsvnsEBIGhRoxPC8j5FRwUoa21je06jz4moDSGCE5%2FsTuYSZfWRuH5afPqubJZmB5Z
S9Me%2FEG89%2BmcZslctHtueKrYAHAme4t5Sw3srdaWLmRaXb6vj9Zm8GPyB4W07JOE1RjdoAii61%2BX6vHMjJWFie%2BnRx2hma%
2Fs37Pp8pAbUYBt0M7Zu9%2BhqMPQfjOadS8uTBTY88RsCit1bd6HBD0XDjISxwozLUMgKBDjyJQvfFsgGyzhDkJJNrkAtufFMjcB4p
cNnaa9eajG%2BuIFTmj%2BVSR2EdeWdbbvKLlxIfZ2OAjHlypMz84kTBCXq%2FvfH3hv%2F3fSP4mR5z4BfyqYUHubM8ydyQcYfaTCv
eBPc%2FqNRaRVUYTjioiXgmVvDnAOV%2BjPj0jTWB0tsg7eKFrAU%3D&Signature=Li9T3WxlTsmvIXjmSjGZG3csJUo%3D.
[06/17/2015 14:09:12] End upload phase.
[06/17/2015 14:09:12] Job finished with result Succeeded
One thing I need to fix: right now the test code passes in a sample DWG that’s posted on web. I haven’t yet worked out how to start without a base DWG file – we’re generating all our geometric content from scratch, of course – so we’re currently erasing any existing geometry before we start. I’m sure there’s an easy fix: once I’ve found out what it is I’ll update this post.
The next couple of posts will be on other topics, as we need a small break from this series (and I have some other things to talk about). When we return we’ll be looking at how to call our Activity from the Node.js back-end of our Jigsawify.com web-site.
Update:
It turns out you do indeed need to provide a HostDwg input parameter: this is by far the most common scenario, even if it’s to specify a blank initial drawing. Luckily the standard AutoCAD drawing templates are available online, so you can replace the above-linked DWG with this one. And remove the “_erase _all ” part of the test script, of course, as it doesn’t contain anything to erase.