A few days ago I had a bizarre website administration issue. I had written an ASP.Net app that inadvertently locked my assemblies in the /bin directory. Granted the obvious, that assemblies in ASP.Net applications are shadow-copied to the Temporary ASP.Net files folder under the specific version of the framework, but my app was special in that it dynamically loaded and inspected the assemblies in the
actual bin directory.
In designing the app, I had waited to put the inspection of these assemblies in a separate AppDomain, electing to do that when time permits. Because of this, When I loaded the assemblies in the /bin directory, the .Net Framework keeps a lock on those files for the lifetime of the current AppDomain (until it is unloaded). For working on my local machine, this is fine during development. If anything locks, I can simply run an IISRESET command, which will unload all AppDomains for ASP.Net web applications on my server. When migrating these assemblies to my hosted environment, it was a different story.
Using FTP, I pushed up all my new development work, did a small smoke test, tried my pages and voila! She worked! I went back into development and modified my pages so they load all of these assemblies on a separate AppDomain (which can be unloaded through code). I went to push up my new changes and then it hit me...
The assemblies were now locked in the hosted environment!Well, great! Now I can't push up my new changes, or any other changes for that matter, until the AppDomain which runs my app in the hosted environment is unloaded. This means either calling the hosts on the phone and asking them to do an IISRESET for me, which since this is a shared server, they probably won't do because it would knock off other customers for a few seconds, or asking them to reboot the server, which for the same reason, they also probably will not do. Oh, I'm so screwed.
So I got to thinking. I could either wait for one of these two things to happen naturally, which since one of the founding principles of shared/dedicated hosting is 100% uptime, could be never, or I could take matters into my own hands. I figured there had to be a way to release file locks through code. Seems easy enough, right?
My first challenge was to find a method to unlock these files which would only affect ME. I couldn't be responsible for interrupting someone else's site on that server. My solution had to be isolated to my own application. I started writing a little administrative ASPX-only (no codebehind) page which could perform my task.
I tried using System.IO.FileStream to unlock the individual files. Well, that didn't work because the Framework will sense that the files are already locked BEFORE you even try to unlock them, and throws an exception.
So I did a little research. I found a way to unload a web app and return it to a state just like it was the first time it was run. Call HttpRuntime.UnloadAppDomain(). What this does is clears out the Temporary ASP.Net Files folder for your application. The next time a request is made, it reinitializes the app, shadow copying all the assemblies in the /bin directory to the Temporary ASP.Net Files folder. Cool. You would think that would work. It did not! It shadow copied all of the assemblies, but naturally it doesn't touch the actual assemblies in the /bin directory, which means it didn't affect the locks. D'Oh!
I was growing frustrated. My next step was to do something a touch more drastic that wouldn't be isolated to just me. I was going to programmatically perform an IISRESET on the server. I wished there was a way to do an IISRESET just for a particular LM Instance, but alas, there is not. I would be taking down every internet service on that remote machine momentarily. Fuck it. Let's do it. So I wrote some VB code that would start a Process which would run IISRESET. I tried it, it worked on the hosted server. I was actually rather surprised that the user account which ASP.Net was running under on that server actually had permissions to start a process. Gift Horse -- Mouth -- whatever.
I could see IIS coming down and coming back up. Thankfully it only took a few seconds. Must be a powerful machine to perform an entire IISRESET in like 3 seconds. Alright! I reconnected to FTP and tried to upload my latest DLLs and DAMMIT! Still locked. Sumummabitch! I was really pissed now. I couldn't understand why an IISRESET wouldn't release those file locks.
I did a little more research. The basic principle of unlocking a file that your app did not lock is to get its file handle, get the process which has the file handle open, and use the Win32 API method CloseHandle to release the lock. Well since I did not and could not know what the file handle was on the hosted server, I tried a slightly different method that ultimately worked out for me.
I started thinking I would use the Win32 API to get a list of the processes with open file handles on my files and simply kill them. Then it hit me. You're overthinking, stupid! You know which processes are running ASP.Net. The only processes ASP.Net has ever run under are aspnet_wp.exe (ASP.Net 1.0) and w3wp.exe (ASP.Net 1.1 and 2.0). Fuck it! I'll just kill them. At this point after I had done a couple of IISRESETs on the hosted server, I didn't give a shit about interrupting anyone's services anymore. I was too pissed off.
THANK GOD! It worked. Killing those processes on the server released my file locks and I was able to make my updates. So now I kind of have an ultimate IIS administration tool now. I can provide any task I need from a web page, provided the ASP.Net process account is given enough permission. Here Is a sample of my work.
<%@ Page Language="vb" AutoEventWireup="false" Inherits="System.Web.UI.Page" %>
<%@ Import Namespace="System.Diagnostics" %>
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<html>
<head>
<title>IIS Override Tool</title>
<meta name="GENERATOR" content="Microsoft Visual Studio .NET 7.1">
<meta name="CODE_LANGUAGE" content="Visual Basic .NET 7.1">
<meta name=vs_defaultClientScript content="JavaScript">
<meta name=vs_targetSchema content="http://schemas.microsoft.com/intellisense/ie5">
</head>
<body MS_POSITIONING="FlowLayout">
<form id="Form1" method="post" runat="server">
<asp:Button ID="Button1" Runat="server" Text="Unload AppDomain" OnClick="Button1_Click" />
<asp:Button ID="Button2" Runat="server" Text="Reset IIS" OnClick="Button2_Click" />
<asp:Button ID="Button3" Runat="server" Text="Kill ASP Worker Process" OnClick="Button3_Click" />
<script language="vb" runat="server">
Private Sub Button1_Click(ByVal sender As Object, ByVal e As System.EventArgs)
HttpRuntime.UnloadAppDomain()
End Sub
Private Sub Button2_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Dim si As New ProcessStartInfo
si.FileName = "iisreset"
si.Arguments = ""
si.UseShellExecute = True
Dim prc As Process = Process.Start(si)
End Sub
Private Sub Button3_Click(ByVal sender As Object, ByVal e As System.EventArgs)
Dim killer As New ArrayList
For Each p As Process In Diagnostics.Process.GetProcesses()
Dim processName As String = String.Empty
Try
processName = p.ProcessName
Catch ex As Exception
End Try
If (processName <> Nothing) Then
If ((String.Compare(processName, "w3wp", True) = 0) OrElse (String.Compare(processName, "aspnet_wp", True) = 0)) Then
killer.Add(p)
End If
End If
Next
For Each p As Process In killer
p.Kill()
Next
End Sub
</script>
</form>
</body>
</html>
Labels: ASP.Net, Programming, Technology, VB.Net, Web Development