Under My LInks, My SharePoint Sites, you see a link to a site, but when you click on the link, you are directed to the Access Denied page. It's possible, you may be running into this scenario:
- Sub site is setup to inherit permissions from the parent
- Users were in the Members group of the parent site.
- The Profile Synchronization job ran and added the link to the subsite in My SharePoint Sites.
- Break permissions inheritance on the sub site. This copies all the permissions into the sub site
- Remove all the parent groups from the permissions on the sub site
Let's look at a quick example. You have a site collection at http://server/sites/teamsite. You then have a sub web at /sites/teamsite/project1 that inherits permissions from TeamSite. The Profile Synchronization timer job runs at the top of the hour and the users in the TeamSite Members group now have a link under My SharePoint Sites for TeamSite and Project1. Under Project1, you do the following:
- Click Site Actions | Site Settings
- Click People and Groups
- Click Site Permissions
- Click the Actions menu, then Edit Permissions
- Click OK on the dialog. At this point, Project1 has a copy of the permissions from TeamSite.
- On the Permissions page, select all the groups, click the Actions menu, select Remove User Permissions. This removes all the TeamSite groups and users from the permissions of the site.
At this point, Project1 no longer has the TeamSite SharePoint groups; however, Project1's Viewers, Members, and Owners groups still reference them. When the Profile Synchronization timer job runs, it's unable to determine which users are Members of the sub web and doesn't make any changes. This results in the Project1 link showing up in your My SharePoint Sites menu, but you don't actually have access to the site. Another symptom of this problem is that when you display the Open or Save As dialog in an Office application, you get prompted for authentication daily. This is a result of the Office client synchronizing the My SharePoint Sites information. See this post for more info on how that works. There is also a fix in the Office 2007 Client Hotfix Update from June that avoid the Office clients from prompting for authentication. KB Article 970950.
To fix this issue, you have to assign a SharePoint Group as the Members group on the sub web and all nested subwebs. This allows the Profile Synchronization job to determine which users are Members and clean up the links on the server-side. Once the link is cleaned up on the server-side, the Office client will sync up the next day and remove the link as well. In my testing, if you do not have the above Office hotfix, you will get one last prompt on the client before the link is cleaned up.
- Browse the subsite
- Click Site Actions | Site Settings
- Click People and Groups
- Settings menu | Set Up Groups
- Create 3 new groups for this subsite associated with Viewers, Members, and Owners
- Repeat for the other sub webs under this particular sub web.
SharePoint gives you the ability to get a list of sites that your login account is explicitly a member of. This grouping of links is built on the server-side by the Profile Synchronization timer job that runs for each Web Application. The data is stored in the SSP database and is visible in the following locations:
SharePoint Web UI
- Under your My Site by clicking the Memberships link

- From the My Links menu next to the "Welcome LoginName" link. Once you have informaiton in the Memberships view of the My Site, there will be a menu item added named My SharePoint Sites. Hovering over this menu item will show you all the links from your My Site, Memberships view.
Client machines
- These links are available on client machines in the Office Open/Save As dialog box as well.

