Справка по службе Windows / запланированной задаче, которая должна использовать веб-браузер и диалоговые окна файлов.

Что я пытаюсь сделать

Я пытаюсь создать решение любого типа, которое будет работать каждую ночь на сервере Windows, аутентифицироваться на веб-сайте, проверять веб-страницу на сайте на наличие новых ссылок, указывающих на новую версию zip-файла, использовать новые ссылки (если они есть ), чтобы загрузить zip-файл, распаковать загруженный файл в существующую папку на сервере, использовать разархивированное содержимое (сценарии sql и т. д.) для создания экземпляра базы данных и регистрировать все, что происходит с текстовым файлом.

Приложение Forms: часть, которая работает с Sorta

Я создал приложение Windows Forms, которое использует пару элементов управления WebBrowser, пару потоков и несколько таймеров для всего этого, кроме работы в ночное время. Он отлично работает как форма, когда я вхожу в систему и запускаю ее, но мне нужно получить ее (или что-то в этом роде), чтобы она работала сама по себе, как служба или запланированная задача.

Моя попытка обслуживания

Итак, я создал службу Windows, которая работает каждый час и, если System.DateTime.Now.Hour> = 22, пытается запустить приложение Windows Forms для этого. Когда Сервис пытается запустить форму, возникает эта ошибка:

Элемент управления ActiveX «8856f961-340a-11d0-a96b-00c04fd705a2» не может быть создан, поскольку текущий поток не находится в однопоточном апартаменте.

который я исследовал и попытался решить, поместив атрибут [STAThread] в метод Main класса Program службы или используя некоторый подобный код в нескольких местах, включая конструктор формы:

        webBrowseThread = new Thread(new ThreadStart(InitializeComponent));
        webBrowseThread.SetApartmentState(ApartmentState.STA);
        webBrowseThread.Start();

Я не мог заставить работать ни один из подходов. В последнем подходе элементы управления в форме (которые инициализируются внутри IntializeComponent) не инициализируются, и я получаю исключения нулевой ссылки.

Моя запланированная попытка выполнения задания

Итак, я попытался создать задачу, запланированную на ночь, используя свои собственные учетные данные, для локального запуска формы на моем компьютере разработчика (просто тестирование). Он идет дальше, чем служба, но зависает в диалоговом окне загрузки файла.

Связанное примечание: Чтобы отправить последовательности клавиш для прохождения диалоговых окон "Загрузка файла" и "Сохранить файл как", моя форма фактически запускает несколько файлов vbscript, которые используют WScript.Shell.SendKeys. Хорошо, стыдно признавать, но я пробовал несколько разных вещей, включая SendMessage в Win32 API и ссылку на IWshRuntimeLibrary для использования SendKeys в моем коде C #. Когда я изучал, как работать с диалоговыми окнами, мне показалось, что рекомендуется использовать Win32 API, но я не мог этого понять. Файлы vbscript были единственной вещью, с которой я мог работать, но сейчас я беспокоюсь, что это может быть причиной того, что запланированная задача не будет работать.

Что касается моего выбора управления веб-браузером

Я читал о классе System.WebClient в качестве альтернативы элементу управления WebBrowser, но на первый взгляд не похоже, что у него есть то, что мне нужно для этого. Например, мне понадобились (или я думаю, что мне понадобились) события DocumentCompleted и FileDownload WebBrowser для обработки задержек при загрузке страниц, загрузке файлов и т. Д. Есть ли в WebClient что-то еще, чего я не вижу? Есть ли другой класс, помимо WebBrowser, который более удобен для обслуживания и поможет?

В сводке

Блин, это долго. Прости! Было бы полезно иметь даже высокоуровневую рекомендацию для лучшего способа делать то, что я пытаюсь сделать, потому что ничего из того, что я пробовал, не сработало.

Обновление 22.10.09

Что ж, я думаю, что я ближе, но я снова застрял. У меня должен получиться zip-файл приличного размера с несколькими файлами в нем, но zip-файл, полученный из моего кода, пуст. Вот мой код:

// build post request
string targetHref = "http://wwwcf.nlm.nih.gov/umlslicense/kss/login.cfm";
HttpWebRequest request = (HttpWebRequest)WebRequest.Create(targetHref);
request.Method = "POST";
request.ContentType = "application/x-www-form-urlencoded";

// encoding to use
Encoding enc = Encoding.GetEncoding(1252);

// build post string containing authentication information and add to post request
string poststring = "returnUrl=" + fixCharacters(targetDownloadFileUrl);
poststring += getUsernameAndPasswordString();
poststring += "&login2.x=0&login2.y=0";
// convert to required byte array
byte[] postBytes = enc.GetBytes(poststring);
request.ContentLength = postBytes.Length;

