Tuesday, June 12, 2007

Recently I had the need to create a component in .NET that would be callable as a COM component (not unlike many other such objects in the past).  This one, however, was peculiar in that I also needed to be able to call it from a web page running in IE (due to constraints already on the application, cross-browser support is not a concern in this case).  I didn't want the users to be burdened with dialog boxes or even require that they change their security settings.  I wanted to create the object as 'safe for scripting'.

There are two approaches that I know of that address the issue: implementing IObjectSafety (a COM interface) or directly managing the object via the Component Category Manager (ICatRegister COM interface) and registering the CAT_SafeForScripting GUID.

The first presumes that I have access to and can compile the code, because the IObjectSafety interface must be implemented directly on the object in question.  The second solution can be done after the fact (e.g., within an installer).

Due to the nature of the project, I opted for the first option because, frankly, it was the easier of the two to implement given my time constraints.

The IObjectSafety interface is not native to the .NET world.  As such, in order to implement a COM interface on a .NET object the interface must be defined within the managed code and then decorated with the appropriate attributes.

Here's my implementation of the IObjectSafety interface in managed code:

[ComImport()]
[Guid("CB5BDC81-93C1-11CF-8F20-00805F2CD064")]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IObjectSafety {
   [PreserveSig()]
   int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions);
                    
   [PreserveSig()]
   int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions);
}

The ComImportAttribute identifies that the interface was previously defined in COM with the IID specified by the GuidAttribute.  The PreserveSigAttribute is important on each of the methods to tell the .NET runtime to not suppress the HRESULT when the method is called.

Once defined, it's then a simple matter to implement the interface on my class, and indicate that it's safe for scripting:

[ClassInterface(ClassInterfaceType.AutoDispatch)]
[Guid("43B6ADAA-6DE7-43c2-9206-3389C94B9531")]
[ProgId("DemoLib.DemoObject")]
public sealed class DemoObject : IObjectSafety {
   private const int INTERFACESAFE_FOR_UNTRUSTED_CALLER = 0x00000001;
   private const int INTERFACESAFE_FOR_UNTRUSTED_DATA = 0x00000002;
   private const int S_OK = 0;

   public int GetInterfaceSafetyOptions(ref Guid riid, out int pdwSupportedOptions, out int pdwEnabledOptions) {
      pdwSupportedOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA;
      pdwEnabledOptions = INTERFACESAFE_FOR_UNTRUSTED_CALLER | INTERFACESAFE_FOR_UNTRUSTED_DATA;
      return S_OK;
   }

   public int SetInterfaceSafetyOptions(ref Guid riid, int dwOptionSetMask, int dwEnabledOptions) {
      return S_OK;
   }
}

Once registered (via Regasm.exe), you can invoke methods on an instance of the object via your script:

var obj = new ActiveXObject("DemoLib.DemoObject");
obj.CallSomeMethod();

Tuesday, June 12, 2007 11:06:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Thursday, June 07, 2007

I hate little hacks like this, but here it is nonetheless.  If someone has a better solution I would love to hear it.

I've been tinkering with the WinForms TreeView control a bit and ran into a documented (but undesirable) behavior.  I'm wanting to display my hierarchical node structure in the tree, but I want to 'highlight' some of them for one reason or another.  My preferred highlight in this case is to use a bolding effect on the font and to change the text color.

The undesired effect, however, is when you apply a font to the node that is larger than the base font of the node the text will clip.  Well, bolded text is larger - it's wider than the text and as a result the right-most character(s) get clipped and it looks really tacky.

My hack isn't without it's limitations and considerations, but all I need to do to 'workaround' this issue is to apply some non-breaking spaces in the text to display such that they get clipped and not the actual text.  This is definitely error-prone as well because just one nbsp doesn't necessarily address the issue (but does in my situation for most texts).

string text = “This is ridiculous” + (char)160;
TreeNode node = new TreeNode(text);

I can see this being quite an issue with non-latin-based fonts, though I've nothing to substantiate that claim.

One side-effect of doing this is that the 'clip box' around the text grows to accommodate the non-breaking spaces.  You can surely set your TreeView to use .FullRowSelect, and to turn off .ShowLines, but that may not be desirable in some situations.

