По долгу службы наша контора обслуживает несколько организаций, которые для управленческого и бухгалтерского учета используют 1с.
1с, как известно, постоянно выпускает обновления для своих конфигураций.
Соответственно на обновление хотя бы 5 баз уходит приличное количество времени.
Рассказ о том, как добиться полной (кроме скачивания обновлений) автоматизации процесса средствами MSSQL далее.
Автоматизировать процесс начнем с «конца»
В 1с есть командная строка: а вот её параметры
Загвоздка в следующем:
Значит нам надо получить текущую версию 1с.
Есть конечно вариант использования 1с-COMConnector, который не менее 2 секунд устанавливает соединение, «Кушает» лишнюю лицензию, память и пьет мой кофе.
Я пошел по другому пути. В версиях 1с >8.0 нет каталога базы данных, следовательно всё хранит в себе база данных MSSQL.
В таблицах БД есть таблица _config, а в ней строка с FileName=root. Смотрим содержимое (Binary Data):
Содержимое хранится в сжатом виде. Ниже будет пример его распаковки
Методом научного тыка находим этой же таблице есть запись с FileName=e0666db2-45d6-49b4-a200-061c6ba7d569 (тот же идетификатор):
(приведена только самая интересная часть файла)
Вы наверное догадались что это за конфигурация 1с.
Теперь надо это объяснить MS SQL.
В c# я написал класс:
При создании класса надо передать MemoryStream binary data из таблицы _config.
Как видно в коде им можно парсить и конфигурации 7.7, предварительно распаковав.
Теперь до версии конфигурации можно добраться зная её «адрес»: Далее просто создаем пустую базу с версией «ВЕРСИЯ» и находи что её «адрес» (v8metadata)(((v8metadata)(((v8metadata)(this.array_data[3])).array_data[1])).array_data[1])).array_data[15].ToString();
«Обвязочка» класса:
Usage:
Но где же здесь MSSQL?
Вот CLR функция, которая получит эти данные в самом MSSQL:
У нас есть отдельная БД, которая хранит в себе сервера и базы. Соответственно функция адаптирована под это.
А как определить какое обновление необходимо для данной конфигурации 1с?
При установке обновлении 1с можно использовать каталог обновлений на сервере. В каждом обновлении есть файл .mft вида:
И файл UpdInfo.txt
Это же всё, что нам надо!!!
Зная FromVersions и дату выхода обновления мы можем автоматически генерировать строку для запуска обновления 1с. (ссылка на параметры в начале топика)
Но тут появляется еще одна проблема — наличие пользователей в базе. 1с не обновляется. Пишем «выгонялку» пользователей (vbscript)
Осталось только сложить пазл.
1с, как известно, постоянно выпускает обновления для своих конфигураций.
Соответственно на обновление хотя бы 5 баз уходит приличное количество времени.
Рассказ о том, как добиться полной (кроме скачивания обновлений) автоматизации процесса средствами MSSQL далее.
Автоматизировать процесс начнем с «конца»
В 1с есть командная строка: а вот её параметры
Загвоздка в следующем:
- В 1с обновление возможно только с определенной версии на определенную. Это связано с тем, что файлы обновления поставляются не в виде полного «слепка» конфигурации. А в виде изменений от эталонной версии.
- Так же в самом коде 1с есть предопределенные обработки, которые запускаются при переходе с одной версии на другую.
Значит нам надо получить текущую версию 1с.
Есть конечно вариант использования 1с-COMConnector, который не менее 2 секунд устанавливает соединение, «Кушает» лишнюю лицензию, память и пьет мой кофе.
Я пошел по другому пути. В версиях 1с >8.0 нет каталога базы данных, следовательно всё хранит в себе база данных MSSQL.
В таблицах БД есть таблица _config, а в ней строка с FileName=root. Смотрим содержимое (Binary Data):
{2,e0666db2-45d6-49b4-a200-061c6ba7d569}
Содержимое хранится в сжатом виде. Ниже будет пример его распаковки
Методом научного тыка находим этой же таблице есть запись с FileName=e0666db2-45d6-49b4-a200-061c6ba7d569 (тот же идетификатор):
{2,
{1e4190e9-76c2-456e-a607-4d817110ffd9},6,
{9cd510cd-abfc-11d4-9434-004095e12fc7,
{1,
{36,
{0,
{0,
{0,3,6b021b70-482d-4baf-b590-b86b68d4730e},"ЗарплатаИУправлениеПерсоналом",
{1,"ru","Зарплата и Управление Персоналом, редакция 2.5"},""}
},"",1,
{1,"ru","Зарплата и Управление Персоналом, редакция 2.5"},
{1,"ru","Зарплата и Управление Персоналом, редакция 2.5"},
{1,"ru","Copyright (С) ООО ""1C"", 2007-2011. Все права защищены"},
{1,"ru","http://www.1c.ru"},
{1,"ru","http://v8.1c.ru/hrm/"},72ef09b4-4393-4d23-a530-7e5b50cf1d24,31e2aa98-4a9c-4d99-a3e7-540a13396b8c,1e7940af-fba5-46c3-8ffb-e8389ac2b79f,87090b31-2cd0-4b45-8b24-efdb2383b1a2,1,"Фирма ""1С""","2.5.40.3","http://downloads.v8.1c.ru/tmplts/",1,
(приведена только самая интересная часть файла)
Вы наверное догадались что это за конфигурация 1с.
Теперь надо это объяснить MS SQL.
В c# я написал класс:
using System;
using System.Collections.Generic;
using System.Text;
using System.IO;
namespace metadata
{
class v8metadata
{
public object[] array_data;
public v8metadata[] child;
public v8metadata parent;
public string filename;
private Decoder d;
public v8metadata(MemoryStream ms, string file_name, v8metadata p,Decoder dec)
{
d = dec;
filename = file_name;
array_data = new object[0];
child = new v8metadata[0];
parent = p;
parse_ms(ms, true);
}
// Этот конструктор добавлен для MD файлов 1с версии 7.7
// Там кодировка файла другая
public v8metadata(MemoryStream ms, string file_name,Decoder dec)
{
d = dec;
filename = file_name;
array_data = new object[0];
child = new v8metadata[0];
if (parent == null)
ms.Position = 0;
parse_ms(ms);
}
public v8metadata(MemoryStream ms, string file_name)
{
d = Encoding.UTF8.GetDecoder();
filename = file_name;
array_data = new object[0];
child = new v8metadata[0];
if (parent == null)
ms.Position = 0;
parse_ms(ms);
}
public object get(int pos)
{
return array_data[pos];
}
public string get(string descr)
{
string res = "error wrong param";
if (descr == "version")
try
{
res = ((v8metadata)(((v8metadata)(((v8metadata)(this.array_data[3])).array_data[1])).array_data[1])).array_data[15].ToString();
}
catch
{
res = "Ошибка";
}
else if (descr == "vendor")
res = ((v8metadata)(((v8metadata)(((v8metadata)(this.array_data[3])).array_data[1])).array_data[1])).array_data[14].ToString();
else if (descr == "conf")
res = ((v8metadata)(((v8metadata)(((v8metadata)(((v8metadata)(((v8metadata)(this.array_data[3])).array_data[1])).array_data[1])).array_data[1])).array_data[1])).array_data[2].ToString();
if ((res.Substring(0, 1) == "\"") && (res.Substring(res.Length - 1) == "\""))
{
res = res.Substring(1);
res = res.Substring(0, res.Length - 1);
}
return res.Replace("\"\"", "\"");
}
public string ToStr(string lev)
{
string ret = "";
for (int i = 0; i < this.array_data.Length; i++)
{
ret = ret+ lev +"_" + i.ToString() + ":";
if (this.array_data[i].GetType().FullName == "System.String")
ret = ret + " " + this.array_data[i].ToString() + @"
";
else
ret = ret + ((v8metadata)this.array_data[i]).ToStr(lev + "_" + i.ToString()) + @"
";
}
return ret;
}
private void parse_ms(MemoryStream ms)
{
parse_ms(ms, false);
}
private int parse_ms(MemoryStream ms, Boolean findbegin)
{
int cur;
byte[] bytedata = new byte[0];
//for (int i = Convert.ToInt16(ms.Position); i < ms.Length; i++)
while (ms.Position < ms.Length)
{
cur = ms.ReadByte();
//Это заголовки
if (cur == 13)
{
cur = ms.ReadByte();
if (cur == 10)
continue;
else
{
Addtoarraybyte(ref bytedata, 13);
Addtoarraybyte(ref bytedata, cur);
}
}
if (cur == 123)
{
if (findbegin)
{
adddata(bytedata);
adddata(new v8metadata(ms, filename, this,d));
}
else
{
findbegin = true;
bytedata = new byte[0];
}
continue;
}
if (cur == 44)
{
//запятая
adddata(bytedata);
bytedata = new byte[0];
continue;
}
if (cur == 125)
break;
Addtoarraybyte(ref bytedata, cur);
}
adddata(bytedata);
return 0;
}
private static void Addtoarraybyte(ref byte[] arr, int val)
{
byte[] tmp = new byte[arr.Length + 1];
Array.Copy(arr, 0, tmp, 0, arr.Length);
arr = tmp;
arr[arr.Length - 1] = Convert.ToByte(val);
}
private void adddata(byte[] bytedata)
{
if (bytedata.Length == 0) return;
char[] cs = new char[d.GetCharCount(bytedata, 0, bytedata.Length)];
d.GetChars(bytedata, 0, bytedata.Length, cs, 0); //выполняем декодировку
string a_data = new string(cs);
object[] tmp = new object[array_data.Length + 1];
Array.Copy(array_data, 0, tmp, 0, array_data.Length);
array_data = tmp;
array_data[array_data.Length - 1] = a_data;
}
private void adddata(v8metadata childdata)
{
object[] tmp = new object[array_data.Length + 1];
Array.Copy(array_data, 0, tmp, 0, array_data.Length);
array_data = tmp;
array_data[array_data.Length - 1] = childdata;
addv8metadata(childdata);
}
private void addv8metadata(v8metadata childdata)
{
v8metadata[] tmp1 = new v8metadata[child.Length + 1];
Array.Copy(child, 0, tmp1, 0, child.Length);
child = tmp1;
child[child.Length - 1] = childdata;
}
}
}
При создании класса надо передать MemoryStream binary data из таблицы _config.
Как видно в коде им можно парсить и конфигурации 7.7, предварительно распаковав.
Теперь до версии конфигурации можно добраться зная её «адрес»: Далее просто создаем пустую базу с версией «ВЕРСИЯ» и находи что её «адрес» (v8metadata)(((v8metadata)(((v8metadata)(this.array_data[3])).array_data[1])).array_data[1])).array_data[15].ToString();
«Обвязочка» класса:
using System;
using System.Collections.Generic;
using System.Text;
using System.Data.SqlClient;
using System.IO.Compression;
using System.IO;
namespace metadata
{
class v8config
{
public SqlConnection con;
public string rootfile;
public string platform;
public string vendor;
public string conf;
public string version;
public v8metadata v8;
private string db;
private string server = "";
public v8config(string constring)
{
con = new SqlConnection(constring);
db = con.Database;
MemoryStream ms = file_data("root");
v8metadata v82 = new v8metadata(ms, "root");
rootfile = v82.get(1).ToString();
if (v82.array_data.Length > 2)
platform = "8.2";
else
platform = "8.1";
update_prop();
}
//Конструктор для linked серверов
public v8config(SqlConnection conn, string srv, string database)
{
con = conn;
server = srv;
db = database;
if (check_is_v8())
{
MemoryStream ms = file_data("root");
v8metadata v82 = new v8metadata(ms, "root");
rootfile = v82.get(1).ToString();
if (v82.array_data.Length == 3)
platform = "8.2";
else if (v82.array_data.Length > 3)
{
platform = "8.0";
rootfile = "root";
}
else
platform = "8.1";
update_prop();
}
}
public v8config(SqlConnection conn)
{
con = conn;
db = con.Database;
MemoryStream ms = file_data("root");
v8metadata v82 = new v8metadata(ms, "root");
rootfile = v82.get(1).ToString();
update_prop();
}
//Функция, конечно кривовата, но работает
private void update_prop()
{
if (platform != "8.0")
{
if (v8 == null)
{
v8 = new v8metadata(file_data(rootfile), rootfile);
}
if (((v8metadata)(((v8metadata)(((v8metadata)(v8.array_data[3])).array_data[1])).array_data[1])).array_data.Length > 20)
platform = "8.2";
vendor = v8.get("vendor");
conf = v8.get("conf");
version = v8.get("version");
}
else
{
vendor = "очень старая конфигурация";
conf = "очень старая конфигурация";
version = "очень старая конфигурация";
}
}
public void SaveToFile(string file, string filename)
{
FileStream fs = File.OpenWrite(filename);
MemoryStream ms = file_data(file);
byte[] data = ms.ToArray();
fs.Write(data, 0, data.Length);
}
private bool check_is_v8()
{
try
{
bool doopencon = false;
SqlCommand cmdu1;
bool haslinked = true;
if (con.State != System.Data.ConnectionState.Open)
{
con.Open();
doopencon = true;
}
cmdu1 = new SqlCommand("", con);
cmdu1.CommandText = "select name from [" + server + "].[master].dbo.sysdatabases where name='" + db + "'";
if (cmdu1.ExecuteScalar() == null)
{
haslinked = false;
}
if (!haslinked)
{
if (doopencon)
con.Close();
return false;
}
cmdu1.CommandText = "select name from [" + server + "]." + db + ".dbo.sysobjects where name ='config'";
if (cmdu1.ExecuteScalar() == null)
{
haslinked = false;
}
if (doopencon)
con.Close();
return haslinked;
}
catch
{
return false;
}
}
private MemoryStream file_data(string name)
{
MemoryStream ms = new MemoryStream();
if (con.State != System.Data.ConnectionState.Open)
con.Open();
string sql;
if (server != "")
sql = "select BinaryData from [" + server + "].[" + db + "].dbo.Config where [filename]='" + name + "'";
else
sql = "select BinaryData from [" + db + "].dbo.Config where [filename]='" + name + "'";
SqlCommand cmdu1 = new SqlCommand(sql, con);
cmdu1.CommandTimeout = 200;
SqlDataReader rs = cmdu1.ExecuteReader();
if (rs.Read())
{
DeflateStream dfs = new DeflateStream(rs.GetSqlBytes(0).Stream, CompressionMode.Decompress);
byte[] BufferOut = new byte[100];
int BytesRead;
while ((BytesRead = dfs.Read(BufferOut, 0, 100)) > 0)
ms.Write(BufferOut, 0, BytesRead);
dfs.Close();
}
con.Close();
return ms;
}
public string get(string descr)
{
if (rootfile == null)
return "База данных не является бд 1с v8";
if (v8 == null)
{
v8 = new v8metadata(file_data(rootfile), rootfile);
}
if (descr == "test")
{
string fname = ((v8metadata)(((v8metadata)(((v8metadata)(v8.array_data[4])).array_data[2])).array_data[2])).array_data[3].ToString() + ".5";
MemoryStream ms = this.file_data(fname);
v8metadata nv8 = new v8metadata(ms, fname);
return "test";
}
else
return v8.get(descr);
}
}
}
Usage:
v8config v8 = new v8config("server=сервер;database=база;uid=sa;pwd=пароль;Connection Timeout=300;");
v8.get("version");
Но где же здесь MSSQL?
Вот CLR функция, которая получит эти данные в самом MSSQL:
У нас есть отдельная БД, которая хранит в себе сервера и базы. Соответственно функция адаптирована под это.
using System;
using System.Collections;
using System.Data;
using System.Data.Sql;
using System.Data.SqlClient;
using System.Data.SqlTypes;
using Microsoft.SqlServer.Server;
public partial class _1c_conf_functions
{
[SqlFunction(DataAccess = DataAccessKind.Read, FillRowMethodName = "FillRowConfig", TableDefinition = "[platform] nvarchar(5),vendor nvarchar(150),conf nvarchar(250),version nvarchar(100)")]
public static IEnumerable v8getconfig(string srv, string dbs)
{
using (SqlConnection conn = new SqlConnection("context connection=true"))
{
metadata.v8config v8 = new metadata.v8config(conn, srv, dbs);
metadata.v8config[] t = new metadata.v8config[1];
t[0] = v8;
return t;
}
//v8metadata.v8_confmanager vconf = new v8metadata.v8_confmanager(@"\\ackiy_gw\public\1c_distr\tmplts_8.1");
//return vconf.find_config("Фирма \"1С\"", "БухгалтерияПредприятия").data;
}
public static void FillRowConfig(object obj, out SqlChars platform, out SqlChars vendor, out SqlChars conf, out SqlChars version)
{
// dateupdate = new SqlDateTime(((v8metadata.v8info)obj).dataupdate);
metadata.v8config cfg = (metadata.v8config)obj;
platform = new SqlChars(cfg.platform);
vendor = new SqlChars(cfg.vendor);
conf = new SqlChars(cfg.conf);
version = new SqlChars(cfg.version);
}
};
А как определить какое обновление необходимо для данной конфигурации 1с?
При установке обновлении 1с можно использовать каталог обновлений на сервере. В каждом обновлении есть файл .mft вида:
Vendor=Фирма "1С"
Name=БухгалтерияПредприятия
Version=2.0.25.5
AppVersion=8.2
........
И файл UpdInfo.txt
Version=2.0.25.5
FromVersions=;2.0.24.10;
UpdateDate=11.07.2011
Это же всё, что нам надо!!!
Зная FromVersions и дату выхода обновления мы можем автоматически генерировать строку для запуска обновления 1с. (ссылка на параметры в начале топика)
Но тут появляется еще одна проблема — наличие пользователей в базе. 1с не обновляется. Пишем «выгонялку» пользователей (vbscript)
'basename-Имя базы в кластере серверов 1с
'updateway - Путь к файлу обновления
'platform - Платформа 1с (8.1/8.2)
'srv1c - Сервер 1с
'srvUser - пользователя для входа на сервер 1с
'srvPasswd- пароль пользователя для входа на сервер 1с
'confchanged - булево - конфигурация изменена. Выполнить только принятие изменений конфигурации
Function GetUpdateForConfig(basename,updateway,platform,srv1c,srvUser,srvPasswd,baseUsr,basepwd,confchanged)
allowdisconnect=false
'Если мы работаем с 12 до 6 утра - мы можем выгнать пользователей
if Hour(now())>0 and Hour(now)<6 then
allowdisconnect=true
End if
set Connector=CreateObject("V" & Replace(platform,".","") & +".ComConnector")
set AgentConnection=Connector.ConnectAgent(srv1c)
set Cluster=AgentConnection.GetClusters() (0)
AgentConnection.Authenticate Cluster,srvUser,srvPasswd
'WorkingProcess = AgentConnection.GetWorkingProcesses(Cluster)[0];
Process = AgentConnection.GetWorkingProcesses(Cluster)
for each WorkingProcess in Process
if WorkingProcess.Running<>0 then
ConnectString = WorkingProcess.HostName & ":" & WorkingProcess.MainPort
set WorkingProcessConnection = Connector.ConnectWorkingProcess("tcp://" & ConnectString)
WorkingProcessConnection.AddAuthentication baseUsr,basepwd
set ibDesc = WorkingProcessConnection.CreateInfoBaseInfo()
ibDesc.Name = basename
Connections = WorkingProcessConnection.GetInfoBaseConnections(ibDesc)
for each Connection in Connections
if LCase(Connection.AppID) <> "comconsole" then
if allowdisconnect then
WorkingProcessConnection.Disconnect Connection
ShowStatus "Discconnect соединения " & Trim(Connection.ConnID),true,false
else
ShowStatus "Есть работающие пользователи.",false,false
GetUpdateForConfig=0
Exit function
End if
End if
Next
End if
Next
ShowStatus "Запуск обновления " & updateway,true,false
ProcId=0
constr="/S""" & srv1c & "\" & basename & """"
if baseUsr<>"" then
constr=constr & " /N" & baseUsr & " /P" & basepwd
end if
if not confchanged then
constr=constr & " /UpdateCfg""" & updateway & """"
end if
constr=constr & " /UpdateDBCfg"
if Way1cv81="" then
ShowStatus "Не заполнен путь к 81",true,true
GetUpdateForConfig=""
Exit function
End if
RunString="""" & Way1cv81 & "1cv8.exe"" CONFIG " & constr & ""
Resfile=WorkCatalog() & "result.txt"
RunString=RunString & " /Out""" & Resfile & """"
ShowStatus "Строка запуска: " & RunString,true,false
CreateProcess RunString,true
GetUpdateForConfig=ReadFileText(Resfile)
End Function
Осталось только сложить пазл.