// write post to request
Stream postStream = request.GetRequestStream();
postStream.Write(postBytes, 0, postBytes.Length);
postStream.Close();

// get response as stream
HttpWebResponse response = (HttpWebResponse)request.GetResponse();
Stream responseStream = response.GetResponseStream();

// writes stream to zip file
FileStream writeStream = new FileStream(fullZipFileName, FileMode.Create, FileAccess.Write); 
ReadWriteStream(responseStream, writeStream);

response.Close();
responseStream.Close();

Код ReadWriteStream выглядит так.

private void ReadWriteStream(Stream readStream, Stream writeStream)
{
    // taken verbatum from http://www.developerfusion.com/code/4669/save-a-stream-to-a-file/
    int Length = 256;
    Byte[] buffer = new Byte[Length];
    int bytesRead = readStream.Read(buffer, 0, Length);
    // write the required bytes
    while (bytesRead > 0)
    {
        writeStream.Write(buffer, 0, bytesRead);
        bytesRead = readStream.Read(buffer, 0, Length);
    }
    readStream.Close();
    writeStream.Close();
}

Построение строки сообщения взято из моего предыдущего работающего приложения форм. Я сравнил полученные значения в poststring для обоих наборов кода (моего приложения рабочих форм и этого), и они идентичны.

Я даже не знаю, как это исправить дальше. Кто-нибудь видит что-нибудь очевидное, почему это не работает?

Заключение 23.10.09

Наконец-то это работает. Мне пришлось преодолеть пару важных препятствий. У меня возникли проблемы с кодом метода ReadWriteStream, который я получил в сети. Не знаю почему, но у меня это не сработало. Помог парень по имени JB на встрече с виртуальным коричневым мешком Клаудио Лассала я придумал этот код, который работал намного лучше для моих целей:

private void WriteResponseStreamToFile(Stream responseStreamToRead, string zipFileFullName)
{
    // responseStreamToRead will contain a zip file, write it to a file in 
    // the target location at zipFileFullName
    FileStream fileStreamToWrite = new FileStream(zipFileFullName, FileMode.Create);
    int readByte = responseStreamToRead.ReadByte();
    while (readByte != -1)
    {
        fileStreamToWrite.WriteByte((byte)readByte);
        readByte = responseStreamToRead.ReadByte();
    }
    fileStreamToWrite.Flush();
    fileStreamToWrite.Close();
}

Как Уилл предположил ниже, у меня возникли проблемы с аутентификацией. Следующий код помог решить эту проблему. Было добавлено несколько комментариев, касающихся ключевых проблем, с которыми я столкнулся.

string targetHref = "http://wwwcf.nlm.nih.gov/umlslicense/kss/login.cfm";
HttpWebRequest firstRequest = (HttpWebRequest)WebRequest.Create(targetHref);
firstRequest.AllowAutoRedirect = false; // this is critical, without this, NLM redirects and the whole thing breaks
// firstRequest.Proxy = new WebProxy("127.0.0.1", 8888); // not needed for production, but this helped in order to debug the http traffic using Fiddler
firstRequest.Method = "POST";
firstRequest.ContentType = "application/x-www-form-urlencoded";

// build post string containing authentication information and add to post request
StringBuilder poststring = new StringBuilder("returnUrl=" + fixCharacters(targetDownloadFileUrl));
poststring.Append(getUsernameAndPasswordString());
poststring.Append("&login2.x=0&login2.y=0");
// convert to required byte array
byte[] postBytes = Encoding.UTF8.GetBytes(poststring.ToString());
firstRequest.ContentLength = postBytes.Length;

// write post to request
Stream postStream = firstRequest.GetRequestStream();
postStream.Write(postBytes, 0, postBytes.Length); // Fiddler shows that post and response happen on this line
postStream.Close();

// get response as stream
HttpWebResponse firstResponse = (HttpWebResponse)firstRequest.GetResponse();

// create new request for new location and cookies
HttpWebRequest secondRequest = (HttpWebRequest)WebRequest.Create(firstResponse.GetResponseHeader("location"));
secondRequest.AllowAutoRedirect = false;
secondRequest.Headers.Add(HttpRequestHeader.Cookie, firstResponse.GetResponseHeader("Set-Cookie"));

// get response to second request
HttpWebResponse secondResponse = (HttpWebResponse)secondRequest.GetResponse();