Does anyone have/know of a better solution with the plain, vanilla TreeView?  I've tried other tactics but none led to any better results.  Maybe I'm missing something blatently obvious.

Thursday, June 07, 2007 7:29:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Wednesday, May 30, 2007

Just the other day I posted some code wherein an MSMQ queue is created and the Network Service account was granted full control to the queue.  Though I didn't mention it, this was necessary because the Windows Service runs under that account and would need access to the queue.

As it turns out the code sample has issues when running on Windows XP.  Windows XP doesn't allow the NT AUTHORITY\NETWORK SERVICE account to be associated with the permissions on a message queue (I've not looked into why yet).  When the code runs no error is thrown and it happily attempts to grant the permissions.  This results in a queue whose permissions are corrupted (not even an administrator can access it) and all users are denied access.  The only way I've found to remove such a queue is to uninstall MSMQ and reinstall it.  Does anyone know of another solution to that?

Therefore, in my code a put a rudimentary workaround to the problem.  I perform a simple check to see if the OS is Windows XP and if so, not set permissions.  This works in this case (though I hate to rely on these behaviours) because the default permissions on a queue in XP is Everyone:Full Control, so the service can see the queue just fine.

if ( !isWindowsXP() ) {
   const string accountName = @"NT AUTHORITY\NETWORK SERVICE";
   Trustee trustee = new Trustee(accountName, null, TrusteeType.User);
   MessageQueueAccessRights rights = MessageQueueAccessRights.FullControl;
   MessageQueueAccessControlEntry ace = new MessageQueueAccessControlEntry(trustee, rights);
   queue.SetPermissions(ace);
}

...

private static bool isWindowsXP() {
   OperatingSystem os = Environment.OSVersion;
   return ( PlatformID.Win32NT == os.Platform && 5 == os.Version.Major && 0 != os.Version.Minor );
}

Again, very simple and potentially error prone (what about Windows 2000 Pro?), but in my case it suffices. 

Wednesday, May 30, 2007 6:33:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [1]  |  Trackback
 Sunday, May 27, 2007

In terms of keeping this blog up-to-date on my goings on, the month of May has been a dismal one.  There has been a lot going on, but I've simply not made the time to log my activities and/or code snippets and it's eating at me.

Recently, I've been writing some code that needed to run in response to certain activities that would be triggered by another application.  These applications would not be connected, per se, in the sense that one would 'hook' into the other and respond to events.  Rather, one application would set a flag, send a message, etc such that the second application would recognize the notification and run some activity in response.

The logical choice for such an architecture is a messaging-based system.  In this case, I decided upon MSMQ/Queued Components and boy am I happy about that!  I hadn't seriously worked in MSMQ for years (though I used to do quite a bit of it) and it was fun to romp around my old playing grounds, but now with a .NET engine helping out.  Tons of fun!

This particular application is structured such that I have a web-based application (ASP.NET in this case) that will send certain messages to a message queue when the user completes some task.  Then I have a Windows Service running that will read messages from the queue and run a configurable response.  As part of the setup, I have written a few installer classes that will 1) create the message queue and 2) install the Windows Service and 3) run it automatically after install.

The task of creating a message queue installer is quite simple and straightforward.  I wanted my installer to detect the existance of the queue and create it if it wasn't found.  Upon rollback or uninstall, the queue would be deleted.  Here's an example from the Install method of my Installer class:

public override void Install(IDictionary stateSaver) {
   base.Install(stateSaver);

   try {
      const QueueName = @".\Private$\TestQueue";
      if ( !MessageQueue.Exists(QueueName) ) {
         MessageQueue queue = MessageQueue.Create(QueueName, true);
         Trustee trustee = new Trustee(@"NT AUTHORITY\NETWORK SERVICE");
         MessageQueueAccessRights rights = MessageQueueAccessRights.FullControl;
         MessageQueueAccessControlEntry ace = new MessageQueueAccessControlEntry(trustee, rights);
         queue.SetPermissions(ace);
      }
   }
   catch ( Exception er ) {
      // log the error to the event log
      throw;
   }
}