You can run into various issues in which these links are not populated, or once populated are no longer cleaned up. I wanted to provide some information on how SharePoint determines which links to add to the Memberships view as well as provide some information about how the Office client knows which links to show. This should give some ideas for isolating and troubleshooting these issues. For this example, we'll setup a Site Collection at http://companyportal/sites/Home.
- When you create the Site Collection, there are three SharePoint Groups created. Home Viewers, Home Members, and Home Owners. These three SharePoint Groups are designated as the Viewers, Members, and Owners groups for the site. You can validate that these groups are setup and have permissions to the site by following these steps:
- Browse the Site collection
- Click Site Actions | Site Settings
- People and Groups
- Settings menu | Set Up Groups. You'll see which SharePoint Groups are designated as Visitors, Members, and Owner
I'll be posting another blog on a scenario in which these groups can cause links to display in My SharePoint Links when you no longer have access to the site.
- The Profile Synchronization timer job runs every hour and reads users that are explicitly added to the SharePoint Group listed as the Members group. In this case, users that are explicitly added to Home Members. A link to the Home site is added to the Memberships list in the My Site. The user's login account has to be explicitly added to the Members group as the job does not expand domain groups. It's also important to note that the user has to be in the Members group...being in the Owners group will not add the link.
- At this point the My SharePoint Sites menu item will show up under the My Links menu.
- When you open an Office client that is SharePoint aware (2003 and 2007), and you have configured a My Site on the client, a call is made to the GetLinks method of the following web service: /personal/<user>/_vti_bin/publishlinksservice.asmx
- This call by default occurs once a day.
- The response of the web service call is the items from your My Site's Memberships data.
- The timestamp of the sync is stored in the following registry key. If you delete this registry key, the Office client will sync with the server the next time the Open/Save As Dialog is displayed.
- HKEY_Current_User\Software\Microsoft\Office\12.0\Common\Portal\LinkPublishingTimestamp
- The Membership data returned is used to create a Network Places under your local profile at:
- DRIVE:\Documents and Settings\<LoginName>\Local Settings\Application Data\Microsoft\OFFICE\My SharePoint Sites
- Office also creates a registry key for each link that stores the meta data that came down from the web service call at:
- HKEY_Current_User\Software\Microsoft\Office\12.0\Common\Server Links
- The information is then displayed in the Open/Save As dialog under My SharePoint Sites
To reset everyone's Memberships links on the server side: [Word of caution...if the sync is failing for whatever reason, the links will remain empty until that is fixed...in other words, don't randomly run these steps.]
- Run: Stsadm.exe -o Sync -DeleteOldDatabases 0
- All the Memberships information for all users should be cleared out.
- Wait for the Profile Synchronization Timer Job to fire.
To reset your My SharePoint Links on the client side: [Same warning as above...if the web service call to the My Site fails, these links will not be repopulated until that connection is established.]
- Clear out the Network Places and Shortcuts under:
DRIVE:\Documents and Settings\<LoginName>\Local Settings\Application Data\Microsoft\OFFICE\My SharePoint Sites
- Delete the following registry keys:
HKEY_Current_User\Software\Microsoft\Office\12.0\Common\Server Links
HKEY_Current_User\Software\Microsoft\Office\12.0\Common\Portal\LinkPublishingTimestamp
- Open an Office client, and open the Open File Dialog
- Click My SharePoint Sites and the links should repopulate. May take 30-60 seconds since the web service call has to complete and the links have to be generated.
Hopefully this can save someone some time. The symptoms are:
- Clicking the My Site link in a SharePoint site results in an IIS 404 File Not Found error page being returned. However, you are able to browse directly to the users My Site by browsing http://mysite/personal/user. [http://mysite is the URL to the web application hosting the Personal Sites].
- Browsing anything in _layouts on this Web Application returns an IIS 404 File Not Found error page. For example, the following URL would fail: http://mysite/personal/user/_layouts/settings.aspx.
- IIS shows the _layouts directory and it is configured the way other working web applications are configured.
After doing some troubleshooting, we found that IIS is not trying to load any files from the Web Application content path. So the web.config is not even being looked for when browsing _layouts or the root of the My Site. We were able to find that the site collection in the root of the Web Application was missing. You can check for a Site Collection at "/" using "stsadm.exe -o enumsites -url http://mysite" or browsing Central Admin | Application Management | Site Collection List.
The root site collection in the My Site Web Application redirects users to their Personal Site. If the user does not have a My Site, this Site Collection creates one for them. It also displays the public information about users. Since this site was missing, SharePoint returns a 404 File Not Found message and no one is redirected to their My Site.
The reason the Site Collection was missing was because an admin tried to delete a user's My Site. The tricky part in this situation is when a user browses another person's My Site, they get redirected to a page in the root site collection. Here's the scenario:
- You need to delete user bobsmith's My Site and decide to do this by browsing the Site Collection, going into Site Settings and deleting the Site Collection.
- Open a browser and enter the URL to the persons My Site: http://mysites/personal/bobsmith .
- Since you are not logged in as bobsmith, you get redirected to http://mysite/person.aspx?accountname...
- When you click on Site Actions | Site Settings, you are really doing this on the root Site Collection at "/" and not the one for bobsmith at /personal/bobsmith. Deleting the Site Collection in this manner, will result in the root Site Collection being deleted.
- It is important to note that by default the SharePoint System account is the only account that can perform the delete action.
In order to get the Site Collection back, you need to do the following:
- Browse Central Administration | Application Management
- Click Create Site Collection
- Select the My Site Web Application
- Set the Title to My Site
- For URL, select the "/" option for the root
- Select the My Site Host site template on the Enterprise tab
- Set the SharePoint System account as the owner.
- Click OK
If you need to delete an individual's My Site, you need to use the Delete Site Collection option in Central Admin | Application Management, or the STSAdm.exe command-line (stsadm -o deletesite -url http://mysite/personal/bobsmith).
I apologize for the delay between blogs. Switched jobs within Microsoft last December and moved from Texas to Southern California as part of that transfer. Needless to say, I've been insanely busy :-)
Anyway...this was an interesting question I received a couple months back and have been meaning to blog about it. The problem: How can you check if a Word document is password protected when uploading it to an ASP.Net based application?
This may not seem that complicated...you could upload the file to the server, then load the Word Object Model on the server and check some properties. Not so fast there! The problem with this approach is that leveraging the Office Object Model in a service based process [i.e. non-interactive] is not supported. Someone wrote a Knowledge Base article on this, and if you call in to support for help with a hanging ASP.Net application and you are automating Office, this will likely be the link you will receive in the email explaining what's going on:
257757 Considerations for server-side Automation of Office
http://support.microsoft.com/default.aspx?scid=kb;EN-US;257757
Without being able to load the Office DLL's into the ASP.Net application pool, this leaves you with running some code on the client to perform the check. I came up with a sample that leverages JavaScript, a .Net Winform control running in the browser, and a File Upload control to do this work. The idea was to only leverage the .Net Winform control to check if the file was password protected. The sample also shows how to handle events across the Winform control and JavaScript.
The general process is:
-
User types in or selects a file by clicking the Browse button generated by the File Upload HTML control
-
When the focus moves away from this control, JavaScript runs that sets a public property on the WinForm control to the FileName the person is uploading.
-
When the user clicks Submit, which is the WinForm control, code runs to open the Word Document to determine that the file is password protected. If the file is not password protected, the OnSubmitClick event is fired.
-
There is JavaScript in the ASPX page that listens for the OnSubmitClick event and submits the form when this fires. This posts the file to the server.
The code is a sample, there could be more checks to ensure that Word is installed on the box and ensuring that they are selecting a supported file extension, but the idea is the same. The Submit button or file upload control could use some look-and-feel changes to make them look the same. There are two projects in the attached Zip file:
The only downside to having to run the .Net code on the client is client-side permissions. By default, code running in the browser runs in one of 3 Code Access Security (CAS) groups that have varying levels of restriction (LocalIntranet, TrustedSites, and Internet). None of which give access that you need to do what you want. If the URL does not contain periods [typically only in an Intranet environment], give the LocalIntranet zone FullTrust to have your code run or you can create a custom security code group to fully trust your specific URL. If the URL has periods, it will run in the InternetZone by default. Here are the steps in case you are unfamiliar:
1. On the client machine
2. Open Administrative Tools
3. Open the latest .Net Configuration (1.1 or 2.0 Framework)
4. Expand My Computer | Runtime Security Policy | Machine | Code Groups
5. Right-click All_Code, select New…
6. Fill in the Name and click Next
7. From the “Condition type” dropdown, select URL
8. For the URL, type in the path to the directory hosting the ASPX and DLL: http://web01/myapp/*
9. Click Next and use Full Trust and finish the wizard
Good luck!
When you reference a Native C++ assembly from ASP.Net you may run into the following error:
System.IO.FileNotFoundException: The specified module could not be found.
(Exception from HRESULT: 0x8007007E)
[FileNotFoundException: The specified module could not be found. (Exception from HRESULT: 0x8007007E)]
System.Reflection.Assembly.nLoad(AssemblyName fileName, String codeBase, Evidence assemblySecurity, Assembly locationHint, StackCrawlMark& stackMark, Boolean throwOnFileNotFound, Boolean forIntrospection) +0
System.Reflection.Assembly.InternalLoad(AssemblyName assemblyRef, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection) +211
System.Reflection.Assembly.InternalLoad(String assemblyString, Evidence assemblySecurity, StackCrawlMark& stackMark, Boolean forIntrospection) +141
System.Reflection.Assembly.Load(String assemblyString) +25
System.Web.Configuration.CompilationSection.LoadAssemblyHelper(String assemblyName, Boolean starDirective) +32
[ConfigurationErrorsException: The specified module could not be found. (Exception from HRESULT: 0x8007007E)]
System.Web.Configuration.CompilationSection.LoadAssemblyHelper(String assemblyName, Boolean starDirective) +596
System.Web.Configuration.CompilationSection.LoadAllAssembliesFromAppDomainBinDirectory() +3591161
System.Web.Configuration.CompilationSection.LoadAssembly(AssemblyInfo ai) +46
System.Web.Compilation.BuildManager.GetReferencedAssemblies(CompilationSection compConfig) +177
System.Web.Compilation.BuildProvidersCompiler..ctor(VirtualPath configPath, Boolean supportLocalization, String outputAssemblyName) +180
System.Web.Compilation.ApplicationBuildProvider.GetGlobalAsaxBuildResult(Boolean isPrecompiledApp) +3558605
System.Web.Compilation.BuildManager.CompileGlobalAsax() +51
System.Web.Compilation.BuildManager.EnsureTopLevelFilesCompiled() +462
[HttpException (0x80004005): The specified module could not be found. (Exception from HRESULT: 0x8007007E)]
System.Web.Compilation.BuildManager.ReportTopLevelCompilationException() +57
System.Web.Compilation.BuildManager.EnsureTopLevelFilesCompiled() +612
System.Web.Hosting.HostingEnvironment.Initialize(ApplicationManager appManager, IApplicationHost appHost, IConfigMapPathFactory configMapPathFactory, HostingEnvironmentParameters hostingParameters) +642
[HttpException (0x80004005): The specified module could not be found. (Exception from HRESULT: 0x8007007E)]
System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +3539851
System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +69
System.Web.HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr) +252
The core cause to this problem is in the way the operating system loads native DLL's at runtime. Native DLL's are loaded using the following logic which does not include the Temporary ASP.net Files nor the applications /bin folder. This problem will also occur in any .Net application if the Native DLL is not included in the /bin folder with the .EXE file or if the DLL is not in the Path Environment Variable.
- The directory from which the application loaded. In the case of ASP.Net, this will resolve to %windir%\Microsoft.Net\Framework\v###\ or %windir%\system32\inetsrv for IIS 6.
- The current directory. In the case of ASP.Net, this will resolve to %windir%\System32\inetsrv for IIS 6. If using the built-in web server, this resolves to a path under C:\Program Files\Microsoft Visual Studio 8.
- The Windows system directory. Use the GetSystemDirectory function to get the path of this directory.
- The Windows directory. Use the GetWindowsDirectory function to get the path of this directory.
- The directories that are listed in the PATH environment variable.
============
The options:
============
- Use DLLImport to load the dll using a relative or absolute path at runtime.
- Set the PATH Environment Variable so the ASP.Net process can locate the C++ DLL. You can set this property at runtime so that it only affects the process running your code. You can also set this globally in the System Properties (Environment Variables | PATH property). Setting this programmatically does not require a reboot and you can point the PATH to the /bin folder of the web app if you want to be able to do XCopy deployments of your ASP.Net application. Here are the steps to set the Path programmatically from ASP.Net.
Option 2.a - If you want your Native C++ DLL’s loaded from the /bin of the ASP.Net application.
- Native C++ DLL project
- Use al.exe to build a NativeWrapper for the Native C++ DLL. This allows you to bring the Native C++ DLL along with the DLL that is referencing it. You can add this in the Post Build Script of the Native C++ DLL Project to automate this.
al.exe /link:"$(TargetPath)" /out:"$(TargetDir)$(TargetName).NW.dll" /platform:x86
- Managed C++ DLL Project
- Reference the NativeWrapper DLL - when you build this project, the native wrapper and Native C++ DLL files are copied to the output directory along with the managed C++ DLL
- Set Delay Load DLLs to the Native DLL. (C++ Project Properties, Expand Linker, select Input, Delay Loaded DLLs) - This prevents the Native DLL from loading when the process starts, giving you a chance to set the PATH Environment variable. If you don’t set this property, moci.net and its dependencies (i.e. the Native DLL) will be loaded before any of your ASP.Net code can run and this will fail unless you have set the PATH environment variable on the machine level.
- ASP.Net Project
- Reference the managed C++ DLL - The managed C++ DLL, Native C++ DLL, and NativeWrapper DLL are moved into the applications /bin folder.
- Add a Global.asax with the following code. (Right-click the Web Application, select Add, New Item, select Global Application Class). You could also use an HTTPModule compiled into a DLL if you don’t want people to be able to change your code. Application_Start runs one time when the application loads and this code will set the PATH environment variable for the process to include the /bin directory of the application.
protected void Application_Start(object sender, EventArgs e){
String _path = String.Concat(System.Environment.GetEnvironmentVariable("PATH"), ";", System.AppDomain.CurrentDomain.RelativeSearchPath);
System.Environment.SetEnvironmentVariable("PATH", _path, EnvironmentVariableTarget.Process);
}
Option 2.b - If you want your Native C++ DLLs to load from an installation path outside of the web site you can avoid the AL command. You would still need to set the Delay Load property on any Managed C++ DLL that loads the Native C++ DLL’s as well as set the Environment Variable. If you choose to go this route, you can load the path to your Native C++ DLL’s dynamically from the web.config file at runtime:
- Native C++ DLL Project - don’t need to do anything in the Post Build Script
- Managed C++ DLL Project
- Configure Delay Load DLL’s and specify the Native C++ DLL
- ASP.Net Project
- Reference the Managed C++ DLL - only Managed C++ DLL is in the /bin
- In web.config, add the following…use a path where the Native C++ DLL is located:
<appSettings>
<add key="NativePath" value="C:\MyNativeDLLs"/>
</appSettings>
- In global.asax, add the following:
protected void Application_Start(object sender, EventArgs e){
String _path = String.Concat(System.Environment.GetEnvironmentVariable("PATH"), ";", ConfigurationSettings.AppSettings["NativePath"]);
System.Environment.SetEnvironmentVariable("PATH", _path, EnvironmentVariableTarget.Process);
}
I was recently working with someone that was leveraging the AJAX Extension and the Control Toolkit and they were running into an issue where a customer's environment was causing issues making requests to ScriptResource.axd. Since they couldn't change the environment, they needed to reference the .js files directly. Here's a rundown of the steps in case anyone needs to do this in the future:
1. Create a folder to hold the .js files in your site. In my case I created a "Scripts" folder on the root of my app.
2. Create the following folder structure under the folder from Step 1. The folder that is a version number should match the version of the control toolkit. This can be found on the Releases page for the extension.
- <Root>
- Scripts
- AjaxControlToolkit
- 1.0.10301.0
3. Open the Control Toolkit project and build the solution. This should generate the \obj folder that contains all the .js files.
4. Copy the Script files for the toolkit project to the site
From: <path_to_unzipped_toolkit>\AjaxControlToolkit\obj\Debug
To: 1.0.10301.0 folder from above
5. Copy the Script files for the AJAX Extension:
From: Copy the System.Web.Extensions folder and all it's contents from C:\Program Files (x86)\Microsoft ASP.NET\ASP.NET 2.0 AJAX Extensions\v1.0.61025\MicrosoftAjaxLibrary
To: ~/Script folder. You should have a folder structure similar to:
- <Root>
- Scripts
- System.Web.Extensions
- 1.0.61025.0
6. Set the ScriptPath on the <scriptManager> to "~/Scripts"
7. Make sure the controls are not explicitly setting ScriptPath as that will override the ScriptManager property.
Now when you browse the page, the .js files will be loaded from the ~/Scripts folder instead of via calls to ScriptResource.axd. If you're getting script errors, take a netmon and ensure all the requests to the .js files are returning a status code of 200 or 304.
This was tricky to track down and hopefully the troubleshooting information will help you should you run into this type of error and can't track it down.
Symptom
When browsing a web application you receive the following JavaScript errors.
JavaScript errors:
Error 1:
Syntax Error
Error 2:
Object expected
If using 3rd party ASP.Net controls, you may see errors similar to the following:
ASP.Net Error related to 3rd party control
Unable to find script library '/aspnet_client/basicframe_webcontrols_basicdatepicker/1_1_1959/bdplite.js'. Try placing this file manually by uploading the /aspnet_client directory to your web application root.
Typically the aspnet_client directory can be found at the following path:
C:\inetpub\wwwroot\aspnet_client
Looking at the IIS logs will show that requests to these pages are serving correctly with a 304 or 200 response. Investigation of the .js file in the IE cache or network trace response will show you that there is additional text at the end of the .js files. For example, you have a .js file with the following:
function AlertMe()
{
alert('The JS File loaded');
}
However, in the netmon or IE cache, you see the following:
function AlertMe()
{
alert('The JS File loaded');
}<div>Confidential Information</div>
The extra information results in Internet Explorer throwing the JavaScript errors. BTW, the error doesn't occur in Firefox as it seems to be more tolerant of the invalid script.
Troubleshooting Steps
Check the IE cache to see if there is extra data appended to the .js file.
- Tools | Internet Options in IE
- General Tab | Settings button under Browsing History
- Click View Files
- Sort by Internet Address and find the URL to your site
- Do you see the .js file being requested? If so, open it in notepad and look for extra text at the end or any characters not in the original file on the server.
You can also use a tool that captures the network data (Netmon 3, Ethereal, Fiddler, Web Development Helper, HttpWatch, Firebug, etc). However, if IE is caching the page, you will see a request with the If-Modified-Since header coming from the client and the server will return a response with a 304 status code which will not contain the script. The If-Modified-Since header indicates the browser has the file in cache, and is asking the server if it has a newer copy. In order to avoid this behavior, you can force IE to always request the actual page instead of using cache. This way you can see the 200 response coming from the server with the contents of the .js file to see if anything extra is added to your script.
- Open IE
- From Tools menu, select Internet Options
- Click Settings under Browsing History (IE7)
- Click Every Time I Visit the Web Page, click OK
- Use network capture tool and browse the page.
Cause
In this particular scenario, the "Enable document footer" option in IIS was enabled on the folders serving the .js files. This option appends text to the end of any static documents served:
- Open IIS Manager
- Right-click the folder hosting the test pages, select Properties
- Click the Documents tab
- Uncheck "Enable document footer".
Anything in the pipeline could potentially cause this. Isapi filters, proxy servers, HttpModules/handlers(if .js is mapped to aspnet_isapi.dll), etc.
Issue - Random FileNotFoundException when browsing ASP.Net 2.0 application. An event similar to the following will be logged in the event viewer. The file name in the exception is typically different every time:
Exception message: Could not load file or assembly 'App_Web_-a8debde, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null' or one of its dependencies. The system cannot find the file specified.
Stack trace: at ASP.UserControl_ascx.__BuildControlmain()
at ASP.UserControl_ascx.__BuildControlTree(dynamic_default_ascx __ctrl)
at ASP.UserControl_ascx.FrameworkInitialize()
at System.Web.UI.UserControl.InitializeAsUserControlInternal()
at System.Web.UI.UserControl.InitializeAsUserControl(Page page)
at ASP.default_aspx.__BuildControlhtml_content()
at ASP.default_aspx.__BuildControlTree(default_aspx __ctrl)
at ASP.default_aspx.FrameworkInitialize()
at System.Web.UI.Page.ProcessRequest(Boolean includeStagesBeforeAsyncPoint, Boolean includeStagesAfterAsyncPoint)
at System.Web.UI.Page.ProcessRequest()
at System.Web.UI.Page.ProcessRequestWithNoAssert(HttpContext context)
at System.Web.UI.Page.ProcessRequest(HttpContext context)
at ASP.default_aspx.ProcessRequest(HttpContext context)
at System.Web.HttpApplication.CallHandlerExecutionStep.System.Web.HttpApplication.IExecutionStep.Execute()
at System.Web.HttpApplication.ExecuteStep(IExecutionStep step, Boolean& completedSynchronously)
Workarounds - In some cases, it's necessary to clear the Temporary ASP.Net Files folder and restart the application pool. We've found in some cases that setting batch="false" in the web.config file will resolve the issue as well, however, there are some perf reasons for not doing that long term in production.
Getting the Fix - The KB for this fix is not yet public (I'll link it as soon as it is), however, the fix is available. You will need to contact Microsoft Support (http://support.microsoft.com/) and ask for the fix from KB article 934839.
This sample was a result of an idea from Kent Post at Akamai. He was wanting to be able to launch a process on a remote server and get real-time updates in the browser. In this case, we focused on running WinPE scripts to build ISO's on the server. The WinPE scripts can take a long time to process, and the goal is to launch the process and see what was going on without having to be logged into the server console using the browser. The attached sample provides a way to perform these actions by using System.Diagnostics to launch the process and the AJAX Extension to get updates. Before getting into the sample, let's look at some of the complications of doing this type of work.
- Permissions on the server - By default, the application pool running ASP.Net cannot run other processes on the server. The process by default runs as Network Service which typically won't have Read access to the .bat, .cmd, .exe you're trying to run. If you're writing files, it won't have access to write to directories either.
- Getting the data back to the client - Once you launch the process, how do you get the data down to a remote browser? HTTP is a disconnected protocol, so once you spin up the process, you need some way of making additional requests and getting the output.
- Application can not be interactive - Making sure the application you're running does not have an interface that requires interaction. Keep in mind you are launching a child process from a non-interactive service and there is no way for a remote or local user to actually interact with the program. Batch files are good for this scenario.
The first obstacle is relatively easy to overcome. For the demo, you can use a file based web site in Visual Studio 2005 and the built in web server. In this scenario, the web server and code run as the account you are logged into the box with and assuming this account has appropriate permissions to launch the program and write to any area that the program writes to, the app will work fine. From a server perspective, we ran the Application Pool as Local System. You can setup IIS with a new application pool running under local system and assign the application pool to the specific application to ensure other code is not running with elevated privileges. Another option is to use PInvoke and call CreateProcessAsUser and explicitly set desktop permissions for the application pool account you're using. This is a bit more complicated and won't be covered here. If you're interested in looking into that approach for launching processes, check out Q165194
165194 - INFO: CreateProcessAsUser() Windowstations and Desktops
http://support.microsoft.com/support/kb/articles/Q165/1/94.asp
The second problem of getting the output has a couple of options. One way to go is to pipe the output from the program to a text file and then redirect the browser to the textfile. If you haven't done this before, the following pipes the output of the dir command to the file c:\output.log: dir >c:\ouptut.log This gets you the output after the application has completed execution. However, in order to get the data as it's occurring, you have to make multiple requests to the server and access the StandardOutput stream directly. In this sample, we use the AJAX Extension, the setInterval method in JavaScript, the System.Diagnostics.Process class, and a custom class that's cached on the server to handle this scenario.
Overview of the sample
The attached files provide all the source to run the sample. Here's a rundown of what the app does and what is in the .zip files.
- User browses Default.aspx - If the user does not have a cookie called MyGUID, one is created for them. The cookie value is used to uniquely identify the requests when making web service calls and for caching.
- User clicks the Launch button
- JavaScript posts to the LaunchProcess method of the ProcessLauncher.asmx web service using the AJAX Extension
- LaunchProcess method does the following:
- Creates an instance of the ProgramWatcher class
- Calls the Execute method of ProgramWatcher
- Execute method launches a ThreadPool thread
- Runs the ReadLog.exe program
- Redirects the StandardOutput of the ReadLog.exe program to a Stream property of the ProgramWatcher instance
- The ProgramWatcher instance is cached using the MyGUID cookie value from Step 1
- When the Async call returns, a timer kicks off that runs the GetStatus JavaScript method after 750 milliseconds.
- GetStatus JavaScript method runs
- Timer is stopped to prevent async callbacks from getting out of sync. i.e. IE can make 2 calls per server, this prevents 2 requests leaving the client and the 2nd request returning before the first request and messing up the output.
- GetStatus method of the ProcessLauncher webservice is called
- GetStatus loads the ProgramWatcher method out of cache using the MyGUID cookie value
- Calls the ReadOutput method of the ProgramWatcher instance passing it how many bytes to read from the Stream property on ProgramWatcher - in this sample 50 bytes at a time.
- Returns the bytes returned from ReadOutput
- Client parses the response and handles output that contains \r vs. \r\n. See the Issues section below for why this is important.
- Timer started again and Steps 4 - 5 occur again until the GetStatus method returns Null
Included Files
Here's a list of the files included in the .zip files with a summary of what they do. The code is commented thoroughly.
- AjaxBatchProcess.zip
- App_Code
- ProgramWatcher.cs - This class is what drives the process on the server side. It contains an Execute method that launches a program via System.Diagnostics.Process and redirects the StandardOutput to a property on the class. The class is cached so that the browser can get the output from the process in chunks.
- ProcessLauncher.cs - Web Service methods that allow the AJAX Extension to generate JavaScript proxy classes that are used to launch the process on the web server and also get the output in chunks.
- AjaxBatchProcess.js - JavaScript file that calls the webservice methods and also handles getting and parsing the output.
- Default.aspx - Page requested by an end user. Has a ScriptManager that hooks up AjaxBatchProcess.js and ProcessLauncher.asmx
- ProcessLauncher.asmx - Web Service requested by the client.
- Web.config - Web.config from the AJAX Extension project
- ReadLog.zip
- ReadLog.exe - Command-line program that reads the build_pe_out.txt file and pushes the output to the console.
- build_pe_out.txt - Output from WinPE commands.
- ReadLog-Source.zip
- Program.cs - source for the ReadLog program. This program reads in a file passed in as an argument and pushes the text to the console.
- Default C# console project files
Here are the steps to use the files attached to the blog:
- Unzip AjaxBatchProcess.zip to a folder in your file system.
- Unzip ReadLog.zip to c:\ReadLog
- Open Visual Studio 2005
- From the File menu | Select Open Web Site
- Select the path from Step 1 (the folder that contains app_code, default.aspx, etc. from the .zip file)
- Right-click Default.aspx, select View in Browser
- Click the Launch button
Issues
I wanted to highlight a few areas and why we chose to go that route.
Caching the ProgramWatcher and the Stream property
This is an important part of the application and is the piece that allows access to the output from the application in real-time. In this case you have a program that is redirecting the StandardOutput to a Stream. This is essentially writing bytes into memory with a pointer at the end.
These are bytes<StandardOutput Pointer>
The ProgramWatcher class sets the StandardOutput stream to a property called Stream. This gives you another pointer to the same bytes that starts at the beginning.
<ProgramWatcher.Stream pointer>These are bytes<StandardOutput Pointer>
With this setup, you have the System.Diagnostics.Process adding bytes to the end of the stream via StandardOutput and ProgramWatcher.Stream reading the same bytes with a pointer that starts at the beginning. The ProgramWatcher instance is cached so that when you Read bytes off of ProgramWatcher.Stream, you are incrementing the pointer.
These are <ProgramWatcher.Stream pointer> bytes. This is additional data <StandardOutput Pointer>
When the GetStatus method is called again, you are accessing the same instance of the class, so the pointer is where you left it. I was making this way to hard, so thanks to Todd Carter for helping clear this up.
Parsing the \r and \r\n in the output returned by GetStatus
This is important as console applications handle text ending in \r different than \r\n. In the case of WinPE, you get output showing the percentage complete. The text looks like:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package\r\n[============== 25.0% ]\r[===========================50.0% ]\r[============================75.0%=========== ]\r
[==========================100.0%==========================]\r\nMore Text
In the console, this renders as the following when it is complete:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package
More Text
However, while the process is running, you would see the WinPE-FontSupport-JA-JP-Package line followed by the percentage. When the next line ending in \r is written to the console, the previous line is replaced with the updated percentage. The problem is that the browser doesn't handle \r and \r\n the same way. So you would end up with the following:
|6.0.6001.16464 | + | WinPE-FontSupport-JA-JP-Package
[============== 25.0% ]
[===========================50.0% ]
[============================75.0%=========== ]
[==========================100.0%==========================]
More Text
Not real pretty. In the sample, I parse the output and if the output ends in \r, I put the information in the TextBox. If the text ends in \r\n, I clear the textbox and add the line to the TextArea. This gets pretty close to the same output you see in a console and avoids a lot of noise and clutter in the output.
Hopefully this sample and the information provided will help anyone out there trying to solve similar issues.
This is my third mobile post this week! This one was tricky to track down so hopefully this will save someone some time. The symptom you see is that users get the following exception. If you're using ASP.NET 2.0, we would log this to the Event Viewer by default thanks to Web Events:
|
Exception of type 'System.Web.HttpUnhandledException' was thrown. --->
System.Exception: The page requires session state that is no longer available. Either the session has expired, the client did not send a valid session cookie, or the session state history size is too small. Try increasing the history size or session expiry limit. |
This error would appear randomly throughout the day in a high throughput site. We were able to track this down using the IIS logs, Network Monitor, and in this case, the exceptions logged in the Event Viewer. The event logging in 2.0 captures a lot of information that is useful for lining up the exception with data gathered. In this instance, we used the client IP linked to the request and then filtered the Network Monitor logs using this IP. This allowed me to track the user's requests to see how they were using the site at the time of the failure.
In this case, we could see the client making requests for a few minutes, then their traffic would go away for longer than the session timeout. Their next request would come in for a page that they weren't browsing prior and would cause the exception. Looking at this data, we determined that users were bookmarking the site on their mobile devices. While this normally isn't a big deal, there are some devices that result in an __viewstate field in the QueryString. When you bookmark this URL, the initial request is now going to cause the mobile page to lookup the viewstate which is stored in Session for mobile forms. Since this is the first request, the session is just now being initialized, the ViewState isn't available, and the exception is thrown.
You can avoid this problem by adding the following code to Session_Start in the global.asax or a HttpModule. You can even get fancier and add some code to only remove the __viewstate QueryString if your pages are looking for other items in the QueryString collection. This code will ensure that when a new session is being created, that the __viewstate QueryString is not part of that request.
|
void Session_Start(object sender, EventArgs e) { // Code that runs when a new session is started if (HttpContext.Current.Request.QueryString.Count > 0) HttpContext.Current.Response.Redirect(HttpContext.Current.Request.Url.LocalPath.ToString()); }
|
You could also track this down by logging the QueryString in the IIS logs and looking for __viewstate in that field. The exception logged in the Event Viewer in 2.0 also log the UTC time to make it easy to line up the IIS entries which use UTC by default with the exception.
When you make a request to an ASP.NET based site, the browser's capabilities are determined by the <browserCaps> section in the 1.x Framework and the .browser files in the 2.0 framework. Knowing which capabilites that ASP.NET is using to render the page can be especially important in mobile applications.
You can use the following page to return all the MobileCapabilities for the device by browsing the page:
|
<% @ Page language="c#" AutoEventWireup="true" Inherits="System.Web.UI.MobileControls.MobilePage" %> <%@ Import namespace="System.Web.Mobile" %> <%@ Import namespace="System.Reflection" %> <script runat="server"> protected void Page_Load(Object sender, EventArgs e) { MobileCapabilities capabilities = (MobileCapabilities)Request.Browser; Type t = typeof(MobileCapabilities); PropertyInfo[] propertyInfos = t.GetProperties(); foreach(PropertyInfo pi in propertyInfos) { System.Web.UI.MobileControls.Label lbl = new System.Web.UI.MobileControls.Label(); lbl.Text = pi.Name + " = " + capabilities[pi.Name]; Form1.Controls.Add(lbl); } } </script> <mobile:Form id=Form1 runat="server"> </mobile:Form> |
The MobileCapabilities are determined by the UserAgent string that ASP.NET receives. You can log the UserAgent string in the IIS logs or you can use a tool like Fiddler or Web Development Helper in IE to get this information. Once you have the UserAgent string, you can use Fiddler to pass the same UserAgent string while browsing in IE. This will give you a larger viewing area for looking at hte list of MobileCapabilities.
When developing Mobile ASP.NET apps, it is useful to test the application across a variety of devices if the site is going to be posted to the internet. ASP.NET Mobile controls rely on adapters to handle rendering on the different devices. In order to hit the different adapters to ensure your page will work across the devices, you can use emulators. While troubleshooting mobile issues, I typically use the following emulators:
- HTML3.2 - I use Internet Explorer or one of the Pocket PC/Smartphone emulators that ship with Visual Studio. The default rendering for these devices is HTML 3.2 and as a result, the mobile controls will use the HTML32 adapters to render.
- WML - I use the OpenWave 7 emulator that comes as part of the OpenWave 7 SDK. ASP.NET picks up this device with a default rendering of WML allowing the mobile controls to use the WML adapters for rendering.
- XHTML - I use the OpenWave 6.2.2 emulator. The default rendering for this device is XHTML.
You can also use the <browserCaps> section in machine.config (1.x Framework) or .browser files (2.x Framework) to force the preferredRenderingType property of a device, however, I have found that using the emulators is quicker for testing against.
OpenWave emulators can be found here: http://developer.openwave.com/dvl/tools_and_sdk/phone_simulator/
The Pocket PC/Smartphone emulators are available on the Tools menu, under the Connect to Device option.
If you have a specific device and ASP.NET is not rendering the right content; i.e. your device supports JavaScript, but you're not getting JavaScript; you can use the Device Profiling Tool to generate a profile of the device and generate a <browserCaps> section (1.x) or a .browser file(2.0).
If you need to build a section for <browserCaps>, you can do this online here:
http://www.asp.net/mobile/profile/default.aspx
If you need to generate a .browser file you can download the device profiler and run it locally:
http://www.asp.net/sandbox/app_devprof.aspx?tabid=62
You can find links to training and mobile ASP.NET development here: http://www.asp.net/default.aspx?tabIndex=6&tabId=44
I was recently working with JavaScript and ran into some weird things I hadn't ran into before. While I'm sure the JavaScript masters of the world already know all about this stuff, hopefully this post can keep others from pulling out too much hair. The first one deals with scoping:
If you don’t explicitly var an object, that object is declared globally. The code will typically not fail, however, it will give you odd behavior. This becomes an issue if you have the following:
function Loop1(){
for(x = 0; x < 10; x++){
Loop2();
alert(x);
}
}
function Loop2(){
for(x = 0; x < 10; x++){
//do work here
}
}
You would expect the alert to occur 10 times, however, since the x declared in the first for loop does not have var in front of it, the variable is global. When Loop2 is called, the loop increments x to 10 causing the for loop in Loop1 to only occur 1 time. To correct this code, change the for loops to:
for(var x = 0; x < 10; x++)
The other issue deals with naming of objects and functions. If you're developing everything it's no big deal as you will typically know what the ID's will be on the client as well as the names of JavaScript functions, but if you're leveraging someone's JavaScript library, this can become more interesting. The error you get in Internet Explorer is "Object doesn't support this property or method". The following line will reproduce the behavior, but imagine if you had a page with 50 controls and the function name that matches is in a JavaScript file you didn't write:
<input type="button" id="AbortCallback" value="Abort" onclick="AbortCallback()" />
Trying to catch back up on my blogging, I have a list of topics to post on, just haven't had time to get to them. I wrote this article over a month ago...figured I would link it up from here.
The default web part catalogs that are shipped with ASP.Net require you to configure the list of web parts you want to display in your catalog. We've had a few requests for a more dynamic approach and the article has a sample that can get you started on this.
http://support.microsoft.com/kb/910446
The article links up to a sample Solution and lays out the files and what they do. The main piece is a custom web part catalog that uses reflection to populate the list of web parts. The idea is that you drop a .dll into the /bin folder of your app and the catalog will automatically pick out the web part controls in the dll. It caches the list of parts to avoid the cost of looking up the dll's on future requests. This works great since adding a dll to the /bin will recycle the app domain and clear the cache, so all you need to do to add web parts to the catalog is to add the dll to the /bin.
The other piece is a custom WebPart that pulls data from an RSS feed and shows the different types of properties you can implement in the web part.
We've seen a few of these issues come through lately andwanted to get something posted so people can find it.
If you created a custom account to run the process that ASP.NET is running in, you may encounter the following exception:
|
System.InvalidOperationException: Mutex could not be created.
Stack Trace: [InvalidOperationException: Mutex could not be created.] System.Web.Compilation.CompilationMutex..ctor(String name, String comment) +3464689 System.Web.Compilation.CompilationLock..cctor() +158
[TypeInitializationException: The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception.] System.Web.Compilation.CompilationLock.GetLock(Boolean& gotLock) +32 System.Web.Compilation.BuildManager.EnsureTopLevelFilesCompiled() +114
[HttpException (0x80004005): The type initializer for 'System.Web.Compilation.CompilationLock' threw an exception.] System.Web.HttpRuntime.FirstRequestInit(HttpContext context) +3426871 System.Web.HttpRuntime.EnsureFirstRequestInit(HttpContext context) +88 System.Web.HttpRuntime.ProcessRequestInternal(HttpWorkerRequest wr) +149 -------------------------------------------------------------------------------- Version Information: Microsoft .NET Framework Version:2.0.50727.42; ASP.NET Version:2.0.50727.42 |
Note: The error will appear to be random, but once it appears, will remain until a reboot or until the handles to the mutex are closed.
Cause:
======
This occurs because the permissions on the following registry key no longer have your custom account:
HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\ASP.NET\2.0.50727.0\CompilationMutexName
The compilation mutex gets it's permissions from this registry key and with your custom account missing, the process running ASP.NET cannot get a handle to the mutex and fails during compilation.
Resolution:
=========
- Run the following to add your custom account to the registry key permissions:
ASPNET_regiis -ga domain\account.
- Either restart the server or close the handles to the mutex (see below)
To close the handles to the Mutex:
- Download and launch Process Explorer from www.sysinternals.com
- From the Find menu, select Find Handle or DLL
- In the Handle or DLL substring box, type “mutant” (without quotes) and click Search
- Click the Handle or DLL column heading to sort the items and find a mutex that starts with CL. The handle will typically be devenv.exe (the IDE for Visual Studio) and aspnet_wp.exe or w3wp.exe. The handle will look similar to:
- \BaseNameObjects\CLbdd6aa8f
- Select the handle in the search box and Process Explorer locates the Process and the handle in the main window
- Right-click the handle in the main window and select Close Handle
Additional Info:
============
The error may occur anytime you run ASP.NET as a custom account and the above mentioned registry key does not have the account listed in the permissions.
If you receive the error and you are running with a custom account on IIS 6 in worker process mode (code running in w3wp.exe), you can add your custom account to the IIS_WPG group on the server. The IIS_WPG group is granted access to the registry key by default.