// write stream to zip file
Stream responseStreamToRead = secondResponse.GetResponseStream();
WriteResponseStreamToFile(responseStreamToRead, fullZipFileName);
responseStreamToRead.Close();
sl.logScriptActivity("Downloading update.");

firstResponse.Close();

Я хочу подчеркнуть, что установка для AllowAutoRedirect значения false в первом экземпляре HttpWebRequest была критичной для всей работы. Fiddler показал два дополнительных запроса, которые произошли, когда он не был установлен, и сломал остальную часть скрипта.


person Marvin    schedule 07.10.2009    source источник
comment
Код выглядит правильно. Я бы взял хороший кусок Fiddler fiddlertool.com и смотрел, как ваши запросы передаются по сети. Если это не сработает, я создам небольшое веб-приложение и буду работать с ним, чтобы видеть, что происходит на обоих концах.   -  person    schedule 22.10.2009
comment
Еще одна мысль - у вашего запроса могут быть проблемы с входом и выходом. Опять же, наблюдение за запросами, проходящими через линию, позволит вам сравнить, как вы делаете это в браузере и в своем коде. У меня есть подозрение, что вам нужно войти в систему, получить файл cookie аутентификации и отправить этот файл cookie обратно при попытке загрузить zip-файл. Начните со скрипачей и сравните свои запросы.   -  person    schedule 22.10.2009
comment
Потрясающий. И спасибо, что обновили свой вопрос с подробностями о том, как это работает. Я мог бы вернуться, чтобы украсть часть вашего кода ...   -  person    schedule 24.10.2009


Ответы (3)


Вы пытаетесь использовать элементы управления пользовательского интерфейса для выполнения каких-либо действий в службе Windows. Это никогда не сработает.

Вам нужно просто использовать WebRequest и WebResponse классы для загрузки содержимого веб-страницы.

var request = WebRequest.Create("http://www.google.com"); 
var response = request.GetResponse(); 
var stream = response.GetResponseStream();

Вы можете выгрузить содержимое потока, проанализировать текст в поисках обновлений, а затем создать новый запрос для URL-адреса файла, который вы хотите загрузить. В этом потоке ответов будет файл, который вы можете сбросить в файловую систему и т. Д. И т. Д.

Прежде чем вы задаетесь вопросом, GetResponse будет блокироваться до тех пор, пока не вернется ответ, а поток будет блокироваться по мере получения данных, поэтому вам не нужно беспокоиться о срабатывании событий, когда все было загружено.

person Community    schedule 07.10.2009
comment
Спасибо, Уилл. Я работаю над тем, чтобы использовать предложенный вами подход, и пока все идет хорошо. Как только я сделаю это и начну работать, я отправлю ответ с любыми дополнительными заметками. - person Marvin; 21.10.2009
comment
У меня есть обновление. Я снова застрял. Я отредактировал основной пост, потому что его было слишком много, чтобы уместиться в комментарии. - person Marvin; 22.10.2009
comment
Я наконец понял это и обновил свой исходный пост, чтобы отразить это. Спасибо, Уилл, за вашу помощь. - person Marvin; 23.10.2009

Вам определенно нужно переосмыслить свой подход (как вы уже начали делать), чтобы отказаться от подхода приложения на основе форм. Сервис, который вы описываете, должен работать без пользовательского интерфейса.

Я не знаком с деталями System.WebClient, но поскольку он

предоставляет общие методы для отправки и получения данных от ресурса, идентифицированного URI,

это, вероятно, будет ваш ответ. На первый взгляд, WebClient.DownloadFile (...) или WebClient.DownloadFileAsync (...) сделает то, что вам нужно.

person Tim Coffman    schedule 07.10.2009

Единственное, что я могу добавить, это то, что после того, как вы очистили свой экран и получили полное имя файла, который хотите загрузить, вы можете передать его команде Windows / DOS 'get', которая будет извлекать файлы через HTTP. При желании вы также можете создать скрипт для FTP-клиента из командной строки. Прошло много времени с тех пор, как я пробовал что-то подобное в Windows, но я думаю, что вы почти у цели. После того, как вы выбрали правильный файл, создание командного файла для всего остального должно быть довольно простым. Если вам удобнее работать с Unix, погуглите «службы unix для Windows», просто следите за службами, которые они запускают (DHCP и т. Д.). Есть несколько хороших утилит, которые позволят вам рассматривать dos как unix-подобную оболочку (ls -l, grep и т. Д.). Наконец, вы можете попробовать другой язык, например Perl или Python, но я не думаю, что вы были такими советами. находясь в поиске. :)

person Community    schedule 09.10.2009