At this point, I'm ready to start interacting with the message queue by sending and receiving messages.

To simplify this process and to make it more structured, I decided to create a simple class called QueueItem.  This class provides the structure and data needed by the recipient application so that it can effectively run in response to the website's activity.  This class is also decorated with the appropriate attributes that enable it to be serialized to XML.  To further simplify the process of interacting with the message queue, the QueueItem class contains a method that allows the caller to send the message to the queue; all of the logic necessary is enclosed therein:

[XmlRoot("queueItem")]
public sealed class QueueItem {
   public QueueItem() { /* parameterless ctor for serialization support */ }
   public QueueItem(string id, string action) {
      _id = id;
      _action = action;
   }

   private string _id, _action

   [XmlElement("id")]
   public string Id {
      get { return _id; }
      set { _id = value; }
   }

   [XmlElement("action")]
   public string Action {
      get { return _action; }
      set { _action = value; }
   }

   public void SendToQueue(string label) {
      QueueItem.SendToQueue(this, label);
   }

   public static void SendToQueue(QueueItem item, string label) {
      MessageQueue queue = new MessageQueue(@".\Private$\TestQueue", QueueAccessMode.Send);
      queue.Formatter = new XmlMessageFormatter(new Type[] {typeof ( QueueItem )});

      using ( MessageQueueTransaction trans = new MessageQueueTransaction() ) {
         trans.Begin();
         queue.Send(item, label, trans);
         trans.Commit();
      }
   }
}

The Windows Service, now, is responsible for watching the queue and running the appropriate response to messages that find their way into the queue.  To accomplish this, I decided to have the service spin off a secondary thread at start up.  It's this thread that is responsible for processing messages off of the queue.

protected override void OnStart(string[] args) {
   try {
      Thread th = new Thread(new ThreadStart(processMessages));
      th.IsBackground = true;
      th.Name = "QueueListener";
      th.Start();
   }
   catch ( Exception er ) {
      EventLog.WriteEntry(Service.ActualName,
                          string.Format("Unable to start service\n\n{0}", er),
                          EventLogEntryType.Error);
      this.ExitCode = 1;
      this.Stop();
      return;
   }
}

private void processMessages() {
   try {
      using ( MessageQueue queue = new MessageQueue(@".\Private$\TestQueue", QueueAccessMode.Receive) ) {
         queue.Formatter = new XmlMessageFormatter(new Type[] {typeof ( QueueItem )});

         while ( true ) {
            using ( MessageQueueTransaction trans = new MessageQueueTransaction() ) {
               trans.Begin();
               Message msg = queue.Receive();
               trans.Commit();
               QueueItem item = msg.Body as QueueItem;
               if ( null != item ) {
                  EventLog.WriteEntry(Service.ActualName,
                                      string.Format("Message received: {0}, {1}", item.Id, item.Action),
                                      EventLogEntryType.Information);
               }
               else {
                  EventLog.WriteEntry(Service.ActualName,
                                      string.Format("Invalid message datatype received: {0}.  Expected: QueueItem.", msg.Body.GetType().FullName),
                                      EventLogEntryType.Warning);
               }
            }
         }
      }
   }
   catch ( Exception er ) {
      EventLog.WriteEntry(Service.ActualName,
                          string.Format("Critical shutdown\n\n{0}", er),
                          EventLogEntryType.Error);
      this.ExitCode = 1;
      this.Stop();
      return;
   }
}

The beauty here is that I don't have to poll.  Rather, I have the background thread call .Receive() on the queue.  This will effectively block the thread indefinitely or until a message is pushed into the queue.  At that point, it unblocks and begins processing the message.  Once finished it calls .Receive() again.  If there's a message, it gets processed, if not, it blocks.  And thus the cycle repeats itself.

Using MSMQ can be very rewarding and a lot of fun, and this little project has reminded me of that fact.

Sunday, May 27, 2007 6:51:00 PM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [2]  |  Trackback
 Tuesday, May 15, 2007

