.NET Forum / Languages / C# / January 2008
Riddle Me This
|
|
Thread rating:  |
pbd22 - 11 Jan 2008 02:51 GMT OK.
Can somebody explain why a windows service, when compiled in Debug mode, has no problem starting an executable program in one of its methods and the exe does what it is supposed to.
But, when the service is installed using installutil the exe fails.
Why might that be??
Thanks.
Michael C - 11 Jan 2008 03:03 GMT > OK. > [quoted text clipped - 7 lines] > > Why might that be?? Not really sure but I don't think a service should start an exe. What happens if no-one is logged in? What happens if 2 people are logged in? What account does the exe run under?
Michael
pbd22 - 11 Jan 2008 03:14 GMT LocalSystem.
It does start this exe in Debug mode - quite nicely, actually. Not sure what you mean by "nobody logged in". It starts automatically at boot.
I put the exe in C:/bin
It is a VB6 project called by a C# service.
Michael C - 11 Jan 2008 03:21 GMT > LocalSystem. > > It does start this exe in Debug mode - quite nicely, actually. > Not sure what you mean by "nobody logged in". A service can run when no-one is logged in, how is it going to run the exe when there is no desktop. When 2 people are logged in which person's desktop is it going to go to?
> It starts automatically at boot. That's right *before* anyone logs in.
Michael
pbd22 - 11 Jan 2008 03:43 GMT isn't this the whole idea behind MTA?
Michael C - 11 Jan 2008 04:49 GMT > isn't this the whole idea behind MTA? Microsoft Message transfer agent? :-)
Michael
Michael C - 11 Jan 2008 04:51 GMT > isn't this the whole idea behind MTA? You mean threading? What's that got to do with it?
pbd22 - 11 Jan 2008 04:58 GMT Is this right? Do I need to give my service Admin or some privledge more powerful than LocalService to successfully use an external exe?
Family Tree Mike - 11 Jan 2008 12:22 GMT Or give execute rights to the LocalService account on your external exe and any file paths that it uses. All of this assumes there is no desktop interaction by the external application.
> Is this right? Do I need to give my service Admin or some privledge > more powerful > than LocalService to successfully use an external exe? pbd22 - 11 Jan 2008 14:56 GMT Thank everybody for your suggestions.
The purpose of the EXE is to open a file, read its metadata, create a text file, move the metadata to the text file, close the file, exit.
So, it sounds to me like this very much uses the desktop!
I have tried clicking the interact with desktop option on the Logon tab in services >> my service >> poperties but this didn't change anything.
The EXE is, of course, STA. It originally had a UI but this bit of code does not assume UI interaction in any way. It does what I said it does above when it finds a file in the folder I point it to.
OK, I am going to try your suggestions (thanks). And, please let me know if my info here further reveals the solution.
Regards, Peter
Michael C - 13 Jan 2008 23:00 GMT > Thank everybody for your suggestions. > [quoted text clipped - 3 lines] > > So, it sounds to me like this very much uses the desktop! No, that doesn't sound like it uses the desktop. You could compile it as a dll and reference it in the usual way.
Michael
Michael D. Ober - 11 Jan 2008 05:12 GMT > LocalSystem. > [quoted text clipped - 5 lines] > > It is a VB6 project called by a C# service. Does the Debug version work as a service? Also, since most VB6 programs require a user interface, how do you handle the fact that there is no UI for a service?
Mike.
pbd22 - 11 Jan 2008 05:45 GMT > Does the Debug version work as a service? Yes, swimmingly.
> Also, since most VB6 programs > require a user interface, how do you handle the fact that there is no UI for > a service? I click my heels and say "there is no place like home"?
Well, this was the thinking behind the exe in the first place. We wanted the DLL to be called from the service but the COM object is STA and the service (C#) is MTA and we couldnt come up with a reliable way to handle the differences. Declaring the service as [STAThread] did not do the trick nor did other programmatic tricks.
So, this is what we are doing.
I am starting to come into Michael C's camp - it makes sense that the LocalSystem permissions are less powerful than my user account and therefore I am having access problems.
I tried setting my account to "User" and the username/pass to my windows login.
But, now, when I try to start the service, it tells me I couldn't login.
Question, must I set the windows account programmatically in the service and in the properties >> login tab in Services???
thanks.
Tobias Schröer - 11 Jan 2008 08:39 GMT pbd22 schrieb:
> So, this is what we are doing. > [quoted text clipped - 3 lines] > having access > problems. Does your exe has any UI? I guess not, reading the other posts. Does your exe access other resources than local resources (e.g. network)? That would not be permitted to Local System.
> I tried setting my account to "User" and the username/pass to my > windows login. > > But, now, when I try to start the service, it tells me I couldn't > login. Your account needs the "login as service" privilege (local security settings).
HTH, Tobi
Michael C - 13 Jan 2008 23:41 GMT > Well, this was the thinking behind the exe in the first place. > We wanted the DLL to be called from the service but the COM object > is STA and the service (C#) is MTA and we couldnt come up with a > reliable way to handle the differences. Declaring the service as > [STAThread] did not do the trick nor did other programmatic tricks. That answers my question above. I'm no expert on threading, perhaps someone can suggest a solution.
But, why does this dll need to be in VB6, it doesn't sound like it's all that complicated and being that it has no GUI wouldn't it be easy to translate to C#? Or if you need it to work from vb6 and c# you could write it in C++ and wack it in a dll.
Michael
pbd22 - 14 Jan 2008 01:10 GMT Thanks Mike.
Well, if you want to go to the core, the whole problem started with how VB6 and C# handle events. IN the VB6 project, we had:
Dim WithEvents meta As VB6DLL
which was simple enough.
When I tried translating this bit of code into my C# Windows Service I started to realize how different threading is in VB6 and the .NET CLR. We couldn't make events that happened in the VB6 project, happen in the C# windows service.
So, we stuck with the VB6 exe and are now calling it as an external process. Ugly, but it I feel reasonably close to a solution here vs. revisiting dusty computer science books to wrap my mind around the threading differences.
I like your idea about turning this VB6 project into a C# (or C++) Class LIbrary. But I will, however, still need to call a VB6 DLL for event handling. So, I am wondering if I am going to run into the same threading (STA/MTA) problem I had when we tried to do this before. The core of this problem is a VB6 eventhandler translated into a C# (or C++) project. I don't know enough about DLL creation, but it sounds to me that the problem should be the same (whether its a .NET DLL or .NET windows service).
I am all ears if somebody has a clear understanding of the VB6/.NET CLR threading differences and how to translate VB6 event handling into my .NET project. But, because I am already out of time and feel like we are very close with the external exe, I think i'd prefer to follow the exe road unless somebody out there can provide an excellent threading solution (with clear examples that are directly related to my project).
Again, as always, I really appreciate everybody's help!
Peter
Michael C - 14 Jan 2008 02:10 GMT > When I tried translating this bit of code into my C# Windows Service > I started to realize how different threading is in VB6 and the .NET > CLR. > We couldn't make events that happened in the VB6 project, happen in > the C# > windows service. I'm not sure if I follow you 100% here but I'm presuming you're talking about problems with C# interacting with VB6? My question was is it possible to translate ALL code to C#.
Regards, Michael
pbd22 - 14 Jan 2008 03:23 GMT Sorry Mike,
I thought you were talking about the exe, but I now realize you were talking about the exe and the DLL it references. It is absolutely possible to move the EXE to C# - it is only a Form_Load and two supporting subroutines. The DLL it references is some insane DLL in C (not C++) that we hired a contractor for. I think it makes extensive use of DirectShow. I know very little about it, only that it does what it is supposed to do.
Peter
Michael C - 14 Jan 2008 04:17 GMT > Sorry Mike, > [quoted text clipped - 7 lines] > very little about it, only that it does what it is supposed > to do. So let me get this straight, you've got a service in C# that is calling an exe written in VB6 than references a dll written in C that references directShow a lot. Is that about right? Is the dll written in C a com dll or a standard dll.
Michael
pbd22 - 14 Jan 2008 04:41 GMT > > Sorry Mike, > [quoted text clipped - 14 lines] > > Michael Yes, that is all correct. The DLL is COM.
Marc Gravell - 14 Jan 2008 08:17 GMT Yikes; good luck getting that lot to play ball together... I'm not at all convinced that using DirectShow in a service is a very good idea... and all those layers? It may be time to consider calling into managed DirectX instead... but even then I'm not sure that a service is going to cut it... remember that the destop integration is disabled from Vista etc...
Marc
Willy Denoyette [MVP] - 14 Jan 2008 10:11 GMT >> > Sorry Mike, >> [quoted text clipped - 19 lines] > Yes, that is all correct. > The DLL is COM. Your VB6 application runs in the same context of the Windows Service, services should never assume an interactive logon session to be present and should never interact with the desktop, but this is exactly what you are expecting. VB6 applications and the COM components used by this application aren't designed to be used in such context. You will never get it right.
Willy.
pbd22 - 14 Jan 2008 14:38 GMT Willy,
I sent you relevant code to your email over the weekend. Please tell me what I have to do to "get it right". This has been a royal headache and I am really stumped.
Peter
pbd22 - 14 Jan 2008 14:42 GMT What really stumps me is that (remember) this melting pot of varying technologies in my window service """"works"""" in Debug mode from the IDE. I compile, it runs - external exe and all. But, when I use installutil and make it an proper windows service, the external exe breaks. This feels like a security problem but that is just a gut hunch.
Thanks again.
Willy Denoyette [MVP] - 14 Jan 2008 17:20 GMT > Willy, > [quoted text clipped - 3 lines] > > Peter Never got this email.
Anyway, what you should do is, ask yourself whether this must be driven by a service, if the answer is YES then you should get rid of all features/technologies that really aren't designed to be used from a service. Basically you have three possible options, but be warned, none of them are trivial. 1) The most important thing is, that you should get rid of the VB exe, it doesn't solve anything, if you can successfully access the COM interfaces (implemented in the C/C++ DLL's) from VB6, then they should be directly accessible from a WS as well. Keep in mind, that the COM components (the DLL's that call into DirectShow) must be accessed from an STA thread, and that STA threads *must* pump a message queue, that is, they should not block. This is especially true running managed code, blocking STA's may block the finalizer thread, and this is a big NO NO in managed code. So, your service thread you should fire off a thread that gets initialized to enter a STA, this thread should run a message queue, that is, you should create an empty form, create an instance of the COM object(s) and call Application.Run(). The service thread could (should) use Windows messages to pass command to the STA/pumping thread. That means that you must define and register private Windows message ID's for each command that corresponds to a COM method call (you could also define one single message and pass the command as message parameter), you must handle these messages in your WndProc override. To communicate back with the service thread, you can use a SynchronizationContext, the service thread must register a delegate to get called when an event is fired by the pumping STA thread. The STA pumping thread must implement the callbacks required by the COM object(s).
2) Get rid of the VB6 exe and of the C/C++ DLL's. This requires a rewrite of the code in the C dll's in C#. The CS code must declare/implement the COM interfaces and call into DirectShow just like was done in the C/C++ code. Note that you can also call into the Media Format API's, but it everything depends on what exactly you are doing in the C/C++ COM dll's.
3) Write a C++/CLI wrapper that wraps the existing COM dll's, note that this wrapper must run his own message queue exactly like explained in 1).
Willy.
pbd22 - 14 Jan 2008 18:51 GMT Thanks Willy.
"not trivial" indeed. Non-the-less, I am all for a new learning experience. I am going to try to open door number 1 and see where that gets me. In preparation, do you know of any good and related online examples that can give me a sense of things? If not, no worries. I am "googling".
Thanks again, Peter
DeveloperX - 11 Jan 2008 08:57 GMT On 11 Jan, 05:12, "Michael D. Ober" <obermd.@.alum.mit.edu.nospam.> wrote:
> > LocalSystem. > [quoted text clipped - 11 lines] > > Mike. No they don't. Project properties -> Start up object -> Sub main.
What does the exe do? One handy trick is to import ExitProcess
Private Declare Sub ExitProcess Lib "kernel32" (ByVal uExitCode As Long)
and then return a meaningful return code depending on where the exe fails. At the very least it will tell you whether the exe manages to start at all. I'm going with rights though. As you mention below, go into services and specify your fully qualified account details (to test) and then try and start it.
christery@gmail.com - 11 Jan 2008 09:12 GMT An VB6 app that wants to be run as an service with (click in checkbox for can interact with desktop) or without UI should (must?) use ntsvc.ocx or whatever its called, search on the net for that little tease ;) //CY
pbd22 - 11 Jan 2008 15:08 GMT DeveloperX,
> Private Declare Sub ExitProcess Lib "kernel32" (ByVal uExitCode As > Long) This may sound dull, but I am assuming that this line of code belongs in the EXE itself, and not in the service?
Peter
pbd22 - 11 Jan 2008 15:23 GMT DeveloperX,
Another question,
At what point should I be firing ExitProcess? There is no FormUnload as there is no form or, for that matter, any user interaction.
Can you provide a little more direction (or code?).
Thanks.
pbd22 - 11 Jan 2008 16:22 GMT OK.
Still not working.
I am thinking this HAS to be a permissions error and I am just not putting my ducks in a row.
The Debug version works no prob, the service does not.
Willy Denoyette [MVP] - 11 Jan 2008 16:54 GMT > OK. > [quoted text clipped - 4 lines] > > The Debug version works no prob, the service does not. Why do you call one a "Debug version" and the other a "service"? You have a debug version and a release version or your service, but both are services, right? Does your debug version works as expected when installed using installutil?
Willy.
pbd22 - 11 Jan 2008 17:01 GMT Willy -
Sorry for the semantic misfire.
I mean, the service works in Debug mode. When I put it in Release mode and install it using the installutil and then start it in services, it fails.
Works in the IDE, fails when started from "Services".
I have not tried to install the debug version with installutil, that didn't occur to me. I'll try that now.
Peter
pbd22 - 11 Jan 2008 17:01 GMT Willy -
Sorry for the semantic misfire.
I mean, the service works in Debug mode. When I put it in Release mode and install it using the installutil and then start it in services, it fails.
Works in the IDE, fails when started from "Services".
I have not tried to install the debug version with installutil, that didn't occur to me. I'll try that now.
Peter
pbd22 - 11 Jan 2008 17:54 GMT OK.
Debug and Release do not work after installutil from the Services window. Debug works from the IDE.
The service is Local System account in Services view. The service does not have the "Allow service to interact with desktop" checked.
The ProjectInstaller.cs file has the following code:
this.serviceProcessInstaller1.Account = System.ServiceProcess.ServiceAccount.LocalSystem; this.serviceProcessInstaller1.Password = null; this.serviceProcessInstaller1.Username = null;
The properties of the EXE file are:
Administrators LOCAL SERVICE (My Login) Power Users SERVICE SYSTEM Users
The containing folder's Properties are: Administrators CREATOR OWNER LOCAL SERVICE (My Login) SYSTEM Users
Everything has been granted "Full Control" for tire-kicking.
Debug efforts:
I have set the following in my Service1.cs file:
InitializeComponent();
if (! System.Diagnostics.EventLog.SourceExists("MyService")) { System.Diagnostics.EventLog.CreateEventSource( "MyService", "MyLog"); }
eventLog1.Source = "MyService"; eventLog1.Log = "MyLog";
And, of course, the following try/catch block where appropriate:
catch (Exception e) { eventLog1.WriteEntry("Error with service:" + e.Message, EventLogEntryType.Error); return; }
Finally, I can see the process start and stop when running the code from the IDE. I CANNOT see the process start when running the service from windows (after installutil).
I appreciate your help. Peter
Willy Denoyette [MVP] - 11 Jan 2008 18:30 GMT > OK. > [quoted text clipped - 65 lines] > I appreciate your help. > Peter So, you have the service correctly registered, but when you try to start the service from the service applet (or from the command line using net start...), the service doesn't start at all. What does the eventlog tell you? And what do you have in your OnStart method. Are you sure you start a thread to run the service from within your OnStart or from within a method called from OnStart????
Willy.
pbd22 - 11 Jan 2008 18:55 GMT Thanks for your reply Willy.
Seems I am articulating myself poorly today.
> So, you have the service correctly registered, but when you try to start the > service from the service applet (or from the command line using net > start...), the service doesn't start at all. The service DOES START. In fact (annoyingly) the Event Log throws absolutely no errors. It starts, it runs, and it shuts down when i tell it to. I even wrote a custom Event Log that produces no information.
When I say "it doesn't work", I am talking about the exe.
For what it is worth, here is the OnStart method:
protected override void OnStart(string[] args) { ventLog1.WriteEntry("Service started successfully.", EventLogEntryType.Information); new Thread(new ThreadStart(InitService)).Start(); }
Willy Denoyette [MVP] - 11 Jan 2008 20:37 GMT > Thanks for your reply Willy. > [quoted text clipped - 21 lines] > new Thread(new ThreadStart(InitService)).Start(); > } Ok, so far so good ;-) What makes you think the exec is not started, is the process not visible in the process list? If not, how does the program signals success/failure to the parent process (the service in this case)? How do you start the exec, show us the code that start the exec. Give also some details about the exec, type of program, what security and user context does it require to run in, does it expect a user profile to be loaded?
Willy.
pbd22 - 11 Jan 2008 20:59 GMT > What makes you think the exec is not started, is the process not visible in > the process list? That is right.
> If not, how does the program signals success/failure to > the parent process (the service in this case)? Well, its a queue. There is no return value. We know the proccess takes about 2 seconds. So, it should run accordingly. If one is running when the other wants to run, it queues until the first one is done.
> How do you start the exec, show us the code that start the exec. Here is the code:
if (filetype = true) {
try {
ProcessStartInfo psi = new ProcessStartInfo(exepath, args);
lock (_objLock) { if (_fProcessRunning) { _procq.Enqueue(psi); psi = null; }
_fProcessRunning = true; }
if (psi != null) { StartAProcess(psi); }
} catch (Exception e) { eventLog1.WriteEntry("Error parsing file:" + e.Message, EventLogEntryType.Error); return; }
}
}
void StartAProcess(ProcessStartInfo psi) {
try {
Process process = new Process();
process.StartInfo = psi; process.EnableRaisingEvents = true;
process.Exited += ProcessExitedHandler;
process.Start(); } catch (Exception e) { eventLog1.WriteEntry("Error with StartAProcess method:" + e.Message, EventLogEntryType.Error); return; }
}
void ProcessExitedHandler(object sender, EventArgs e) {
ProcessStartInfo psi = null;
lock (_objLock) { if (_procq.Count > 0) { psi = _procq.Dequeue(); } else { _fProcessRunning = false; } }
if (psi != null) { StartAProcess(psi); }
}
> Give also some details about the exec, type of program, VB6 application. That has been stripped of all UI "stuff".
> what security and user context does it require to run in, Can you tell me how I would find that out?
> does it expect a user profile to be loaded? No. It simply does the following:
Opens a file. Extracts metadata. Writes metadata to text file. Closes file.
Thanks a ton! Peter
pbd22 - 11 Jan 2008 22:53 GMT More info on the exe project:
Type: Standard EXE
Startup Object: Form 1
Upgrade ActiveX Controls: checked
References:
Visual Basic for Applications Visual Basic runtime objects and procedures Visual Basic objects and procedures OLE Automation COM DLL
Willy Denoyette [MVP] - 12 Jan 2008 01:12 GMT >> What makes you think the exec is not started, is the process not visible >> in [quoted text clipped - 119 lines] > Thanks a ton! > Peter What I wan is the Service code that initiate a process start, that is the call site of the piece of code you posted above (starting with if (filetype = true), which is not the start of a method I suppose. I also would suggest you to add some logging stuff to the above code. Also, you can't strip all UI stuff from VB6 application,a VB6 application is by definition a windows application, which makes it hard to get started from a Windows Service.
Willy.
pbd22 - 12 Jan 2008 04:42 GMT Thanks again Willy.
I sent you the windows service code to your private email address. You will see the calling method there (I sent a description in my email).
Feel free to continue the thread by responding here.
THanks again for all your help! Peter
Willy Denoyette [MVP] - 14 Jan 2008 21:17 GMT > Thanks again Willy. > [quoted text clipped - 6 lines] > THanks again for all your help! > Peter Ok, I noticed in another reply that the only thing that gets done by the COM components, is retrieving the metadata from Windows media files (video) and dumping this data to a text file. If this is what you are doing, then there is no reason to use VB6 and COM stuff any longer. All you should do is declare the IWMHeaderInfo3 and the IWMMetadataEditor COM interfaces (search wmsdkidl.idl in the platform sdk headers) and declare the WMCreateEditor function exported by the WMVCore.dll.
Here the PInvoke declaration and part of the COM interface definitions to get you started.
[DllImport("WMVCore.dll",SetLastError=true, CharSet=CharSet.Unicode, SuppressUnmanagedCodeSecurity)] public static extern uint WMCreateEditor( [Out, MarshalAs(UnmanagedType.Interface)] out IWMMetadataEditor ppMetadataEditor );
// COM interfaces IWMMetadataEditor and IWMHeaderInfo3 (check wmsdkidl.idl in the Platform or the Media SDK headers). [Guid("96406BD9-2B2B-11d3-B36B-00C04F6108FF"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IWMMetadataEditor { uint Open( [In,MarshalAs(UnmanagedType.LPWStr)] string pwszFilename ); uint Close(); uint Flush(); }
[Guid("15CC68E3-27CC-4ecd-B222-3F5D02D80BD5"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)] public interface IWMHeaderInfo3 { uint GetAttributeCount( [In] ushort wStreamNum, [Out] out ushort pcAttributes );
// other IWMHeaderInfo3 methods ....
Basically what you should do is call the WMCreateEditor API to get a pointer to the IWMMetadataEditor interface. Once you have this interface, you can call it's Open method to open a media file. When you cast the IWMMetadataEditor to an IWMHeaderInfo3 interface, you can get the metadata. Methods to call are GetAttributeCountEx and GetAttributeByIndexEx. When done with the file, you call IWMMetadataEditor Close and quit. The media API's and COM interfaces can be used from MTA threads, so there is nothing to worry about when used from Service threads.
Willy.
Willy.
pbd22 - 14 Jan 2008 21:48 GMT Thanks Willy!
That last hope provided some hope. Just to be certain, this particular COM DLL takes MPEG files, extracts metadata, and writes that metadata to text files.
Is this COM no longer necessary? Does your code do this? Does it do it for both MPG and WMV files?
Thanks again.
pbd22 - 14 Jan 2008 22:40 GMT Can you confirm that this approach works for both WMV and MPG files?
Thanks.
Willy Denoyette [MVP] - 14 Jan 2008 23:25 GMT > Thanks Willy! > [quoted text clipped - 7 lines] > > Thanks again. It supports all Advanced Systems Format (ASF) formats, mpeg is not an ASF container and can not contain MetaData, WMV and WMA are AFS container, so you will have to transcode the mpeg file into an ASF container using an ASF Write filter, this is all possible with the Windows Media Format SDK. Please search the Platform SDK for "Direct Show and Windows Media".
I will try to find some time to build a small sample using above. Note that DirectShow and Windows Media are exposing their functionality (DS uses Windows Media) via COM interfaces These are pure COM interfaces, designed to be used from Service applications like streaming media services, they are "free threaded" so a perfect fit when used from Windows Services MTA threads.
Willy.
pbd22 - 15 Jan 2008 14:55 GMT Thanks a ton Willy.
This sounds like the way to go.
So, what you are telling me is that that MPG file has to be transcoded into an ASF container before metadata can be inserted and extracted? I know we can currently extract metadata from MPEG files (using the pesky COM DLL) and, since I am only coding the extraction if metadata (not the insertion), I'll need to find out what method was used to insert metadata into the MPEG files. I think the consultant used DirectShow for this method.
Yes! I would really appreciate any and all code examples of that could set me off down this new path. I would be seriously indebted.
Thanks, Peter
pbd22 - 17 Jan 2008 15:51 GMT Hey Willy,
Any chance of seeing some examples?
Thanks, Peter
Willy Denoyette [MVP] - 17 Jan 2008 19:54 GMT > Hey Willy, > > Any chance of seeing some examples? > > Thanks, > Peter Sent to your private mail alias.
Willy.
Nicholas Paldino [.NET/C# MVP] - 11 Jan 2008 03:14 GMT More likely than not, you are running the service executable as yourself, which probably has elevated permissions. When you install it as a service, it defaults to the LocalService account (I believe) which does not have the same permissions. The new process runs under the user of the process that spawned it.
If you set the service to run under your user account, I bet it would work. Not that you should do this, though. Rather, you should find out the permissions the app that the service runs needs, and then create an account that has the appropriate permissions.
 Signature - Nicholas Paldino [.NET/C# MVP] - mvp@spam.guard.caspershouse.com
> OK. > [quoted text clipped - 9 lines] > > Thanks.
Free MagazinesGet these publications absolutely FREE for up to 12 months. There are no hidden fees and no obligation. Simply choose a title, complete the application form and submit it. Read more ...
|
|
|