Google
Web alhem.net
Main Page | Namespace List | Class Hierarchy | Alphabetical List | Class List | File List | Class Members | File Members

MyApp.cpp

Go to the documentation of this file.
00001 00005 /* 00006 Copyright (C) 2004 Anders Hedström (grymse@alhem.net) 00007 00008 This program is free software; you can redistribute it and/or 00009 modify it under the terms of the GNU General Public License 00010 as published by the Free Software Foundation; either version 2 00011 of the License, or (at your option) any later version. 00012 00013 This program is distributed in the hope that it will be useful, 00014 but WITHOUT ANY WARRANTY; without even the implied warranty of 00015 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 00016 GNU General Public License for more details. 00017 00018 You should have received a copy of the GNU General Public License 00019 along with this program; if not, write to the Free Software 00020 Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. 00021 */ 00022 #pragma warning(disable:4786) 00023 #define _WIN32_WINNT 0x0400 00024 #define _WIN32_IE 0x0500 00025 //#define WINVER 0x0500 00026 #include <windows.h> 00027 //#include <io.h> 00028 #include <ListenSocket.h> 00029 #include <config-win.h> 00030 #include <mysql.h> 00031 #include <libmysqlwrapped.h> 00032 #include <stdio.h> 00033 #include <shellapi.h> 00034 #include "resource.h" 00035 #include <socket_include.h> 00036 #include <SocketHandler.h> 00037 #include <HttpGetSocket.h> 00038 #include <process.h> 00039 #include <commctrl.h> 00040 #include <StdoutLog.h> 00041 #include "Mutex.h" 00042 #include "MyErrlog.h" 00043 #include "MySocketsLog.h" 00044 #include "MenuHandler.h" 00045 #include "MyMenu.h" 00046 #include <Utility.h> 00047 #include <libcgi++.h> 00048 #include "FinderHandler.h" 00049 #include "ExecSocket.h" 00050 #include "MyMinionSocket.h" 00051 #include "MyMinderSocket.h" 00052 00053 #include "MyApp.h" 00054 00055 //#define STRESS 00056 00057 00058 static HANDLE g_hD2; 00059 00060 #define D2(x) { \ 00061 Mutex lock(g_hD2); \ 00062 if (lock.Locked()) \ 00063 { \ 00064 FILE *fil = fopen("c:\\deb.log","at"); \ 00065 x; \ 00066 fclose(fil); \ 00067 } \ 00068 } 00069 00070 00071 // Statics 00072 static bool g_bQuit = false; 00073 static HANDLE g_hMutex; 00074 static bool g_bRestart = true; 00075 static HANDLE g_hMutexScan; 00076 static bool g_bWsRunning = false; 00077 static MenuHandler g_menuHandler; 00078 00079 // Globals 00080 MyApp *g_app = NULL; 00081 HWND g_hwndMain; 00082 00083 00084 MyApp::MyApp(HINSTANCE hInst,HINSTANCE hPrevInst,Database& db) : WinApp(hInst, hPrevInst) 00085 ,m_db(db) 00086 ,m_quit(false) 00087 ,m_local_port(18018) 00088 ,m_bind_local(true) 00089 ,m_minder_host("localhost") 00090 ,m_minder_port(9696) 00091 ,m_minion_port(19696) 00092 ,m_no_minder(true) 00093 ,m_new_minder_host(m_minder_host) 00094 ,m_new_minder_port(m_minder_port) 00095 ,m_new_no_minder(m_no_minder) 00096 ,m_create_top(false) 00097 ,m_join_group("fd") 00098 ,m_use_password(true) 00099 ,m_new_join_group(m_join_group) 00100 ,m_new_use_password(m_use_password) 00101 { 00102 g_app = this; 00103 } 00104 00105 00106 MyApp::~MyApp() 00107 { 00108 } 00109 00110 00111 //UINT start( LPVOID pParam ) 00112 DWORD WINAPI start( LPVOID lpParam ) 00113 { 00114 mysql_thread_init(); 00115 MyErrlog log; 00116 g_bWsRunning = true; 00117 { 00118 Database db("fd",&log); 00119 while (g_bRestart) 00120 { 00121 MySocketsLog log; 00122 FinderHandler h(db); 00123 h.RegStdLog(&log); 00124 h.ResolveLocal(); 00125 ListenSocket<ExecSocket> l(h); 00126 ListenSocket<MyMinionSocket> l2(h); 00127 // apply changes 00128 g_app->ApplyChanges(); 00129 // bind 00130 std::string address = g_app->BindLocal() ? "127.0.0.1" : "0.0.0.0"; 00131 l.Bind(address, g_app->GetLocalPort()); 00132 h.Add(&l); 00133 if (!g_app->NoMinder()) 00134 { 00135 int port = g_app->GetMinionPort(); 00136 while (l2.Bind( port )) 00137 { 00138 port++; 00139 } 00140 h.Add(&l2); 00141 h.SetLocalPort( port ); 00142 g_app->reg_minder(h, "Hello"); 00143 } 00144 // times 00145 time_t tminder = time(NULL); 00146 time_t tminion = time(NULL) - 30; 00147 time_t ttop = time(NULL); 00148 // reg new sockets 00149 h.Select(1,0); 00150 // reset restart 00151 g_bRestart = false; 00152 while (h.GetCount() && !g_bQuit && !g_bRestart) 00153 { 00154 h.Select(1,0); 00155 time_t tnow = time(NULL); 00156 if (!g_app->NoMinder()) 00157 { 00158 if (tnow - tminder > 15 * 60 ) 00159 { 00160 g_app->reg_minder(h, "Hello"); 00161 tminder = tnow; 00162 } 00163 if (tnow - tminion > 60 ) 00164 { 00165 g_app->minion_connect(h); 00166 tminion = tnow; 00167 } 00168 if (g_app->CreateTop()) 00169 { 00170 if (ttop < tnow) 00171 { 00172 h.BeginTop(); 00173 ttop = tnow + 15; 00174 } 00175 else 00176 if (ttop == tnow) 00177 { 00178 h.EndTop(); 00179 g_app->SetCreateTop( false ); 00180 } 00181 } 00182 } // if (!g_app->NoMinder()) 00183 } 00184 if (!g_app->NoMinder()) 00185 { 00186 g_app->reg_minder(h, "Goodbye"); 00187 h.Select(1,0); 00188 while (h.MinderSockets()) 00189 { 00190 h.Select(1,0); 00191 } 00192 } 00193 } // while (g_bRestart) 00194 } 00195 // 00196 g_bWsRunning = false; 00197 mysql_thread_end(); 00198 return 0; 00199 } 00200 00201 00202 static int CALLBACK MyDlgProc(HWND hwnd, UINT uMsg, WPARAM wParam, LPARAM lParam) 00203 { 00204 switch (uMsg) 00205 { 00206 case WM_INITDIALOG: 00207 break; 00208 00209 case WM_SHELLNOTIFY: 00210 if (lParam == WM_LBUTTONDBLCLK) 00211 { 00212 char cmd[100]; 00213 sprintf(cmd, "http://127.0.0.1:%d/", g_app->GetLocalPort()); 00214 ::ShellExecute(NULL, NULL, cmd, NULL, "", SW_SHOW); 00215 } 00216 else 00217 if (lParam == WM_RBUTTONDOWN) 00218 { 00219 MyMenu m(g_menuHandler); // 'Menu' constructor adds new object to handler 00220 POINT pt; 00221 00222 GetCursorPos(&pt); 00223 SetForegroundWindow(g_hwndMain); 00224 int cmd = TrackPopupMenuEx(m.Handle(), 00225 TPM_RIGHTALIGN|TPM_BOTTOMALIGN|TPM_RIGHTBUTTON|TPM_RETURNCMD, 00226 pt.x, pt.y, g_hwndMain, NULL); 00227 if (cmd) 00228 { 00229 if (!g_menuHandler.OnCommand( cmd )) 00230 { 00231 ::PostMessage(g_hwndMain, WM_COMMAND, cmd, 0); 00232 } 00233 } 00234 ::PostMessage(g_hwndMain, WM_NULL, 0, 0); 00235 } 00236 break; // WM_SHELLNOTIFY 00237 case WM_INITMENU: 00238 D2(fprintf(fil,"WM_INITMENU %08lx %08lx\n",wParam,lParam);) 00239 { 00240 HMENU hMenu = (HMENU)wParam; 00241 g_menuHandler.OnInitMenu(hMenu); 00242 } 00243 break; 00244 case WM_INITMENUPOPUP: 00245 D2(fprintf(fil,"WM_INITMENUPOPUP %08lx %08lx\n",wParam,lParam);) 00246 { 00247 HMENU hMenu = (HMENU)wParam; 00248 int relpos = lParam & 0xffff; 00249 int winmenu = (lParam >> 16) & 0xffff; 00250 D2(fprintf(fil," relpos: %d winmenu: %d\n",relpos,winmenu);); 00251 { 00252 MENUITEMINFO info; 00253 ::GetMenuItemInfo(hMenu, relpos, true, &info); 00254 } 00255 g_menuHandler.OnInitPopupMenu(hMenu, relpos, winmenu ? true : false); 00256 } 00257 break; 00258 case WM_MENUSELECT: 00259 D2(fprintf(fil,"WM_MENUSELECT %08lx %08lx\n",wParam,lParam);) 00260 { 00261 HMENU hMenu = (HMENU)lParam; 00262 int index = wParam & 0xffff; 00263 int flags = (wParam >> 16) & 0xffff; 00264 g_menuHandler.OnMenuSelect(hMenu, index, flags); 00265 } 00266 break; 00267 // 00268 case WM_CHANGE_ICON: 00269 g_app->ChangeIcon( wParam ); 00270 break; 00271 case WM_COMMAND: 00272 switch (wParam) 00273 { 00274 case IDOK: 00275 g_app->GetDialogData(); 00276 case IDCANCEL: 00277 ShowWindow(g_hwndMain, SW_HIDE); 00278 break; 00279 case ID_POPUP_EXIT: 00280 g_app->SetQuit(); 00281 break; 00282 00283 default: 00284 break; 00285 } 00286 break; 00287 } 00288 return 0; 00289 } 00290 00291 00292 void MyApp::ApplyChanges() 00293 { 00294 m_minder_host = m_new_minder_host; 00295 m_minder_port = m_new_minder_port; 00296 m_no_minder = m_new_no_minder; 00297 m_join_group = m_new_join_group; 00298 m_use_password = m_new_use_password; 00299 m_group_password = m_new_group_password; 00300 00301 if (m_no_minder) 00302 { 00303 ::PostMessage(g_hwndMain, WM_CHANGE_ICON, IDR_MAIN, 0); 00304 } 00305 else 00306 { 00307 ::PostMessage(g_hwndMain, WM_CHANGE_ICON, IDI_GREY, 0); 00308 } 00309 } 00310 00311 00312 void MyApp::SetDialogData() 00313 { 00314 if (IsWindow(g_hwndMain)) 00315 { 00316 char slask[1000]; 00317 // local port 00318 sprintf(slask, "%d", m_local_port); 00319 SetWindowText(GetDlgItem(g_hwndMain, IDED_LOCAL_PORT), slask); 00320 // bind local 00321 ::CheckDlgButton(g_hwndMain, IDRB_BIND_LOCAL, m_bind_local ? 0 : 1); 00322 // minder host 00323 SetWindowText(GetDlgItem(g_hwndMain, IDED_MINDER_HOST), m_minder_host.c_str()); 00324 // minder port 00325 sprintf(slask, "%d", m_minder_port); 00326 SetWindowText(GetDlgItem(g_hwndMain, IDED_MINDER_PORT), slask); 00327 // minion port 00328 sprintf(slask, "%d", m_minion_port); 00329 SetWindowText(GetDlgItem(g_hwndMain, IDED_MINION_PORT), slask); 00330 // no minder 00331 ::CheckDlgButton(g_hwndMain, IDRB_NO_MINDER, m_no_minder ? 0 : 1); 00332 // join group, password 00333 SetWindowText(GetDlgItem(g_hwndMain, IDED_JOIN_GROUP), m_join_group.c_str()); 00334 SetWindowText(GetDlgItem(g_hwndMain, IDED_GROUP_PASSWORD), m_group_password.c_str()); 00335 } 00336 } 00337 00338 00339 void MyApp::GetDialogData() 00340 { 00341 if (IsWindow(g_hwndMain)) 00342 { 00343 int old_local_port = m_local_port; 00344 bool old_bind_local = m_bind_local; 00345 char slask[1000]; 00346 // local port 00347 GetWindowText(GetDlgItem(g_hwndMain, IDED_LOCAL_PORT), (LPSTR)slask, 1000); 00348 m_local_port = atoi(slask); 00349 // bind local 00350 m_bind_local = ::IsDlgButtonChecked(g_hwndMain, IDRB_BIND_LOCAL) ? false : true; 00351 // minder host 00352 GetWindowText(GetDlgItem(g_hwndMain, IDED_MINDER_HOST), (LPSTR)slask, 1000); 00353 m_new_minder_host = slask; 00354 // minder port 00355 GetWindowText(GetDlgItem(g_hwndMain, IDED_MINDER_PORT), (LPSTR)slask, 1000); 00356 m_new_minder_port = atoi(slask); 00357 // no minder 00358 m_new_no_minder = ::IsDlgButtonChecked(g_hwndMain, IDRB_NO_MINDER) ? false : true; 00359 // create top 00360 m_create_top = ::IsDlgButtonChecked(g_hwndMain, IDRB_CREATE_TOP) ? true : false; 00361 // join group, password 00362 GetWindowText(GetDlgItem(g_hwndMain, IDED_JOIN_GROUP), (LPSTR)slask, 1000); 00363 m_new_join_group = slask; 00364 GetWindowText(GetDlgItem(g_hwndMain, IDED_GROUP_PASSWORD), (LPSTR)slask, 1000); 00365 m_new_group_password = slask; 00366 // 00367 if (m_local_port != old_local_port || 00368 m_bind_local != old_bind_local || 00369 m_new_minder_host != m_minder_host || 00370 m_new_minder_port != m_minder_port || 00371 m_new_no_minder != m_no_minder || 00372 m_new_join_group != m_join_group || 00373 m_new_group_password != m_group_password 00374 ) 00375 { 00376 g_bRestart = true; 00377 } 00378 } 00379 } 00380 00381 00382 void MyApp::ShowTaskBar(BOOL show) 00383 { 00384 NOTIFYICONDATA tnid; 00385 DWORD dwMsg; 00386 00387 tnid.cbSize = sizeof(NOTIFYICONDATA); 00388 tnid.hWnd = g_hwndMain; 00389 00390 if (show) 00391 { 00392 tnid.uFlags = NIF_MESSAGE | NIF_ICON | NIF_TIP; 00393 tnid.uCallbackMessage = WM_SHELLNOTIFY; 00394 tnid.hIcon = (HICON)LoadImage(GetInstance(), MAKEINTRESOURCE(IDR_MAIN), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR); 00395 tnid.uID = IDR_MAIN; 00396 00397 strcpy(tnid.szTip, "fd"); 00398 dwMsg = NIM_ADD; 00399 } 00400 else 00401 { 00402 tnid.uFlags = 0; 00403 tnid.uID = IDR_MAIN; 00404 dwMsg = NIM_DELETE; 00405 } 00406 00407 ::Shell_NotifyIcon(dwMsg, &tnid); 00408 } 00409 00410 00411 int MyApp::Init(LPSTR lpCmdLine, int nCmdShow) 00412 { 00413 if (!(g_hwndMain = CreateDialog(GetInstance(), MAKEINTRESOURCE(IDD_SETTINGS), GetDesktopWindow(), MyDlgProc))) 00414 { 00415 ::MessageBox(GetDesktopWindow(), "Unable to create dialog", "Error", MB_OK); 00416 return -1; 00417 } 00418 00419 // Create notify changes lock 00420 g_hMutex = ::CreateMutex(NULL, FALSE, NULL); 00421 g_hMutexScan = ::CreateMutex(NULL, FALSE, NULL); 00422 g_hD2 = ::CreateMutex(NULL, FALSE, NULL); 00423 00424 // set icon 00425 SendMessage(g_hwndMain, WM_SETICON, TRUE, (LPARAM)LoadIcon(GetInstance(), MAKEINTRESOURCE(IDR_MAIN))); 00426 ShowTaskBar(true); 00427 00428 // CreateThread - webserver 00429 HANDLE h; 00430 DWORD dwThreadId; 00431 h = CreateThread(NULL, 0, start, NULL, 0, &dwThreadId); 00432 00433 // winmain loop 00434 { 00435 MSG msg; 00436 while (!m_quit && IsWindow(g_hwndMain) && GetMessage(&msg, g_hwndMain, NULL, NULL) != WM_QUIT) 00437 { 00438 if (!IsDialogMessage(g_hwndMain, &msg)) 00439 { 00440 DispatchMessage(&msg); 00441 } 00442 } 00443 } 00444 D2(fprintf(fil,"shutting down\n");) 00445 00446 // shutdown threads 00447 g_bQuit = true; 00448 00449 // End of program 00450 { 00451 ShowWindow(g_hwndMain, SW_HIDE); 00452 D2(fprintf(fil,"hide window\n");) 00453 // %! popup: "Shutting Down" ??? 00454 // %! kolla running status för varje tråd och gör en timeout istället 00455 bool bWait = true; 00456 int timeout = 50; // 5s 00457 while (bWait && timeout--) 00458 { 00459 Sleep(100); 00460 bWait = false; 00461 if (g_bWsRunning) 00462 { 00463 bWait = true; 00464 } 00465 } 00466 if (!timeout) 00467 { 00468 ::MessageBox(NULL, "Thread wait timeout", "", MB_OK); 00469 } 00470 #ifdef _DEBUG 00471 ::MessageBox(NULL, "All threads gone", "", MB_OK); 00472 #endif 00473 // delete webserver thread handle 00474 CloseHandle(h); 00475 D2(fprintf(fil,"webserver h closed\n");) 00476 // 00477 DestroyWindow(g_hwndMain); 00478 D2(fprintf(fil,"g_hwndMain closed\n");) 00479 } 00480 // Remove taskbar icon 00481 ShowTaskBar(false); 00482 D2(fprintf(fil,"taskbar icon removed\n");) 00483 ::CloseHandle(g_hMutex); 00484 D2(fprintf(fil,"g_hMutex closed\n");) 00485 ::CloseHandle(g_hMutexScan); 00486 D2(fprintf(fil,"g_hMutexScan closed\n");) 00487 ::CloseHandle(g_hD2); 00488 00489 // 00490 D2(fprintf(fil,"MyApp::Init() returns 0\n");) 00491 return 0; 00492 } // Init 00493 00494 00495 int MyApp::MemoryUsage() 00496 { 00497 typedef BOOL (_stdcall * GetProcessMemoryInfoPtr)(HANDLE, void*, DWORD); 00498 struct _PROCESS_MEMORY_COUNTERS 00499 { 00500 DWORD cb; 00501 DWORD PageFaultCount; 00502 SIZE_T PeakWorkingSetSize; 00503 SIZE_T WorkingSetSize; 00504 SIZE_T QuotaPeakPagedPoolUsage; 00505 SIZE_T QuotaPagedPoolUsage; 00506 SIZE_T QuotaPeakNonPagedPoolUsage; 00507 SIZE_T QuotaNonPagedPoolUsage; 00508 SIZE_T PagefileUsage; 00509 SIZE_T PeakPagefileUsage; 00510 }; 00511 // 00512 HINSTANCE hLib; 00513 GetProcessMemoryInfoPtr GetProcessMemoryInfo; 00514 struct _PROCESS_MEMORY_COUNTERS mem; 00515 int result = 0; 00516 00517 memset(&mem, 0, sizeof(mem)); 00518 mem.cb = sizeof(mem); 00519 if ((hLib = LoadLibrary("psapi.dll")) != NULL) 00520 { 00521 if ((GetProcessMemoryInfo = (GetProcessMemoryInfoPtr)GetProcAddress(hLib, "GetProcessMemoryInfo")) != NULL) 00522 { 00523 if ((*GetProcessMemoryInfo)(GetCurrentProcess(), &mem, sizeof(mem))) 00524 { 00525 result = mem.WorkingSetSize / 1024; 00526 } 00527 } 00528 FreeLibrary(hLib); 00529 } 00530 return result; 00531 } 00532 00533 00534 BOOL MyApp::ShowBalloonTip(const std::string& title,const std::string& msg) 00535 { 00536 NOTIFYICONDATA tnid; 00537 DWORD uTimeout = 500; // milliseconds 00538 memset(&tnid, 0, sizeof(tnid)); 00539 tnid.cbSize = sizeof(NOTIFYICONDATA); 00540 tnid.hWnd = g_hwndMain; 00541 tnid.uID = IDR_MAIN; 00542 tnid.uFlags = NIF_INFO; 00543 tnid.uTimeout = uTimeout; 00544 tnid.dwInfoFlags = NIIF_INFO; 00545 /* 00546 By default, dwInfoFlags is set to NIIF_INFO to display an information icon 00547 next to the text; other possibilities are NIIF_ERROR for an error, 00548 NIIF_WARNING for a warning, and NIIF_NONE for no icon at all. 00549 */ 00550 00551 strcpy(tnid.szInfo, msg.c_str() ); 00552 strcpy(tnid.szInfoTitle, title.c_str() ); 00553 return Shell_NotifyIcon(NIM_MODIFY, &tnid); 00554 } 00555 00556 00557 void MyApp::ChangeIcon(int id) 00558 { 00559 NOTIFYICONDATA tnid; 00560 DWORD uTimeout = 500; // milliseconds 00561 memset(&tnid, 0, sizeof(tnid)); 00562 tnid.cbSize = sizeof(NOTIFYICONDATA); 00563 tnid.hWnd = g_hwndMain; 00564 tnid.uID = IDR_MAIN; 00565 tnid.uFlags = NIF_ICON; 00566 tnid.hIcon = (HICON)LoadImage(GetInstance(), MAKEINTRESOURCE(id), IMAGE_ICON, 16, 16, LR_DEFAULTCOLOR); 00567 Shell_NotifyIcon(NIM_MODIFY, &tnid); 00568 } 00569 00570 00571 void MyApp::reg_minder(FinderHandler& h,const std::string& cmd) 00572 { 00573 std::string group = Utility::base64(m_join_group); 00574 if (m_use_password && m_group_password.size()) 00575 { 00576 group += "/" + Utility::base64(m_group_password); 00577 } 00578 MyMinderSocket *m = new MyMinderSocket(h, group); 00579 m -> Init(); 00580 00581 ::PostMessage(g_hwndMain, WM_CHANGE_ICON, IDI_YELLOW, 0); 00582 m -> Function(cmd); // Hello / Goodbye 00583 m -> SetDeleteByHandler(true); 00584 bool opened = m -> Open(m_minder_host.c_str(), m_minder_port); 00585 h.Add(m); 00586 m -> SetLocalIpPort(h.GetLocalAddress(),h.GetLocalPort()); 00587 if (opened && !m -> Connecting()) // connected 00588 { 00589 m -> SendHello(); 00590 } 00591 if (!opened && !m->Connecting()) // fatal 00592 { 00593 ::PostMessage(g_hwndMain, WM_CHANGE_ICON, IDI_RED, 0); 00594 m->SetCloseAndDelete(); 00595 } 00596 00597 } 00598 00599 00600 void MyApp::minion_connect(FinderHandler& h) 00601 { 00602 if (h.Count() < 3) 00603 { 00604 ipaddr_t a; 00605 port_t p; 00606 std::string key; 00607 long host_id; 00608 if (h.GetHost(a,p,key,host_id)) 00609 { 00610 MinionSocket *tmp = new MyMinionSocket(h,key,a,p); 00611 tmp -> Init(); 00612 ipaddr_t my_ip; 00613 port_t my_port; 00614 h.GetMyIpPort(my_ip, my_port); 00615 tmp -> SetMyIpPort(my_ip,my_port); 00616 tmp -> SetRemoteHostId(host_id); 00617 if (tmp -> Open(a,p)) 00618 { 00619 tmp -> SetDeleteByHandler(true); 00620 h.Add(tmp); 00621 // 00622 tmp -> SendHello("Hello"); 00623 } 00624 else 00625 if (tmp -> Connecting()) 00626 { 00627 tmp -> SetDeleteByHandler(true); 00628 // check OnConnect 00629 h.Add(tmp); 00630 } 00631 else 00632 { 00633 delete tmp; 00634 } 00635 } 00636 else 00637 { 00638 h.LogError(NULL, "minion_connect", 0, "GetHost() failed"); 00639 } 00640 } 00641 } 00642 00643

Generated on Thu Feb 10 22:42:34 2005 for Distributed URL Classification Tool by doxygen 1.3.7