I have a very small utility application that I wrote that takes an XML string and submits it as a query against a web service, displaying the result.  Very simple, and just for testing purposes.  There are times when the XML string that I want to use as the web service query is encoded.  That is, it's littered with &lt; and &gt; strings in place of the XML '<' and '>' respectively.  Were these strings succinct it wouldn't be a big deal, but there are times where the query string is several thousand characters.

In the past I've resorted to opening a text editor and performing a find/replace on each of the escaped sequences, and then copying and pasting the result into my application to run the test.  Simple but cumbersome.

Well, I finally got tired of jumping through those hoops and decided to make the application a bit smarter in that I wanted to have it identify when encoded XML was being pasted into it, and have the application automatically format the result performing the string replacements automatically.  This is quite trivial, but I wanted to share the result as it may be informative.

My 'algorithm' for determining whether the text being pasted is in fact XML is quite rudimentary and basic, but it suffices in this instance.  I'm simply checking for '&lt;' at the start of the string.  If that sequence is found, it determines, hey, this is an XML string, and attempts to perform the replacement.

It occurred to me as well while writing this that XML isn't the only format that I may want to custom-paste into a TextBox.  Therefore, I abstracted the code such that you can write your own special formatting code and simply 'plug it in' to your application.  NOTE: I did NOT want to derive from the TextBox to create a special 'CustomPasteTextBox' control as that would further restrict its usefulness.

The base class (which I called PasteFormatterBase) takes a TextBox and a callback.  The operation is wrapped within a NativeWindow-derived SubClasser class; when the WM_PASTE message is received, a call is made back out to the callback to handle it.

public abstract class PasteFormatterBase {
   public PasteFormatterBase(TextBox txt, MethodInvoker callback) {
      _subClass = new SubClasser(callback);
      _subClass.AssignHandle(txt.Handle);
   }

   private SubClasser _subClass;

   private class SubClasser : NativeWindow {
      private const int WM_PASTE = 0x0302;
      private MethodInvoker _callback;

      public SubClasser(MethodInvoker callback) {
         _callback = callback;
      }


      protected override void WndProc(ref Message m) {
         if ( m.Msg == WM_PASTE && null != _callback ) {
            _callback();
            return;
         }

         base.WndProc(ref m);
      }
   }
}

Then, the class that performs the actual XML decoding of the string is quite simple:

public sealed class PasteXmlFormatter : PasteFormatterBase {
   public PasteXmlFormatter(TextBox txt)
      : base(txt,
             delegate {
                   IDataObject obj = Clipboard.GetDataObject();
                   string data = obj.GetData(DataFormats.Text) as string;
                   if ( null != data && data.StartsWith("&lt;") ) {
                      data = data.Replace("&lt;", "<").Replace("&gt;", ">");
                      txt.SelectedText = data;
                   }
                })
   { }
}

There may be better ways to write this (it's just a quick first pass), but it's elegant in its simplicity.

Tuesday, May 15, 2007 8:15:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Friday, May 04, 2007
 For the waffle and computer lover in me, this may be just the thing I need.  Why couldn't have this been made earlier?
Friday, May 04, 2007 6:00:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback
 Wednesday, May 02, 2007

<grin>I've had a lot of free time lately.  So much, in fact, that I don't know what to do with myself.</grin>

With this copious free time I've dabbled a bit in Subversion.  I'd experimented with it before, but not on a grand scale.  In fact, I've used SourceSafe (which in my experience isn't as bad as people would have you believe), SourceGear's Vault (which I use presently and absolutely love), and I've experimented a bit with CVS, Perforce, et al.  I've been very impressed with Subversion and thought I'd give it a whirl for all of my personal software development.  If nothing else, I get a good, rock-solid SCM system and a more well-rounded understanding of this other system.

Thus, with that in mind, I set out to establish a new source code control system.

First of all, I downloaded the current version (version 1.4.3) and installed it on my server.  This gave me the %ProgramFiles%\Subversion folder and added the %ProgramFiles%\Subversion\bin folder to my path.

Next, I don't ever have to want to logon to that server interactively just to start the application 'listening'.  So, I set it up as a service with the following command line:

sc.exe create svnserve binpath= "C:\Program Files\Subversion\bin\svnserve.exe --service --root C:\SVN" displayname= "SubVersion" depend= tcpip start= auto obj= "NT AUTHORITY\NetworkService"

(Note that there is no space before the equal sign for each parameter but there is one afterwards.  Personally, I think this is a silly construct for the sc.exe command, but maybe it made the command line easier to deal with.  Oh well, I didn't write it)

That statement will register the svnserve.exe application as an NT Service so in the event that I have to restart the server the service will automatically start.

Next, I created the root folder for my repositories (which for purposes of this example is C:\SVN) and granted the NT AUTHORITY\NetworkService account the appropriate rights to it.

The very first thing that I didn't really care for with respect to Subversion was the way it does authentication.  I wanted to have a single source for all authentication and not maintain it for each repository.  While not a show stopper, I don't like the idea of having to change n passwd files (one for each repository) in the event my password changes.  Now that I mention it, I don't like the idea of having to put it in a file at all (especially in clear text!) - there are oh so many security issues with this.  I have seen some articles on how to set it up to use Windows Authentication (which would be ideal in my situation), but I'm not that far along with it yet.

Nonetheless, I set up a simple folder named “C:\SVN\(auth)” to hold the authz and passwd files (I like to use parens on folder names like this so they filter to the top of my directory lists and stand out).

I then edited the files accordingly:

authz

[/]
Aaron = rw

passwd

[users]
Aaron = xxxxxx

As is traditional with SCM systems (and in keeping with that tradition), each repository has three folders: \trunk, \tags, and \branches.  However, when you create a repository those don't get created automatically for you.  Rather than always remembering to create these for each repository, I created a template folder called “C:\SVN\(template_repo)” which contains the structure and helper files for new repositories.  In addition to the folders there's a simple svnserve.conf configuration file that is designed to replace the automatically created svnserve.conf file in each new repository and point back to the (auth) folder.  That way, each new repository uses the same catalog of credentials.  Then I have a batch file that simplifies the creation of a repository and automatically imports the folder structure, replaces the files, and deletes extraneous ones.  I put this batch file in the root C:\SVN folder:

CreateRepo.bat

@echo off
if "%1" == "" goto USAGE

echo Creating SVN repository "%1".
svnadmin create "%1"

echo Establishing default credentials.
copy "(template_repo)\svnserve.conf" "%1\conf\svnserve.conf" > NUL
del "%1\conf\authz." > NUL
del "%1\conf\passwd." > NUL

echo Building folder structure.
svn import "(template_repo)\folders" svn://%COMPUTERNAME%/%1 --message "Initialize folder structure for repository." --quiet

echo Done.  "%1" repository created.
goto DONE

:USAGE
echo.
echo Invalid usage.  Please specify a repository name.

:DONE

I guess the last thing I needed to do to set it all up was to configure my backups.  There's not much point in having an SCM system without a good backup system in place.  Well, everything done to this point was done with a purpose.  I intentionally put everything in the C:\SVN folder to have a single point of reference for all repositories and file, and to simplify backups.  I can simply walk the directories therein and back them up.  If I add a new repository it's automatically included in my backups and there's nothing more to set up.

In the Subversion documentation they warn against simply 'copying' the repository tree or your backup might be completely invalid and useless.  Instead, you're recommended to run svnadmin.exe with the hotcopy argument.

I wrote a very simple VBScript (appropriately named BackupSVN.vbs) to take care of my backups.  I put this file one level out of the repository root (e.g., C:\ for this example).

Essentially the script file enumerates all of the directories, determining whether they are Subversion repositories or not.  For those that are, it runs svnadmin hotcopy.  For those that aren't, it simply runs xcopy.  All backups are then made to the \SVNBackup\SVN folder.  The script then executes WinRar to archive the tree into a date-stamped .rar file which is placed in the \SVNBackup folder.

Though it doesn't do it yet, I'll soon have this archiving the .rar files to a remote store (e.g., a network share) because a backup on the same physical drive isn't much good if the drive crashes.

BackupSVN.vbs

' ************************************************************************************

Dim fso, shell, f, root, fileName
Set fso = CreateObject("Scripting.FileSystemObject")
Set shell = CreateObject("WScript.Shell")
Set root = fso.GetFolder(".\SVN")

'
' copy each folder to the svn backup directory
'

For Each f in root.SubFolders
   if ( IsSVNFolder(f) ) then
      cmd = "svnadmin hotcopy ""SVN\" + f.Name + """ ""SVNBackup\SVN\" + f.Name + """"
   else
      cmd = "xcopy ""SVN\" + f.Name + "\*.*"" ""SVNBackup\SVN\" + f.Name + """ /I /E /Y /H"
   end if
   Call Exec(cmd)
next

'
' copy each file in the \SVN directory to the backup directory and archive it
'

fileName = """SVNBackup\SVNBackup (" + Replace(FormatDateTime(Date, 2), "/", "-") + ").rar"""
Call Exec("xcopy SVN\*.* SVNBackup\SVN /Y")
Call Exec("""%ProgramFiles%\WinRar\rar.exe"" a " + fileName + " SVNBackup\SVN\*.* /ep1 /r /df")
fso.DeleteFolder(".\SVNBackup\SVN")


' ************************************************************************************

Function Exec(cmd)
   Dim result
   Set result = shell.Exec(cmd)
   Do While ( result.Status = 0 )
      WScript.Sleep 100
   Loop
End Function


Function IsSVNFolder(folder)
   IsSVNFolder = HasFolder(f, "conf")
End Function


Function HasFolder(folder, name)
   Dim f
   for each f in folder.SubFolders
      if ( f.Name = name ) then
         hasFolder = true
         exit function
      end if
   next
   hasFolder = false
End Function

I simply set this VBScript to run nightly and I'm off to the races.

Wednesday, May 02, 2007 5:28:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [6]  |  Trackback
 Tuesday, May 01, 2007

Today was not a fun day.  Among the many tasks that I had to accomplish my primary development virtual machine decided to barf on me.  The system would boot but then the moment I attempted to run VS and load a project it would 'blink' off and reboot.  It did this twice and then decided to run a CHKDSK at the third boot.  This found several invalid file chains and ended up 'restoring' several files.

At this point, the OS wouldn't even boot, saying that the C:\WINDOWS\SYSTEM32\CONFIG directory was missing.  Great!  My next step was to run a repair from the Windows XP boot disk.  This ran fine (for a while) until it eventually crashed with an invalid memory address read.

The rest of my day was spent attempting to get my development files off of the VPC so I could move over (a.k.a., rebuild) another.  That, in and of itself, isn't a big deal - I have several base images to work with so I'm not worried.  Getting the files off of a VPC image turned out to be non-trivial.  There may be better ways to do this (and I'm open to thoughts), but here's what I did:

  1. Create a secondary, blank VHD (virtual hard drive).  I had to do this because VPC wouldn't let me mount a non-CD/DVD/ISO drive within the image and I couldn't share a folder between the VM and the host machine.
  2. Boot the VPC to the command prompt (I used an old Win98 disk cause it was laying around).
  3. FDISK to create a partition on the VHD.
  4. Boot to the XP Recovery Console (via the XP boot disk) - a step which we'll soon see was a waste of time.
  5. FORMAT D:\ /FS:NTFS
  6. At this point, I wanted to copy my ~\dev directory to the new VHD.  Well, as it turns out the XP Recovery Console doesn't allow XCOPY (even when referenced directly in the c:\windows\system32 folder) !! Even then, COPY doesn't allow wildcards nor directory copying !!  This completely, utterly baffles me.
  7. I then remembered my best friend in boot-time travails: Windows PE (Pre-installation Environment) Server Edition: boot to WinPE.
  8. Once I booted up to PE it was a cakewalk.  It's a good thing too, because manually copying 2490 files manually (one by one due to lack of wildcards in NTRC) would have been a nightmare.

Well, now I'm back up and running, but what a nightmare.

Tuesday, May 01, 2007 10:33:00 AM (Mountain Standard Time, UTC-07:00)  #    Disclaimer  |  Comments [0]  |  Trackback