From 2713cba136c925c415656946de7c7702553a6f8b Mon Sep 17 00:00:00 2001 From: wanjia Date: Tue, 25 Feb 2025 15:23:41 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E7=9B=91=E6=8E=A7=E6=96=87?= =?UTF-8?q?=E4=BB=B6=E5=A4=B9=E4=B8=8B=E7=9A=84python=E8=BF=9B=E7=A8=8B?= =?UTF-8?q?=E5=92=8C=E5=89=8D=E7=AB=AF=E7=9A=84=E5=9B=BE=E8=A1=A8=E5=B1=95?= =?UTF-8?q?=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | Bin 0 -> 18 bytes .idea/.gitignore | 56 +- .idea/automated_task_monitor.iml | 58 +- .idea/misc.xml | 12 +- .idea/modules.xml | 14 +- .idea/vcs.xml | 6 + .idea/workspace.xml | 34 +- .../__pycache__/settings.cpython-310.pyc | Bin 2645 -> 3101 bytes .../__pycache__/urls.cpython-310.pyc | Bin 1029 -> 1357 bytes automated_task_monitor/settings.py | 46 +- automated_task_monitor/urls.py | 6 +- .../process_monitor/all/2136_20250225_all.log | 23 + .../all/22424_20250225_all.log | 184 +++ .../all/22740_20250225_all.log | 184 +++ .../all/27836_20250225_all.log | 92 ++ .../all/31964_20250225_all.log | 483 ++++++ .../all/35500_20250225_all.log | 92 ++ .../all/37712_20250225_all.log | 460 ++++++ .../process_monitor/all/7616_20250225_all.log | 23 + .../process_monitor/cpu/1132_20250225_cpu.log | 46 + .../process_monitor/cpu/6432_20250220_cpu.log | 138 ++ .../process_monitor/cpu/6432_20250222_cpu.log | 23 + .../gpu/process_22572_20250218.log | 67 - .../memory/process_40128_20250218.log | 15 - monitor/__pycache__/models.cpython-310.pyc | Bin 2507 -> 3815 bytes monitor/__pycache__/tasks.cpython-310.pyc | Bin 9819 -> 10790 bytes monitor/__pycache__/urls.cpython-310.pyc | Bin 480 -> 933 bytes monitor/__pycache__/views.cpython-310.pyc | Bin 9533 -> 22498 bytes ...2_alter_highcpuprocess_options_and_more.py | 132 ++ monitor/migrations/0003_allresourceprocess.py | 41 + ...puprocess_options_and_more.cpython-310.pyc | Bin 0 -> 1861 bytes .../0003_allresourceprocess.cpython-310.pyc | Bin 0 -> 1817 bytes monitor/models.py | 84 +- monitor/static/css/style.css | 17 + monitor/static/img/1740047317581.jpg | Bin 0 -> 1782 bytes monitor/static/js/monitor.js | 928 ++++++++++++ monitor/templates/high_resource_list.html | 176 +++ monitor/templates/index.html | 298 ++++ monitor/urls.py | 17 +- monitor/views.py | 1335 ++++++++++++----- test_program/main.py | 18 + 41 files changed, 4527 insertions(+), 581 deletions(-) create mode 100644 .gitignore create mode 100644 .idea/vcs.xml create mode 100644 logs/process_monitor/all/2136_20250225_all.log create mode 100644 logs/process_monitor/all/22424_20250225_all.log create mode 100644 logs/process_monitor/all/22740_20250225_all.log create mode 100644 logs/process_monitor/all/27836_20250225_all.log create mode 100644 logs/process_monitor/all/31964_20250225_all.log create mode 100644 logs/process_monitor/all/35500_20250225_all.log create mode 100644 logs/process_monitor/all/37712_20250225_all.log create mode 100644 logs/process_monitor/all/7616_20250225_all.log create mode 100644 logs/process_monitor/cpu/1132_20250225_cpu.log create mode 100644 logs/process_monitor/cpu/6432_20250220_cpu.log create mode 100644 logs/process_monitor/cpu/6432_20250222_cpu.log delete mode 100644 logs/process_monitor/gpu/process_22572_20250218.log delete mode 100644 logs/process_monitor/memory/process_40128_20250218.log create mode 100644 monitor/migrations/0002_alter_highcpuprocess_options_and_more.py create mode 100644 monitor/migrations/0003_allresourceprocess.py create mode 100644 monitor/migrations/__pycache__/0002_alter_highcpuprocess_options_and_more.cpython-310.pyc create mode 100644 monitor/migrations/__pycache__/0003_allresourceprocess.cpython-310.pyc create mode 100644 monitor/static/css/style.css create mode 100644 monitor/static/img/1740047317581.jpg create mode 100644 monitor/static/js/monitor.js create mode 100644 monitor/templates/high_resource_list.html create mode 100644 monitor/templates/index.html create mode 100644 test_program/main.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000000000000000000000000000000000..d860227d4e5caaa3dad32f8744c9f02f40607003 GIT binary patch literal 18 ZcmezWPmdv!A%!88A(26!ftP`c0RT9N1O)&9 literal 0 HcmV?d00001 diff --git a/.idea/.gitignore b/.idea/.gitignore index ce51e77..aca24db 100644 --- a/.idea/.gitignore +++ b/.idea/.gitignore @@ -1,11 +1,45 @@ -echo "*.pyc -__pycache__/ -.env -*.log -logs/ -.idea/ -.vscode/ -*.sqlite3 -db.sqlite3 -venv/ -.venv/" > .gitignore \ No newline at end of file +# Python +__pycache__/ +*.py[cod] +*$py.class +*.so +.Python +env/ +build/ +develop-eggs/ +dist/ +downloads/ +eggs/ +.eggs/ +lib/ +lib64/ +parts/ +sdist/ +var/ +wheels/ +*.egg-info/ +.installed.cfg +*.egg + +# Django +*.log +local_settings.py +db.sqlite3 +db.sqlite3-journal +media + +# IDE +.idea/ +.vscode/ +*.swp +*.swo +*~ + +# Virtual Environment +venv/ +.venv/ +ENV/ + +# Project specific +logs/ +.env \ No newline at end of file diff --git a/.idea/automated_task_monitor.iml b/.idea/automated_task_monitor.iml index 337c034..20b68db 100644 --- a/.idea/automated_task_monitor.iml +++ b/.idea/automated_task_monitor.iml @@ -1,30 +1,30 @@ - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml index 6a77fe2..c774be5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,7 +1,7 @@ - - - - - + + + + + \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml index a36eaaa..c31fcd4 100644 --- a/.idea/modules.xml +++ b/.idea/modules.xml @@ -1,8 +1,8 @@ - - - - - - - + + + + + + + \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml new file mode 100644 index 0000000..c8397c9 --- /dev/null +++ b/.idea/vcs.xml @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/.idea/workspace.xml b/.idea/workspace.xml index df22ea7..e1c8734 100644 --- a/.idea/workspace.xml +++ b/.idea/workspace.xml @@ -4,7 +4,9 @@ - + + + + + { "customColor": "", "associatedIndex": 6 @@ -26,20 +31,20 @@ - { - "keyToString": { - "RunOnceActivity.OpenDjangoStructureViewOnStart": "true", - "RunOnceActivity.OpenProjectViewOnStart": "true", - "RunOnceActivity.ShowReadmeOnStart": "true", - "last_opened_file_path": "D:/pythonProject/myproject/.venv/Scripts", - "node.js.detected.package.eslint": "true", - "node.js.detected.package.tslint": "true", - "node.js.selected.package.eslint": "(autodetect)", - "node.js.selected.package.tslint": "(autodetect)", - "nodejs_package_manager_path": "npm", - "vue.rearranger.settings.migration": "true" + +}]]> @@ -87,6 +92,7 @@ + diff --git a/automated_task_monitor/__pycache__/settings.cpython-310.pyc b/automated_task_monitor/__pycache__/settings.cpython-310.pyc index 947c750d0a26336b7d306c442146092c3db68029..9e07d3d47a0254ac1d1c5dfb11165a20668e0851 100644 GIT binary patch delta 904 zcmX|7OK%fN5T5SodH9L*g1pEJyx?7u0Q)#Z+Czln#H$cPY*wO$Gz_lJWX#wTO^*;b zLOyU?X%EO(uat1(f{-{Ou82RtrT+o9Jx5}*WjjhsUG-IeU0;3OU&sHM%!ZjvQp5H6 z{wJ?K`Z_yHexgLMMy zEaoKU3y|VDNDCV>Gnz-hfIf5K4e<%&ddwqy1V;EMjKUaZivPrw4dh$b%Ko`gj{CG^Jh(F_h-I;JQ+dtP5Tru;Ho<#UjS zAK)5XhXVZAqo`MapWp^8!%9!bY89@b$(wL%z}@Z{OMD(h7T{-e{>$H#Xkt;YBX+F4 zB76z-s#f^*Lza9b%)r0<@P#IrnMTy!>MmN>yQk*KoV_YzH}>nR+#CXx8sb(r+FFqCk~V4_ql#VlRxEZXAoK zJ$C5Idh9pF%Z0mK~&l>;S*sYc;J8!kYfr_|~r#NQFy N?CjM1H#?Qn{|7h^2L%8C delta 479 zcmW+yJ8K(35Wc-z-JW!MS(a@*^RV+kCdi~tr64N^1cpRzrpR*EtS@^$X~o^;K^hmS zQy8@l*oF{ND_#CWx-?tlw`A>sneQ8B_`YGjHU8Gis#GdiCdBkt__1E!Qapw-Yl7tj$cn>!5KGe+tTPC;h z0qo#IXb2~2UbT$jF791B&^CQXw2kqjuQsvJd1Lh+uTRNuvVP)tl=$ld_LQ8nGz&60 zJNVqiC|HE5k0<@PLJ_C^mogl@&(uhY7=gwrOrf$&)J&u)pR0w;RMI2b6Q5+7adp<~ zYQ{OL8RwcUgX~=_PZHaFQ`;)l9_Obr7PqrRq$>U(w7U$F)t~%e)h&E~Q5znP2FH^r z9~@7Q_-K4M8ESf~qG_b$0@&o}6(5dYYi}N$36x3t(@OS@da}QI?!T>rq0}lCJyLZj SqmE}&w))`@U7D*pHRnH;8iMTr diff --git a/automated_task_monitor/__pycache__/urls.cpython-310.pyc b/automated_task_monitor/__pycache__/urls.cpython-310.pyc index 3b5d63501ff9c9e0aab54be31915540a5e28f9f0..f422acffa0792f91c33f5d3db169f76bbfac7ac7 100644 GIT binary patch delta 527 zcmYjNy-ve05O&fuasJvefmoTUN@YU~2!UWhf+9g#vY-%~k|>Gc>?|xG6-EZc$P+N~ z0K5PaFOZeDKp=5WJM^sk{GEOOkMzqMh%odB%IDF?B(n(l*2bdPXzZZM`{%*h1v8kH z+l3Q320`A=Y2ik0!|mKF{Kz*5x0u7tEXcyCiJbB_bEihbJ?69EflSRwi@V&(I;dS1 zKAFoT>aor#>383+-_5(eQB%p4O3R5vZc7!bG;N`V78su?t!Q$;z z9jQ}L0-c#}$V5^Vyj15Pgc|jiUiMoXcRtJFaw3L_C@-Onp3(BcOViwNHV^3C*1h?! zyb_?2N=eZD)s{>ZudQYl?{vI7KG-`yJ2|ZVIX^i*9@jp8U&l&uC?y0K2Ek^78bY0h eZWV%6Ip5YBkShol33@#vn4Ols&A*OCfBymC3xd)B delta 197 zcmX@h)ykor&&$ij00h@IuS$<(W?*;>;vfT7AjbiSiybCvD=Ve4q_CtiWwB+kH!}g* ztf{OiY|V_#OzBKf94YL<44NDpH*925u3}3}$<55uuj0tf&&w>yFVe5#fwJ^Ui*kzn zG&v_Hu$U>b6la#C-eLg>-Qot41&JjksYQ9kD;bJFt}5c1{G8cOCO1E&G$+-L5#$OU SAi=`GBOt)Y!_33T@ecr@b}<3R%GD3NZqSENmEo|D#@)uAX@|yzI;F@7${XnCY1qds1ia@7`0l z@4dg@Irp?%EE)^-Aj>Cmv}S zjJx01iMe`;_4f)ogWb|8I)9^e z>YvfA`Gq@2ucgC0gmK7S71=R!&N4sEI ze-CVj&JjqnXFOlnRsWhI*5Ny(`R@xmU)<9??wb=&96@@?;3%qA(^ z)B8`F+}1o4s`T{;AH}11(G)EuHlQ(G84+t}{s}UZFk#-Zm|L|hktS4lqv7*eE?%o~ zhlQDDjfMKFd+h`lnz3}`hQh)Q*LAqvh<){%XJG@i{XEQhzU>EISlo%F?nZRP9!6Dg zxpvzRjiAXoZOw6Kt#@h;mrLyJKW?q3mC`2Y@y1~^MNJh_rS9KoYBVi926*e{iWl)( zKW{Ry5P(cY@WdgQMhFt3$j`An0w<5a(U}H@^|17MZFcXgZ@!(xjtm;W#c$8I<}SC- zTW2%YV#+EKd zN-ZFb53XqXT6P)XQsLoPe;`{;ClI!?;>nY0NGv~6Ak)crm1HcRcq2Sw6&bHNuI(qf zs5S#D@N9_cF*uky;i@aRw>sTh;W&cFqpN_vl*k6j)IV@yU6NnN=#mplvTT=R%DFMg zc?duQ%E>VulqZ(~C^wi6%8i)vT}vqk{0Yk8v5ay$J@iKB@3+1?ACD~_|8(orj!G~b z4=r53-}>r!`_`AOAI=VJuOkTYB%G0JB>BB%^~fJw+q8HJ@2pDe78?8=nK(5JtxBla z2Cq<`?JyY{)U5NmD-I>BQb(=9_`%g#{XHh3X!a=2cUYCY(#XKZyS)Atqd$XJVmaoO zZc{?}F3Y%A5!kw9*#OH!;Rwajl-x-tw&^rlAASI~R_8hx>pJx)-L<14qx=!^8qd(x zLO^MK;qxz|6PNl&NC|jNjy}BmRW$eI3Znx@vs@Ds1Dl9tL8*Slo&1C{q-=hMG77O2 z;xG?O32JgtYF)9?=1%_JqzS+6M2XEE?=JRV(QJ+Vl+(AdD4WYM`D zk7wX?mdfQaL~x~3Y9Z&dEaiEKL#Is7G$1OqbX0&bqEZXhV1fz~4JC*QFC9f#>`8?e zD#HW~OH@iwS)$Xai>*&jFCKZXlTUm7(aEo)Gq;m| zLD$IcfNrq3uoOi!1)YJ&!&K7oKIwHqdc!^`gas`QZMupE^wx<%kLu@sy|J{Za@EYT z3{GD?JUMv!GC0j~+i@&hgOV33sjTFc(E8wN!ip@#rz^Q-nK{(h3@X4GluWcK@mn(8 zx-o9ax{1Pzu0@YHr6}sVzc((MRCMoenbO53nshUnEoA5l5KqIH#rQ_&!lGY3<}g9; zkmvKd>=1t<&p(rSp3EP~JOg76HMnopT+2gN=mNp3`^7KF=a*!DMdmkTDEh_YWS$_i zYNmJrqqHt>D-7+?pmHHC>%*R<7M^mal%Th!am(v$pWT>sx3QU|08Vh=*Bnn|;1y=4 t=%=2nFl7ZI=(-Y=gapm9(!qz?OL3+DGRZ{du!2%R2~#eX^|CQB^nZN20S*8F literal 2507 zcma)8O>7%Q6yEjvXYJZf;{5dws0tOes3LkKLa0ikmL3vSP%f)TtBq%H9M)blyPGCG zQ6&&WNhuN~p+PECg*K`}1B#j|f#k-O`(DYeopWwn;Jw+kQ5`a2Rlu!~n-QjsTWP9B`cDC`r68WRGltUm+_x(V{`9Zsrg+J`n&HpqYO{M+HSB zf)XO4BGK?@1Vfk+l_|^9O9Q*iWNa-Rc;Ls-Dvy@3AtzDP##Nn`^418$6mlDH_9u5^wz~4vU7xzq&<{0Rw|17epU=B%tKRIht=VO8o78N}P)o*jRU>4_%9~&IzFn$Y$u%o( zu_e~_4-WUmGw+j|_E8u;-&@_dQ~mMF&F72fPQBUduKecS{N7u9kd+vonlE?aBUCS| z79F=3_tXM27q5P>`Nu}(o5z*g(`V0Q?RFS*Kl{{Oob!jbroTFSxi4#@?d3hTUH+Ii zx4z4K0L+iURQ0=i-mUxlIBTj*jOGF~vlY6=$|NDkv2>%mImbN%)Y7gEX2XuG@&u}? zlTcMCeOssSo>bM3ZB5720+O|1(_&iAS`b(p?7_Q#-s1fa3Z3 zL;_#^NhLj2U=}bMV1C>kF8w#WcT9o~oB1+yvJe)NV!Hlm71L4;WkTGxsTyiRxqS{C zG*$zs0oN%K291e=8h}cH8bm=2Vk8EYB0K4K3;7YK=|B+p-Pn3ESFiF$Rq!uCCu|bj zfO}J5=l$^9-I(<5E-?^sp((gpscvKP8-oKO4F~1gzB>qBs$3-eNc8i z;b;>jwS2LcV78V6(26}dsH;bF4gzth{<7{qoUSZCaDTu1|Ln6@a4YEsXL@I=T};Dp zz%)Ar)(3LT6*_ab2qgp9%^pIg+StA4ZuZJrNFH?rBGqm0>^N#$s|5Nb}UT72lxg~{6 LsI{Y2?vVcj!5gs+ diff --git a/monitor/__pycache__/tasks.cpython-310.pyc b/monitor/__pycache__/tasks.cpython-310.pyc index b3d4af71ebb69b7763a6bf8678e040468c12a991..7212f4f407e7b4d12804b838bf3e8b08d02cc8c5 100644 GIT binary patch delta 4137 zcmZ`+eQ;FO6@T}=w{PEmW_Pm*A)lK-h^uS>5d)ErYCu2(5<>_CoiyxS!p3Ab+(P<2?V4-AXV|a@dyk4~ zyXMOJaS5G7^~gt6@nGpyo38DQEDa-5jU}Q<98dJqr^K(3pB|BxpquEgrLK&qqM+nO z&0dNN$EHqlitM|M=nUf~O6h)I~`U#?k0|7-4ZxNF)A6Y0(g~B9?L} zEsZWQP*%b+w)KlSl$Egp)Rq&{vLR&^Y&Yan!kG++kiZpfSiM2f#uXig4HPXsTdEB( zJ9aXwZunwXC(uN_3_b=_&9PJ58$&``LBePNG@aP3xeR3t6+Cnc+XO78l}8k?^>hju zYcPUFgwcQX3)uTJl(vH_$*|)R>^mwJ#jWyi{DxX^2KoWecW`~jcl4cHztnIR^bKd~ z1&##`;go>O&PcgTVIAe20!C>UJZ{68b>mq%v|4y@dD;W<3uDOeJS>oZ!kRoFtH_}T zq_h{lC5B7$7$q#NW2i+)ySjzl=Vd|jY9)|~f8;Y!$}=&$kO^FAcpxO1rPsu^Kq z@^KV}@#ShHjB6n<_O3f)=au7zacHN+YN1mo(r3vn$}O)CKLkT&R7ZWL z5RFw}(_uh)G{BbdQW-(qJOJj;<<0AOO6L@;@mxb0D1~etWpAry5(qaI(mccOydu9D zK?{Q0_h&@aRB-z}LESWMJR(4nnYEd>_Vb{^|IZrVTx=V@MXwbE>QNSEOxwt=cy!lQ zXX4>Fj`u}ULviATAGux%Wb)956OX?UnijQ&M#V_G&Q62Lv->9BJ`CbZKl$<6O>Cq7?vRQg22fYg{hs)0<6y#V3uj`p^0N3eE#0^v_lP6Unv2# z%P+q<@x)8ubK>+H_|_>uZSmbY|ni9sZy1>Yy%xQ z_v~7HD3;VU9xco^@zZxFPrv&4N4d%U=Ubh5Ut|F&PrHgHhlX`67U>%vPOSjHy}BO9 z^M~QlQ!(5-6dOvCy#BbbeL0T*{ zk@G5=uh%dSFP@!zZX66O?~^Cqm^}LW<>Svzoj>0i(pfKvS+NIadPQ3F@ zVY$}kOkF%MdGcawXvUVZ^-Mka%ZaC+vBdf;09eZTeoh{GaVmG@dQjFHraq(-2j6WC znexVDB3A6pe}%cTOf4gNo4UwCtx59epxQ8x>*g!&`8TKDIu|Z8ohg0CFiA#ZsT7$F zu1F>Qx!R=KNBT&DwZT8DjTN>SAxUCNDUF!6)bL=}-e7&ZfKeS?EByvcIBhMOx;UQjQlW6hpuY*>pY6JbATU{kJQgiqcGT#w3XDW2Gw zw79`|N;6T+l%hkp$H`=tqaGIky_@sF_jvP6LF(3$THj#43N25%79TX_!FVD@Sjm}+ zRidWM1mq?b1t($p8Z2xMj8o46k)idg$SI0ah3G>Sk`Kwy{8c1h3(7&{L+U>9nkcJc z1>+?~Whubql8n5P8;O7fQ2IUZ(5RyBtSOtLfRs?5x+Qz|A=^T?zMu1~@U7!io6{I5Df> zj!^*RS zSau9)YSxhx;I87z!9C>|!mY=k-?Q0(zXN#hz`gm=az1L$k5<5_gj=ix&Nr|jkFSDJ z!@x!^`2pweVXdMo)K;cpnBkbZrXkf{i zmsBM$sqUF21y~Q>%KT|tv)k}C`gh+gbJ8=4u;=?M=mLK|`W@}?&qhzvhk!pvkNOv* z_vx?vx1b7+|Bd7S0eq1p6DLS&l@Vb?2ydtR1K-$47yQ)eW2Gi)pnbt(HgYQ)UkhHy z1b~6_`G)?gGdSOOidvUmW@%fUZpJyEmwfk}|2`Y**w&%3Q(w7nK3N8C!zziui!?K` zn2}aSmNT+~k(G=rqZ`T}aox_?7Dkp{_@aEBj26<{s~WcY7{Z#0Y449|eVRu2-xpIE zPVv*xoI+@fixFJD0qJ059i=ro+Cd$)ZC;Zf?$()1N@-{9E0w%G zu!ES#OtvzP^$0ST&Z=urG{)+5Rb8lM4`cTN>9I$lMI`hg@*@TrjHDUKFtU%4{fr!D zc zU36h+C2FO~&=T}pdM@;|A|mC#J}14SewFkaWhG75pR<1^?-Me~pyO;x32889-RN$HyOJFOZhZxLv@d)FR zjEvDg%w4$PS;le$hqaTv{X5862J)#-F|N4NMjAsXPB%7ocwPZfxRi%NPS95y7q4SC vXX{?TO3-xRfxbk4vKj9n*UOk!uNBE@rp=rb`OmlWrg;(jEOon(aS#3vK*hnD delta 3129 zcmZ`*Yj6|S72dnM+SS{VEx(N|+1N%cGIq?;tkXX;XAFXhH;Jmn`I8 z$c3!L%)UR|wAn@#ePeg-9VGt309a7jTjDExWmB0GZ+vzH0y z@C-zVvfj^*Z$J$F0?Qy7seb?%lBtIoW8%FdjAZ^Ln$Xewithh%3yKi!SGWX(TGFW@ z)Y`+uD`lf>QiP;g%9=sP*_XKqaY8>~&`Pl>qGUhMOWBHE%7I^pV$gnbFN!pWB7!~7 z&+UaW_A)&T&&U?p3Wc6rEHq2XUnmsDE9gl;t=b#e#xE@)6)upZA}x)5{0?F!jEIS6 zLm^pGv7D_KkE7@FGQddegz=~ml9u#j@ED@GW&8N&nOM6PyKHG}37yo1V5xLzsJo1m z>sKcp8^ebKQeemv4F%$?r|qE*R?v-9Q4WNBk(eCU#bm!{easH#)aA+4$8XNPaee+5 zukP&ZYcR&vfpvcN_WbQj_itTIUHD*;v8TVKsin~gtkfH)!7=yY&mLTUEp2`H>lZ%7Y*{+N{E3^X$zu)1FHc?g^392>&nKzKyuLPMjf&VMj6B&)jU z=)nNlYA#CXd>BWtYVA$$hvp-r0qm1hL-Ubg->4jjgjFsoW48%AX`g8MCFD338xp=t_FTK4p15TcG2iOM%AaAs(^t@XvUbf68;h6aJY^uxTBPeQMspq@Sd^12l?o8~Mtz4Al~=wO04>6c^t%yWQG_bOTdt`~W$ZGwehWw6tCopur?TIlBO969u>hur^tMTLG5@ zyS|g17EluG)*^RLqnQK>Z&ONO7XK!-fum?+A7gl2TMO}E6QU;O+m8mkV2^F2xD4-HHojk6f>a$-Tto$SqzjSz(Rra348l=<4xs zA@(XJVD*tMN42LfvuBF5J=6c!_+hp3!Y&*5z6dg8&m)#x z5&ePO$Sp+EWL2&U{fczu)}!0Rl~-z&bZ}X~qX<(x&eup^Xr!-!RLAky0JI7;NSgDW z=v5(ogvri)6*)dD zx-f+i5QE|{bk=Yzgbz^@rXos3jEcilj8SonidN#Ss1tTmc!Zp;sM@4ZxDUjjd00N; zjgAH6;a7aog$51IYH+Gqp-4C&N3b_@%?yGu>L-sXHlqz>{qi-i)56Pt&I^xh1|n2$ z;XV~MkzJMViT~s$nS`x^{HO8|nx6Gn^`Jxtgr=_(FQ5^4;PcfM_*bG2gk>K-Jaq8k z@4@&b*{I0OjDHP&(Zvm`=GW42_QZ!cuyk6V$A&s8nu5XULYa%}6vcA6&r3Q(Yv2S-DP0znXT=v)-YCKwO|A7RV2MS-N-TJO*Y z=|l82I&kfjxm%{**|g*arJ%p?yCd&-(EJe0h(xv9V*?Wu_PR z&K|rkIsd8*GhSW*|3>a2Tl4BNEAzONV#bw*xV zC~_?%O_Dk`;qIGSYZW$bQN|`vT9maj$TnJ}z%`%gIPs6D8to>9l%Zdr)5_1T-jZH*`v^G?o{l+Vg!pD?&S8P*#bwQpc#N<$t zIp6K54prARs!bCcEg9A(dvG7e1LMOoE9T;ABUehc@M@$-g1yRQhono=BcV=C-W;7r z`fsiFR=AQrO%F&yl86?b(ZxO9f$=dfc7^7$P(PTY-(e>Y?><~l%dK9B;*%6hJh&+( tUT2y^sx$TZQ0Ua2#FPrSDz~P4vlg&f@muOM^%^B$ffKor^Z%#&=P#)G8t?!B literal 480 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 literal 9533 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 diff --git a/monitor/migrations/0002_alter_highcpuprocess_options_and_more.py b/monitor/migrations/0002_alter_highcpuprocess_options_and_more.py new file mode 100644 index 0000000..61e7431 --- /dev/null +++ b/monitor/migrations/0002_alter_highcpuprocess_options_and_more.py @@ -0,0 +1,132 @@ +# Generated by Django 5.1.6 on 2025-02-20 07:43 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('monitor', '0001_initial'), + ] + + operations = [ + migrations.AlterModelOptions( + name='highcpuprocess', + options={'verbose_name': 'CPU高占用进程', 'verbose_name_plural': 'CPU高占用进程'}, + ), + migrations.AlterModelOptions( + name='highgpuprocess', + options={'verbose_name': 'GPU高占用进程', 'verbose_name_plural': 'GPU高占用进程'}, + ), + migrations.AlterModelOptions( + name='highmemoryprocess', + options={'verbose_name': '内存高占用进程', 'verbose_name_plural': '内存高占用进程'}, + ), + migrations.RemoveField( + model_name='highcpuprocess', + name='gpu_memory', + ), + migrations.RemoveField( + model_name='highcpuprocess', + name='gpu_usage', + ), + migrations.RemoveField( + model_name='highcpuprocess', + name='log_path', + ), + migrations.RemoveField( + model_name='highcpuprocess', + name='memory_usage', + ), + migrations.RemoveField( + model_name='highcpuprocess', + name='virtual_memory', + ), + migrations.RemoveField( + model_name='highgpuprocess', + name='cpu_usage', + ), + migrations.RemoveField( + model_name='highgpuprocess', + name='log_path', + ), + migrations.RemoveField( + model_name='highgpuprocess', + name='memory_usage', + ), + migrations.RemoveField( + model_name='highgpuprocess', + name='virtual_memory', + ), + migrations.RemoveField( + model_name='highmemoryprocess', + name='cpu_usage', + ), + migrations.RemoveField( + model_name='highmemoryprocess', + name='gpu_memory', + ), + migrations.RemoveField( + model_name='highmemoryprocess', + name='gpu_usage', + ), + migrations.RemoveField( + model_name='highmemoryprocess', + name='log_path', + ), + migrations.AddField( + model_name='highcpuprocess', + name='log_file', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='日志文件路径'), + ), + migrations.AddField( + model_name='highgpuprocess', + name='log_file', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='日志文件路径'), + ), + migrations.AddField( + model_name='highmemoryprocess', + name='log_file', + field=models.CharField(blank=True, max_length=255, null=True, verbose_name='日志文件路径'), + ), + migrations.AddField( + model_name='highmemoryprocess', + name='memory_percent', + field=models.FloatField(default=0, verbose_name='内存使用率'), + ), + migrations.AlterField( + model_name='highcpuprocess', + name='cpu_cores', + field=models.IntegerField(default=0, verbose_name='CPU核心数'), + ), + migrations.AlterField( + model_name='highcpuprocess', + name='cpu_usage', + field=models.FloatField(default=0, verbose_name='CPU使用率'), + ), + migrations.AlterField( + model_name='highcpuprocess', + name='status', + field=models.IntegerField(default=1, verbose_name='进程状态'), + ), + migrations.AlterField( + model_name='highgpuprocess', + name='gpu_usage', + field=models.FloatField(default=0, verbose_name='GPU使用率'), + ), + migrations.AlterField( + model_name='highgpuprocess', + name='status', + field=models.IntegerField(default=1, verbose_name='进程状态'), + ), + migrations.AlterField( + model_name='highmemoryprocess', + name='status', + field=models.IntegerField(default=1, verbose_name='进程状态'), + ), + migrations.AlterField( + model_name='highmemoryprocess', + name='swap_usage', + field=models.FloatField(default=0, verbose_name='交换内存使用量(GB)'), + ), + ] diff --git a/monitor/migrations/0003_allresourceprocess.py b/monitor/migrations/0003_allresourceprocess.py new file mode 100644 index 0000000..d8ba6d1 --- /dev/null +++ b/monitor/migrations/0003_allresourceprocess.py @@ -0,0 +1,41 @@ +# Generated by Django 5.1.6 on 2025-02-25 03:11 + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('monitor', '0002_alter_highcpuprocess_options_and_more'), + ] + + operations = [ + migrations.CreateModel( + name='AllResourceProcess', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('pid', models.IntegerField(verbose_name='进程ID')), + ('process_name', models.CharField(max_length=100, verbose_name='进程名称')), + ('cpu_usage', models.FloatField(default=0, verbose_name='CPU使用率')), + ('cpu_user_time', models.FloatField(default=0, verbose_name='用户态CPU时间')), + ('cpu_system_time', models.FloatField(default=0, verbose_name='系统态CPU时间')), + ('memory_usage', models.FloatField(default=0, verbose_name='内存使用量(MB)')), + ('memory_percent', models.FloatField(default=0, verbose_name='内存使用率')), + ('virtual_memory', models.FloatField(default=0, verbose_name='虚拟内存(MB)')), + ('gpu_usage', models.FloatField(default=0, verbose_name='GPU使用率')), + ('gpu_memory', models.FloatField(default=0, verbose_name='GPU内存使用量(MB)')), + ('net_io_sent', models.FloatField(default=0, verbose_name='网络发送量(MB)')), + ('net_io_recv', models.FloatField(default=0, verbose_name='网络接收量(MB)')), + ('is_active', models.BooleanField(default=True, verbose_name='是否活跃')), + ('status', models.IntegerField(default=1, verbose_name='状态')), + ('log_file', models.CharField(max_length=255, null=True, verbose_name='日志文件路径')), + ('created_at', models.DateTimeField(auto_now_add=True, verbose_name='创建时间')), + ('updated_at', models.DateTimeField(auto_now=True, verbose_name='更新时间')), + ], + options={ + 'verbose_name': '全资源监控', + 'verbose_name_plural': '全资源监控', + 'db_table': 'all_resource_process', + }, + ), + ] diff --git a/monitor/migrations/__pycache__/0002_alter_highcpuprocess_options_and_more.cpython-310.pyc b/monitor/migrations/__pycache__/0002_alter_highcpuprocess_options_and_more.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..2ec1759b4016def3a6544998ac48d0c7b8b64513 GIT binary patch literal 1861 zcmcIk&2QX96!+J9y&Gqnw6svlSAkY+k*dHEA=FaTh(k-o$Hj^?n(Ui+z4h8=YzJ}! zwGaukZ~HY&85K!KXy;&#yxK!Bk%$xUq^P4yC!}08F zQ^RNG_YMEVeogyBE~<}(i&OZe$8i7+^ib>Tp58Y+L)R8HFu=U5fywlqdCBxluwK*J zwJ~C~wjrARz~?jz;wTk%KL!@29cijQM{#iqzw~ci)0pPzpm_%9zV4aK>eb`{_*!2W zP#YLZV?!O^s>Xo^zEh2jyP7bg@KSaw@@z$JRpfm;vJG<;d5X7Tinn2kcPe%md!EII5(*mES`El zj)E-a!d+Ncc!mVH2x#cFx`E&ABzeN)4olMfnq^{v%_XE_Z^7SX`#SGTML zA$xs%6WLTWMpwQbZeAIEw!Zbt#*H7pAO3c^ZHd})NTZ9`k@Rg6vdG6ntSApdUdL^1 zle;(-4%Joy7J_J%E+mY1Sd?+>PA%VuLvhQkUpFz>+aImVOaWV=d6+4#!n;n)S(-~y z%+2W9Ps7biqfdXBD0VgrJI!d8r(?w~;YIqkKiC+(^R9wp_^0q@`W8)!tLKLiz4h}~ zqrsO|kH+gCKehN``+U149HPQZh-eamf1f;LD7%EbiLsP)AUMF7GmaA%=Ices{-Sk22c_=CA@K_OL<{wo(*G~m9kEeVQ>K8BH(Z6;wYg?aY&ANtdugo+VWJ zO?I~jDJz@M2#|oYlgS#F|8yI_^iLcd%`mN-R?W~I-TK$EZk>%%|NqZ(y7iCr#L#ai zxjWW-m7IZ!!FlY92J~p;$0uP~Wnbk~8K;>kq^B`f!g;=^qZeeorZSFm#v$`R+G-_^ literal 0 HcmV?d00001 diff --git a/monitor/migrations/__pycache__/0003_allresourceprocess.cpython-310.pyc b/monitor/migrations/__pycache__/0003_allresourceprocess.cpython-310.pyc new file mode 100644 index 0000000000000000000000000000000000000000..6e1266c365d146ae960b041dde86b818087bd43c GIT binary patch literal 1817 zcmZ{lTTdHD6vy{sjBRWnKxlfur*Ya+nxq$^s7e44<$(yb$%}7+pcFJvOKL%9iP<)>087rc zLjvzMNJ9H!Vp@X~eoH|bGK(rw2T~fckb}-e4Y>`st7DVj8rfFdMu|Y02!0>&;VnGiYp)$O_6<)rAhT-aFV|9af36)pzd}g)i zeyrB3_x_>FS`1TFi9HBeaion=6gWSDFqtMJ_AOwTQx{`{Aw4rYLQAgcf~cjz!LdcOKQ2AA}p#-L0!U>G{+T zyhc=$q*&#fO-vk5nNEpJ8zyV);m0WZ;p+X`&T4pj;c0bqck5B@=lQ(u9>7taumjVK ztCo>nMsQZq7PaeZwd#|&SXkn?6K&@ULII>jPzKSLO^a}CBfR~1-y#+DE?PwDxFXlq zcYm4>pDfqcR>Gx+ypNhDab^D#*`DZI)ULg)T}{en;8K&PV2b#3%4B&Rofqw%oHm#V z9&eYM>wLbGr%1FF2;mum8w~=J;d_>l&$0Z%N)WIzvw_(~gL$qdve7XTZM1Q5n!399 zk{vcmXK)LSi8Xlkob5B*1~H{aW~hA4jF}GgrD)i3Oh)aPa-*1`3%Ctp%sAMf@u~g2 z;tt^w(c_PcqbDcJ6@SLD@f4V4MSoHx&x*3^Q|}T65gjg*`+u7!k2i_O&2c<#%w`^C z8VAZ1SNw+&t>K-*M@rH3#9pFJQ~R_|wfleXo@hk-T|20$nwt1C@mp#yn&~_qNNJd* lcF7rlsVHSpmZHJGgNffx;)*!f|wD96D}UY$U~8MlK^;lJpvn zZKqR-E}RyzFq?>ur-yT>bE?D9aldrj^6Ia6fB1aA&-bU#kKdx802&;6015yA;B+5q z@NQFg2?v9AQ)_4;>}zp?X%xV{!GnMA4&V=-{+fj%m(4U?VEneB#DQ>cO=K+}41Nwyde^IX7YB%vK z5j}R6`bWgLx$aLe_bdNvFtwll>rAKA^|7w*)%hox^O%Jpe+BDi9V2x@I%k<^-r&HC?~ZLjrb&$OmH(c#;d#t#5%wru*~eXPeDIjs3B z>6!ES6O$d!-+o%(zL|9majC=EMY6FftlYHiY4t1a%#ysc=isEWCEHc6SAuFuIgRH!kYi}YomTp&m;&_9zVC-;)0 zu>U^0m1IYeg(N+sZJSr=9}c6NrnX(t z-OlgJbBO3NP^8JHR)@q?=I=0BNi8xkKn5yG^NCv>cOqS_)&!9$tctkuzyn-9hd+4( zG?&s)sOQCG79w(0vWQiAZXR~XF#~^|(%U)bd%RW zh4}n8GZ-;V+mLZwyRB{JLsg|FDHK(X4zv1^$HvB}Y|e1y7qSU-#~g4#!J&hJ=qLn| z4Mgr3;bfrDWqjl7CZLa-2fMjs#Mr{@%+Kz4Zc3xaiL6TnBbhL)DZ)4r4D<6x*Wcrc zkm>~4;SB3mC{@7>?Ino?;J=OQlRemi5|fa*UKG}nX@F_T>4;uVcOAm^;qkpWKM5CV z?&tvHiXz4Jlu5VaMIrdg;}9gF!NKW<#$#R4)53#^s2*u!ZC#u<|0zZq97GZ&1t;$0 z@aY+Y;Ng$;_!?!E0@|``q*ljr>!>(Bhmu+jU29W5}mo|D7h}|vJ@YGrC!HoiqJ(?BzgSE z$$qs8I&9gxM1NEa9#$c+D(oklyTshEiwB1{;He-nBaLT~5o?G_RElODf+3p5mZiXH z8v6WghWBk8q>7_cL+Zz#u^K5#`u%Ls)4anb(`Hdm`R%~FPD57m60;BY*)jn$;tOOu zPU3yk({E6ecat=CM9m{MgxE(1`SsfL*vM-gxi0(P<_ucK+to!X_9oUZ!%Z9g2zJAD z0|bO9xiEFn<#w}vd)kKU1v(lVG&~OSTHUOt5!oRJmF+^)l|Re4g}Ai zLW#Saqow@GKZ)S6SZo905J3rLv}nAqtJO5sxU&7>M@3)PMPq}9?;&E{W8s?D=e~IA zzr^~k9sM#v9DBnm5^xn~VM!s5-`%$lhzWU z5|E*+U}WJGQU{k8uFEFou6mw%W`&J0M-_}1RT-}$-$H5Tu response.json()) + .then(response => { + if (response.status === 'success') { + const data = response.data; + updateUI(data, type); + } + }) + .catch(error => handleError(error, '获取监控数据失败')); +} + +// 获取所有监控按钮和停止按钮 +const monitorButtons = document.querySelectorAll('[data-type]'); +const stopBtn = document.getElementById('stopMonitor'); +const pidInput = document.getElementById('pidInput'); + +// 保存当前监控类型的全局变量 +let currentMonitorType = null; + +// 开始监控 +function startMonitoring(pid) { + console.log('开始监控,类型:', currentMonitorType); // 调试日志 + updateMonitorData(pid); // 立即执行一次更新 + monitorInterval = setInterval(() => { + updateMonitorData(pid); + }, 60000); +} + +// 停止监控 +function stopMonitoring() { + if (!currentDirectory) { + console.error('没有正在监控的目录'); + showMessage('没有正在监控的目录', 'error'); + return; + } + + const stopBtn = document.getElementById('stopDirectoryMonitor'); + if (!stopBtn) return; + + stopBtn.disabled = true; + stopBtn.textContent = '正在停止...'; + + // 先清理定时器 + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + + const directoryToStop = currentDirectory; + + fetch('/monitor/stop-directory-monitor/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken(), + }, + body: JSON.stringify({ directory: directoryToStop }) + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + cleanupMonitoringState(); + showMessage('监控已停止', 'success'); + } else { + throw new Error(data.message || '停止监控失败'); + } + }) + .catch(error => { + console.error('停止监控失败:', error); + showMessage(error.message || '停止监控失败', 'error'); + stopBtn.disabled = false; + }) + .finally(() => { + stopBtn.textContent = '停止监控'; + }); +} + +// 初始化图表 +function initCharts() { + console.log('初始化图表'); + cpuChart = echarts.init(document.getElementById('cpuChart')); + memoryChart = echarts.init(document.getElementById('memoryChart')); + gpuChart = echarts.init(document.getElementById('gpuChart')); + + const baseOption = { + tooltip: { + trigger: 'axis', + formatter: function(params) { + const time = new Date(params[0].value[0]).toLocaleTimeString(); + return `${time}
${params[0].seriesName}: ${params[0].value[1].toFixed(2)}%`; + } + }, + xAxis: { + type: 'time', + splitLine: { show: false } + }, + yAxis: { + type: 'value', + min: 0, + max: 100, + splitLine: { show: true } + }, + grid: { + left: '3%', + right: '4%', + bottom: '3%', + containLabel: true + }, + series: [{ + type: 'line', + showSymbol: false, + data: [], + smooth: true, + areaStyle: { + opacity: 0.1 + } + }] + }; + + cpuChart.setOption({ + ...baseOption, + series: [{ + ...baseOption.series[0], + name: 'CPU使用率', + itemStyle: { color: '#2196F3' } + }] + }); + + memoryChart.setOption({ + ...baseOption, + series: [{ + ...baseOption.series[0], + name: '内存使用量', + itemStyle: { color: '#4CAF50' } + }] + }); + + gpuChart.setOption({ + ...baseOption, + series: [{ + ...baseOption.series[0], + name: 'GPU使用率', + itemStyle: { color: '#FF5722' } + }] + }); +} + +// 更新图表数据 +function updateCharts(process) { + console.log('更新图表:', process); + const now = new Date(); + + // 更新CPU图表 + if (cpuChart) { + updateChartData(cpuChart, now, process.cpu_usage); + } + + // 更新内存图表 + if (memoryChart) { + updateChartData(memoryChart, now, process.memory_usage); + } + + // 更新GPU图表 + if (gpuChart && process.gpu_info) { + updateChartData(gpuChart, now, process.gpu_info.usage); + } +} + +// 辅助函数:更新图表数据 +function updateChartData(chart, time, value) { + if (!chart) return; + + try { + const option = chart.getOption(); + const data = option.series[0].data || []; + data.push([time, value]); + + // 保持最近60个数据点 + if (data.length > 60) { + data.shift(); + } + + chart.setOption({ + series: [{ + data: data + }] + }); + } catch (error) { + console.error('更新图表数据失败:', error); + } +} + +// 修改更新进程表格函数 +function updateProcessTable(processes) { + console.log('更新进程表格:', processes); // 调试日志 + const table = document.getElementById('processTable'); + if (!table) { + console.error('找不到进程表格元素'); + return; + } + + if (!processes || processes.length === 0) { + table.innerHTML = ` + + 暂无进程数据 + + `; + return; + } + + table.innerHTML = processes.map(proc => ` + + ${proc.pid} + ${proc.name} + +
+
+
+
+
+ ${proc.cpu_usage.toFixed(1)}% +
+ + +
+
+
+
+
+ ${proc.memory_usage.toFixed(1)} MB +
+ + +
+
+
+
+
+ ${proc.gpu_info.usage}% +
+ + ${proc.gpu_info.memory.toFixed(1)} MB + + 运行中 + + + + `).join(''); +} + +// 修改选择进程的函数 +function selectProcess(pid) { + console.log('选择进程:', pid); + selectedPid = pid.toString(); // 确保转换为字符串 + + // 更新选择器 + const selector = document.getElementById('processSelector'); + if (selector) { + selector.value = selectedPid; + } + + // 更新表格中的选中状态 + const rows = document.querySelectorAll('#processTable tr'); + rows.forEach(row => { + if (row.cells && row.cells[0] && row.cells[0].textContent === selectedPid) { + row.classList.add('table-primary'); + } else { + row.classList.remove('table-primary'); + } + }); + + // 获取最新数据并更新图表 + if (currentDirectory) { + fetch(`/monitor/directory-status/?directory=${encodeURIComponent(currentDirectory)}`) + .then(response => response.json()) + .then(data => { + if (data.status === 'success' && data.processes) { + const selectedProcess = data.processes.find(p => p.pid.toString() === selectedPid); + if (selectedProcess) { + // 更新图表 + updateCharts(selectedProcess); + // 更新详细信息 + updateProcessDetails(selectedProcess); + } + } + }) + .catch(error => console.error('获取进程详情失败:', error)); + } +} + +// 修改进程选择器更新函数 +function updateProcessSelector(processes) { + console.log('更新进程选择器,进程列表:', processes); // 调试日志 + + const selector = document.getElementById('processSelector'); + const processInfo = document.getElementById('selectedProcessInfo'); + + if (!selector) { + console.error('找不到进程选择器元素'); + return; + } + + // 启用选择器 + selector.disabled = false; + + // 获取当前的进程列表 + const currentPids = processes.map(p => p.pid.toString()); + console.log('当前PID列表:', currentPids); // 调试日志 + + // 如果当前选中的进程不在列表中,清除选择 + if (selectedPid && !currentPids.includes(selectedPid.toString())) { + console.log('选中的进程不再存在,清除选择'); // 调试日志 + selectedPid = null; + clearCharts(); + } + + // 移除旧的事件监听器 + const newSelector = selector.cloneNode(true); + selector.parentNode.replaceChild(newSelector, selector); + + // 更新选择器选项 + newSelector.innerHTML = ` + + ${processes.map(proc => ` + + `).join('')} + `; + + // 添加新的事件监听器 + newSelector.addEventListener('change', function() { + console.log('进程选择变更:', this.value); + selectedPid = this.value; + if (selectedPid) { + const selected = processes.find(p => p.pid === parseInt(selectedPid)); + if (selected) { + processInfo.textContent = `监控中: PID ${selected.pid} - ${selected.name}`; + updateProcessDetails(selected); // 更新详细信息 + clearCharts(); + } + } else { + processInfo.textContent = '未选择进程'; + clearCharts(); + // 清空详细信息 + ['basicInfo', 'resourceInfo', 'networkInfo', 'fileInfo'].forEach(id => { + document.getElementById(id).innerHTML = '

选择进程查看详细信息

'; + }); + } + }); + + console.log('进程选择器更新完成'); // 调试日志 +} + +// 添加清除图表数据函数 +function clearCharts() { + const emptyOption = { + series: [{ + data: [] + }] + }; + cpuChart.setOption(emptyOption); + memoryChart.setOption(emptyOption); + gpuChart.setOption(emptyOption); +} + +document.addEventListener('DOMContentLoaded', function() { + console.log('DOM加载完成,开始初始化'); + + const startBtn = document.getElementById('startDirectoryMonitor'); + const stopBtn = document.getElementById('stopDirectoryMonitor'); + + if (!startBtn || !stopBtn) { + console.error('找不到监控按钮元素'); + return; + } + + // 初始化图表 + initCharts(); + + // 开始监控按钮事件 + startBtn.addEventListener('click', function() { + console.log('点击了开始监控按钮'); + const directory = document.getElementById('directoryInput').value; + const statusDiv = document.getElementById('directoryMonitorStatus'); + + if (!directory) { + statusDiv.style.display = 'block'; + statusDiv.className = 'alert alert-warning'; + statusDiv.textContent = '请输入目录路径'; + return; + } + + // 显示加载状态 + startBtn.disabled = true; + startBtn.textContent = '启动中...'; + statusDiv.style.display = 'block'; + statusDiv.className = 'alert alert-info'; + statusDiv.textContent = '正在启动监控...'; + + console.log('开始监控目录:', directory); + + // 清除旧的选择 + selectedPid = null; + clearCharts(); + + fetch('/monitor/scan-directory/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + }, + body: JSON.stringify({ directory: directory }) + }) + .then(response => response.json()) + .then(data => { + console.log('接口返回:', data); + if (data.status === 'success') { + currentDirectory = directory; + statusDiv.className = 'alert alert-success'; + statusDiv.textContent = data.message; + startBtn.disabled = true; + stopBtn.disabled = false; + + // 开始定期获取详细信息 + if (updateInterval) { + clearInterval(updateInterval); + } + currentDirectory = directory; + getProcessDetails(directory); // 立即获取一次 + updateInterval = setInterval(() => getProcessDetails(directory), 5000); // 每5秒更新一次 + } else { + statusDiv.className = 'alert alert-danger'; + statusDiv.textContent = data.message; + startBtn.disabled = false; + } + }) + .catch(error => { + console.error('监控启动失败:', error); + statusDiv.className = 'alert alert-danger'; + statusDiv.textContent = '启动监控失败: ' + error.message; + startBtn.disabled = false; + }) + .finally(() => { + startBtn.textContent = '开始监控'; + }); + }); + + // 停止监控按钮事件 + if (stopBtn) { + stopBtn.addEventListener('click', function() { + console.log('停止监控,当前目录:', currentDirectory); + + // 检查是否有当前目录 + if (!currentDirectory) { + console.error('没有正在监控的目录'); + showMessage('没有正在监控的目录', 'error'); + return; + } + + // 禁用按钮,防止重复点击 + stopBtn.disabled = true; + stopBtn.textContent = '正在停止...'; + + // 先清理定时器,防止继续发送请求 + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + + // 保存当前目录的副本 + const directoryToStop = currentDirectory; + + // 发送停止监控请求到服务器 + fetch('/monitor/stop-directory-monitor/', { + method: 'POST', + headers: { + 'Content-Type': 'application/json', + 'X-CSRFToken': getCSRFToken(), + }, + body: JSON.stringify({ + directory: directoryToStop, + timestamp: new Date().getTime() // 添加时间戳防止缓存 + }) + }) + .then(response => { + if (!response.ok) { + throw new Error('Network response was not ok'); + } + return response.json(); + }) + .then(data => { + console.log('停止监控响应:', data); + if (data.status === 'success') { + // 清理前端状态 + cleanupMonitoringState(); + // 显示成功消息 + showMessage('监控已停止', 'success'); + } else { + throw new Error(data.message || '停止监控失败'); + } + }) + .catch(error => { + console.error('停止监控失败:', error); + showMessage(error.message || '停止监控请求失败', 'error'); + // 恢复按钮状态 + stopBtn.disabled = false; + stopBtn.textContent = '停止监控'; + }); + }); + } + + // 初始状态设置 + stopBtn.disabled = true; // 初始状态下停止按钮禁用 + console.log('初始化完成'); + + // 为每个监控按钮添加点击事件 + monitorButtons.forEach(button => { + button.addEventListener('click', function() { + const pid = pidInput.value.trim(); + const type = this.dataset.type; // 从按钮的 data-type 属性获取类型 + + if (!pid) { + alert('请输入进程ID'); + return; + } + + // 保存当前选择的监控类型 + currentMonitorType = type; + console.log('设置监控类型为:', currentMonitorType); + + fetch(`/monitor/start/?pid=${pid}&type=${type}`) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + startMonitoring(pid); + // 禁用所有监控按钮 + monitorButtons.forEach(btn => btn.disabled = true); + stopBtn.disabled = false; + pidInput.disabled = true; + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('启动监控失败:', error); + alert('启动监控失败'); + }); + }); + }); + + // 获取自动检测按钮 + const startAutoDetectBtn = document.getElementById('startAutoDetectBtn'); + const stopAutoDetectBtn = document.getElementById('stopAutoDetectBtn'); + + // 获取CSRF Token + function getCSRFToken() { + const name = 'csrftoken'; + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; + } + + // 添加自动检测开始按钮事件 + startAutoDetectBtn.addEventListener('click', function() { + fetch('/auto_detect/', { + method: 'POST', + headers: { + 'X-CSRFToken': getCSRFToken(), + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + alert('已开始自动检测高资源进程'); + startAutoDetectBtn.disabled = true; + stopAutoDetectBtn.disabled = false; + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('启动自动检测失败:', error); + alert('启动自动检测失败'); + }); + }); + + // 添加自动检测停止按钮事件 + stopAutoDetectBtn.addEventListener('click', function() { + fetch('/stop_auto_detect/', { + method: 'POST', + headers: { + 'X-CSRFToken': getCSRFToken(), + 'Content-Type': 'application/json' + } + }) + .then(response => response.json()) + .then(data => { + if (data.status === 'success') { + alert('已停止自动检测'); + startAutoDetectBtn.disabled = false; + stopAutoDetectBtn.disabled = true; + } else { + alert(data.message); + } + }) + .catch(error => { + console.error('停止自动检测失败:', error); + alert('停止自动检测失败'); + }); + }); +}); + +// 更新 UI 的函数 +function updateUI(data, type) { + // 根据监控类型更新对应的表格 + if (type === 'all' || type === 'cpu') { + document.getElementById('cpuTable').innerHTML = ` + 使用率${data.cpu.usage} + 用户态时间${data.cpu.user_time} + 内核态时间${data.cpu.system_time} + CPU核心数${data.cpu.cores} + CPU频率${data.cpu.frequency} + 上下文切换${data.cpu.context_switches} + `; + document.getElementById('cpuStatus').className = 'badge bg-success status-badge'; + document.getElementById('cpuStatus').textContent = '监控中'; + } + + if (type === 'all' || type === 'memory') { + document.getElementById('memoryTable').innerHTML = ` + 物理内存${data.memory.physical} + 虚拟内存${data.memory.virtual} + 内存映射${data.memory.mappings} + 系统内存使用${data.memory.system_usage} + 交换空间使用${data.memory.swap_usage} + `; + document.getElementById('memoryStatus').className = 'badge bg-success status-badge'; + document.getElementById('memoryStatus').textContent = '监控中'; + } + + if (type === 'all' || type === 'gpu') { + document.getElementById('gpuTable').innerHTML = ` + 使用率${data.gpu.usage} + 显存使用${data.gpu.memory} + `; + document.getElementById('gpuStatus').className = 'badge bg-success status-badge'; + document.getElementById('gpuStatus').textContent = '监控中'; + } + + // 更新最后更新时间 + document.getElementById('lastUpdate').textContent = + `最后更新: ${data.timestamp}`; +} + +// 添加错误处理函数 +function handleError(error, message) { + console.error(message, error); + ['cpu', 'gpu', 'memory'].forEach(type => { + const statusElement = document.getElementById(`${type}Status`); + if (statusElement) { + statusElement.className = 'badge bg-danger status-badge'; + statusElement.textContent = '错误'; + } + }); + alert(message); +} + +// 窗口大小改变时调整图表大小 +window.addEventListener('resize', function() { + if (cpuChart) cpuChart.resize(); + if (memoryChart) memoryChart.resize(); + if (gpuChart) gpuChart.resize(); +}); + +// 更新进程详细信息的函数 +function updateProcessDetails(process) { + if (!process) return; + + // 检查元素是否存在 + const basicInfo = document.getElementById('basicInfo'); + const resourceInfo = document.getElementById('resourceInfo'); + + if (!basicInfo || !resourceInfo) { + console.error('找不到详情显示元素'); + return; + } + + try { + // 更新基本信息 + basicInfo.innerHTML = ` +
+
PID
+
${process.pid}
+ +
进程名
+
${process.name}
+ +
命令行
+
${process.command_line}
+ +
工作目录
+
${process.working_directory}
+ +
创建时间
+
${process.create_time}
+
+ `; + + // 更新资源信息 + resourceInfo.innerHTML = ` +
+
CPU使用率
+
${process.cpu_usage.toFixed(2)}%
+ +
内存使用
+
${process.memory_usage.toFixed(2)} MB
+ +
线程数
+
${process.threads}
+ +
GPU使用率
+
${process.gpu_info.usage}%
+ +
GPU内存
+
${process.gpu_info.memory.toFixed(2)} MB
+
+ `; + } catch (error) { + console.error('更新进程详情失败:', error); + } +} + +// 修改定期更新函数 +function startPeriodicUpdate(directory) { + console.log('开始定期更新, 目录:', directory); + + if (updateInterval) { + clearInterval(updateInterval); + } + + // 立即执行一次更新 + getProcessDetails(directory); + + // 设置定时更新 + updateInterval = setInterval(() => { + getProcessDetails(directory); + }, 5000); +} + +// 修改获取进程详情的函数 +function getProcessDetails(directory) { + if (!directory || !currentDirectory) { // 添加currentDirectory检查 + console.log('没有目录或已停止监控'); + return; + } + + // 使用 AbortController 来控制fetch请求 + if (window.currentFetch) { + window.currentFetch.abort(); + } + window.currentFetch = new AbortController(); + + fetch(`/monitor/directory-status/?directory=${encodeURIComponent(directory)}`, { + signal: window.currentFetch.signal + }) + .then(response => response.json()) + .then(data => { + if (!currentDirectory) { // 再次检查是否已停止监控 + console.log('监控已停止,不更新数据'); + return; + } + + console.log('收到进程数据:', data); + if (data.status === 'success' && data.processes && data.processes.length > 0) { + updateProcessTable(data.processes); + + if (selectedPid) { + const selectedProcess = data.processes.find(p => p.pid.toString() === selectedPid); + if (selectedProcess) { + updateCharts(selectedProcess); + updateProcessDetails(selectedProcess); + } + } + } + }) + .catch(error => { + if (error.name === 'AbortError') { + console.log('请求被中止'); + } else { + console.error('获取进程详情失败:', error); + } + }); +} + +// 获取所有正在监控的进程ID +function getAllMonitoredPids() { + const table = document.getElementById('processTable'); + if (!table) return []; + + const pids = []; + const rows = table.getElementsByTagName('tr'); + for (let row of rows) { + const firstCell = row.cells[0]; + if (firstCell && !isNaN(firstCell.textContent)) { + pids.push(firstCell.textContent); + } + } + return pids; +} + +// 修改清理监控状态的函数 +function cleanupMonitoringState() { + console.log('清理监控状态'); + + // 先取消所有正在进行的请求 + if (window.currentFetch) { + window.currentFetch.abort(); + window.currentFetch = null; + } + + // 清除定时器 + if (updateInterval) { + clearInterval(updateInterval); + updateInterval = null; + } + + // 清除状态变量(在清理完其他内容后再清除) + const oldDirectory = currentDirectory; + currentDirectory = null; + selectedPid = null; + + console.log('已清除的目录:', oldDirectory); + + // 清空图表 + clearCharts(); + + // 清空进程列表 + const processTable = document.getElementById('processTable'); + if (processTable) { + processTable.innerHTML = ` + + 未开始监控 + + `; + } + + // 清空详细信息 + const basicInfo = document.getElementById('basicInfo'); + const resourceInfo = document.getElementById('resourceInfo'); + + if (basicInfo) { + basicInfo.innerHTML = '

未选择进程

'; + } + if (resourceInfo) { + resourceInfo.innerHTML = '

未选择进程

'; + } + + // 更新按钮状态 + const startBtn = document.getElementById('startDirectoryMonitor'); + const stopBtn = document.getElementById('stopDirectoryMonitor'); + if (startBtn) startBtn.disabled = false; + if (stopBtn) { + stopBtn.disabled = true; + stopBtn.textContent = '停止监控'; + } +} + +// 添加显示消息的函数 +function showMessage(message, type = 'info') { + const statusDiv = document.getElementById('directoryMonitorStatus'); + if (statusDiv) { + statusDiv.style.display = 'block'; + statusDiv.className = `alert alert-${type}`; + statusDiv.textContent = message; + + // 3秒后自动隐藏 + setTimeout(() => { + statusDiv.style.display = 'none'; + }, 3000); + } +} \ No newline at end of file diff --git a/monitor/templates/high_resource_list.html b/monitor/templates/high_resource_list.html new file mode 100644 index 0000000..0cc59ff --- /dev/null +++ b/monitor/templates/high_resource_list.html @@ -0,0 +1,176 @@ +{% load static %} + + + + + + 高资源进程列表 + + + + + + +
+

高资源进程列表

+ + +
+ + + +
+ + +
+

CPU密集型进程

+ + + + + + + + + + + + {% for process in cpu_processes %} + + + + + + + + {% empty %} + + + + {% endfor %} + +
PID进程名CPU使用率状态最后更新时间
{{ process.pid }}{{ process.process_name }}{{ process.cpu_usage }}% + + {{ process.get_status_display }} + + {{ process.updated_at|date:"Y-m-d H:i:s" }}
暂无数据
+
+ + + + + + +
+ + + + + \ No newline at end of file diff --git a/monitor/templates/index.html b/monitor/templates/index.html new file mode 100644 index 0000000..5ec48d1 --- /dev/null +++ b/monitor/templates/index.html @@ -0,0 +1,298 @@ +{% load static %} + + + + + + 进程监控 + + + + + + + + + {% csrf_token %} +
+ + + + +
+
+ + +
+
+ + +
+
+
+ + + + + +
+
+
+ + +
+ +
+
+
+
CPU监控
+ 未监控 +
+
+ + + + + + + + + + + + +
指标
暂无数据
+
+
+
+ + +
+
+
+
GPU监控
+ 未监控 +
+
+ + + + + + + + + + + + +
指标
暂无数据
+
+
+
+ + +
+
+
+
内存监控
+ 未监控 +
+
+ + + + + + + + + + + + +
指标
暂无数据
+
+
+
+
+ + +
+
+
目录监控
+
+
+
+
+ +
+
+ + +
+
+ +
+
+ + +
+
+
进程列表
+
+
+
+ + + + + + + + + + + + + + + + + + +
PID进程信息CPU内存IO线程状态操作
等待数据加载...
+
+
+
+ + +
+
+
+
CPU使用率
+
+
+
+
+
+
+
+
内存使用量
+
+
+
+
+
+
+
+
GPU使用率
+
+
+
+
+
+
+ + +
+
+
+
+
基本信息
+
+
+

选择进程查看详细信息

+
+
+
+
+
+
+
资源使用
+
+
+

选择进程查看详细信息

+
+
+
+
+ + +
+
+

最后更新: -

+
+
+
+ + + + + + + \ No newline at end of file diff --git a/monitor/urls.py b/monitor/urls.py index 4fae023..351ee92 100644 --- a/monitor/urls.py +++ b/monitor/urls.py @@ -2,8 +2,17 @@ from django.urls import path from . import views urlpatterns = [ - path('start_monitor/', views.start_monitor, name='start_monitor'), - path('stop_monitor//', views.stop_monitor, name='stop_monitor'), - path('metrics//', views.get_process_metrics, name='get_process_metrics'), - path('auto_detect/', views.auto_detect_monitor, name='auto_detect_monitor'), + path('', views.index, name='index'), + path('api/process//status/', views.get_process_status, name='get_process_status'), + path('monitor/start/', views.start_monitor, name='start_monitor'), + path('monitor/stop/', views.stop_monitor, name='stop_monitor'), + path('auto_detect/', views.auto_detect_high_resource_processes, name='auto_detect'), + path('stop_auto_detect/', views.stop_auto_detect, name='stop_auto_detect'), + path('high-resource/', views.high_resource_list, name='high_resource_list'), + path('scan-directory/', views.scan_directory, name='scan_directory'), + path('start/', views.start_monitor, name='start_monitor'), + path('stop/', views.stop_monitor, name='stop_monitor'), + path('status//', views.get_process_status, name='get_process_status'), + path('stop-directory-monitor/', views.stop_directory_monitor, name='stop_directory_monitor'), + path('directory-status/', views.get_directory_status, name='directory_status'), ] diff --git a/monitor/views.py b/monitor/views.py index 442a655..27f47eb 100644 --- a/monitor/views.py +++ b/monitor/views.py @@ -2,7 +2,7 @@ from django.http import JsonResponse from .tasks import monitor_process, get_process_gpu_usage import threading import psutil -from .models import HighCPUProcess, HighGPUProcess, HighMemoryProcess +from .models import HighCPUProcess, HighGPUProcess, HighMemoryProcess, AllResourceProcess import logging import os from datetime import datetime @@ -11,23 +11,348 @@ import nvidia_smi from django.utils import timezone from django.views.decorators.http import require_http_methods from django.views.decorators.csrf import csrf_exempt +from django.shortcuts import render +import pytz +from pathlib import Path # 使用 pathlib 处理跨平台路径 +import json +from django.conf import settings -# 配置日志 -LOG_DIR = 'logs/process_monitor' -os.makedirs(LOG_DIR, exist_ok=True) -def setup_logger(pid): - """为每个进程设置独立的日志记录器""" - log_file = os.path.join(LOG_DIR, f'process_{pid}_{datetime.now().strftime("%Y%m%d")}.log') - logger = logging.getLogger(f'process_{pid}') - logger.setLevel(logging.INFO) + +import threading +directory_monitoring = {} + +# 全局变量来控制检测线程 +monitor_thread = None +is_monitoring = False + +# 在文件开头定义日志目录 +BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +LOG_DIR = os.path.join(BASE_DIR, 'logs', 'process_monitor') + +# 确保基础目录结构存在,添加 'all' 目录 +for resource_type in ['cpu', 'memory', 'gpu', 'all']: + os.makedirs(os.path.join(LOG_DIR, resource_type), exist_ok=True) + +# 获取应用专属的logger +logger = logging.getLogger('monitor') + +# 全局变量来跟踪监控的目录 +monitored_directories = set() + +# 添加新的监控线程函数 +def monitor_directory(directory): + """持续监控目录的后台任务""" + monitor_interval = settings.MONITOR_INTERVAL # 监控间隔(5秒) + log_interval = settings.LOG_INTERVAL # 日志写入间隔(60秒) + processed_pids = set() # 用于跟踪已处理的PID + last_log_time = {} # 每个进程的最后日志时间 - handler = logging.FileHandler(log_file) - formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') - handler.setFormatter(formatter) - logger.addHandler(handler) + while directory_monitoring.get(directory, False): + try: + next_update = timezone.now() + timezone.timedelta(seconds=monitor_interval) + + processes = find_python_processes_in_dir(directory) + for proc in processes: + pid = proc['pid'] + try: + # 设置日志文件 + log_file = setup_process_logger(pid, 'all') + if log_file: + process = psutil.Process(pid) + data = get_process_data(process) + + # 更新数据库 + AllResourceProcess.objects.update_or_create( + pid=pid, + defaults={ + 'process_name': process.name(), + 'cpu_usage': float(data['cpu']['usage'].replace('%', '')), + 'cpu_user_time': float(data['cpu']['user_time'].replace('s', '')), + 'cpu_system_time': float(data['cpu']['system_time'].replace('s', '')), + 'memory_usage': float(data['memory']['physical'].split('MB')[0].strip()), + 'memory_percent': float(data['memory']['physical'].split('(')[1].split('%')[0].strip()), + 'virtual_memory': float(data['memory']['virtual'].split('MB')[0].strip()), + 'gpu_usage': float(data['gpu']['usage'].replace('%', '')), + 'gpu_memory': float(data['gpu']['memory'].replace('MB', '')), + 'net_io_sent': float(data['io']['write'].split('MB')[0].strip()), + 'net_io_recv': float(data['io']['read'].split('MB')[0].strip()), + 'is_active': True, + 'status': 1, + 'log_file': log_file + } + ) + + now = timezone.now() + # 如果是新检测到的进程,立即写入日志 + if pid not in processed_pids: + print(f"首次检测到进程,立即写入日志: PID={pid}") # 调试日志 + log_process_metrics(pid, data, log_file) + processed_pids.add(pid) + last_log_time[pid] = now + # 对于已知进程,每隔一分钟写入一次 + elif (now - last_log_time.get(pid, now)).total_seconds() >= log_interval: + print(f"定期写入日志: PID={pid}") # 调试日志 + log_process_metrics(pid, data, log_file) + last_log_time[pid] = now + + except Exception as e: + logger.error(f"监控进程 {pid} 失败: {str(e)}") + + # 计算需要等待的时间 + now = timezone.now() + if next_update > now: + time.sleep((next_update - now).total_seconds()) + + except Exception as e: + logger.error(f"目录监控出错: {str(e)}") + time.sleep(5) + +def get_python_processes(directory): + """获取指定目录下的Python进程""" + directory = str(Path(directory).resolve()).lower() + processes = [] - return logger, log_file + for proc in psutil.process_iter(['pid', 'name', 'cmdline']): + try: + if 'python' in proc.info['name'].lower(): + process = psutil.Process(proc.info['pid']) + try: + proc_cwd = str(Path(process.cwd()).resolve()).lower() + if directory in proc_cwd or proc_cwd.startswith(directory): + processes.append(process) + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + except (psutil.NoSuchProcess, psutil.AccessDenied): + continue + + return processes + +def get_process_info(process): + """获取进程详细信息""" + try: + with process.oneshot(): + info = { + 'pid': process.pid, + 'name': process.name(), + 'cpu_usage': process.cpu_percent(), + 'memory_usage': process.memory_info().rss / (1024 * 1024), # MB + 'command_line': ' '.join(process.cmdline()), + 'create_time': process.create_time(), + 'status': process.status(), + 'threads': process.num_threads(), + 'working_directory': process.cwd(), + 'gpu_info': { + 'usage': 0, + 'memory': 0 + } + } + + # 获取IO信息 + try: + io = process.io_counters() + info['io'] = { + 'read_bytes': io.read_bytes / (1024 * 1024), # MB + 'write_bytes': io.write_bytes / (1024 * 1024), # MB + 'read_count': io.read_count, + 'write_count': io.write_count + } + except (psutil.NoSuchProcess, psutil.AccessDenied): + info['io'] = { + 'read_bytes': 0, + 'write_bytes': 0, + 'read_count': 0, + 'write_count': 0 + } + + return info + except (psutil.NoSuchProcess, psutil.AccessDenied) as e: + logger.error(f"获取进程 {process.pid} 信息失败: {str(e)}") + return None + +@csrf_exempt +@require_http_methods(["POST"]) +def scan_directory(request): + """扫描目录下的 Python 进程""" + try: + data = json.loads(request.body) + directory = data.get('directory') + + if not directory: + return JsonResponse({ + 'status': 'error', + 'message': '请提供目录路径' + }) + + # 添加到监控目录集合 + directory = str(Path(directory).resolve()) + monitored_directories.add(directory) + logger.info(f"开始监控目录: {directory}") + + return JsonResponse({ + 'status': 'success', + 'message': f'开始监控目录: {directory}', + 'directory': directory + }) + + except Exception as e: + logger.error(f"启动目录监控失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + +@require_http_methods(["GET"]) +def get_directory_status(request): + """获取目录下的进程状态""" + try: + directory = request.GET.get('directory') + if not directory: + return JsonResponse({ + 'status': 'error', + 'message': '未提供目录路径' + }) + + # 获取目录下的Python进程 + processes = get_python_processes(directory) + + # 获取每个进程的详细信息 + process_info = [] + for proc in processes: + info = get_process_info(proc) + if info: + process_info.append(info) + + return JsonResponse({ + 'status': 'success', + 'processes': process_info, + 'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S') + }) + + except Exception as e: + logger.error(f"获取目录状态失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + +@require_http_methods(["POST"]) +def stop_directory_monitor(request): + """停止目录监控""" + try: + data = json.loads(request.body) + directory = data.get('directory') + + if not directory: + return JsonResponse({ + 'status': 'error', + 'message': '未提供目录路径' + }) + + # 从监控集合中移除 + directory = str(Path(directory).resolve()) + if directory in monitored_directories: + monitored_directories.remove(directory) + logger.info(f"停止监控目录: {directory}") + + return JsonResponse({ + 'status': 'success', + 'message': '监控已停止' + }) + + except Exception as e: + logger.error(f"停止监控失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + +def setup_process_logger(pid, monitor_type): + """为进程设置独立的日志记录器""" + try: + # 验证监控类型 + if monitor_type not in ['cpu', 'memory', 'gpu', 'all']: + print(f"警告:无效的监控类型 {monitor_type}") + return None + + # 获取当前时间(使用本地时区) + local_tz = pytz.timezone('Asia/Shanghai') + current_time = timezone.localtime(timezone.now(), local_tz) + current_date = current_time.strftime("%Y%m%d") + + # 如果是从目录扫描启动的监控,使用 'all' 类型 + if monitor_type == 'scan': + monitor_type = 'all' + + # 构建日志文件路径 + log_dir = os.path.join(LOG_DIR, monitor_type) + log_file = os.path.join(log_dir, f'{pid}_{current_date}_{monitor_type}.log') + + # 确保目录存在 + os.makedirs(os.path.dirname(log_file), exist_ok=True) + + print(f"设置日志文件: {log_file}") # 调试信息 + return log_file + + except Exception as e: + print(f"设置日志文件失败: {str(e)}") + raise + +def log_process_metrics(pid, data, log_file): + """记录进程指标到日志文件""" + try: + # 确保目录存在 + os.makedirs(os.path.dirname(log_file), exist_ok=True) + + # 获取当前时间(使用本地时区) + local_tz = pytz.timezone('Asia/Shanghai') # 使用中国时区 + current_time = timezone.localtime(timezone.now(), local_tz) + timestamp = current_time.strftime('%Y-%m-%d %H:%M:%S') + + # 格式化日志内容 + log_content = f"=== {timestamp} ===\n" + + # CPU信息 + log_content += "CPU信息:\n" + log_content += f"├─ 使用率: {data['cpu']['usage']}\n" + log_content += f"├─ 用户态时间: {data['cpu']['user_time']}\n" + log_content += f"├─ 内核态时间: {data['cpu']['system_time']}\n" + log_content += f"├─ CPU核心数: {data['cpu']['cores']}\n" + log_content += f"├─ CPU频率: {data['cpu']['frequency']}\n" + log_content += f"└─ 上下文切换: {data['cpu']['context_switches']}\n" + + # 内存信息 + log_content += "内存信息:\n" + log_content += f"├─ 物理内存: {data['memory']['physical']}\n" + log_content += f"├─ 虚拟内存: {data['memory']['virtual']}\n" + log_content += f"├─ 内存映射: {data['memory']['mappings']}\n" + log_content += f"├─ 系统内存使用: {data['memory']['system_usage']}\n" + log_content += f"└─ 交换空间使用: {data['memory']['swap_usage']}\n" + + # GPU信息 + log_content += "GPU信息:\n" + log_content += f"├─ 使用率: {data['gpu']['usage']}\n" + log_content += f"└─ 显存使用: {data['gpu']['memory']}\n" + + # IO信息 + log_content += "IO信息:\n" + log_content += f"├─ 读取: {data['io']['read']}\n" + log_content += f"├─ 写入: {data['io']['write']}\n" + log_content += f"└─ 其他IO: {data['io']['other']}\n" + + # 进程状态 + log_content += f"进程状态: {data['status']}\n" + log_content += "-" * 50 + "\n" + + # 写入日志文件 + with open(log_file, 'a', encoding='utf-8') as f: + f.write(log_content) + + print(f"已写入日志到: {log_file}") # 调试信息 + + except Exception as e: + print(f"写入日志失败: {str(e)}") + raise def get_process_by_name(process_name): """根据进程名称获取进程PID""" @@ -43,427 +368,647 @@ def get_process_by_name(process_name): def get_process_gpu_usage(pid): """获取进程的GPU使用情况""" try: - nvidia_smi.nvmlInit() - deviceCount = nvidia_smi.nvmlDeviceGetCount() - gpu_usage = 0 - gpu_memory = 0 - - for i in range(deviceCount): - handle = nvidia_smi.nvmlDeviceGetHandleByIndex(i) - processes = nvidia_smi.nvmlDeviceGetComputeRunningProcesses(handle) - for process in processes: - if process.pid == pid: - gpu_memory = process.usedGpuMemory / 1024 / 1024 # 转换为MB - gpu_usage = nvidia_smi.nvmlDeviceGetUtilizationRates(handle).gpu - return gpu_usage, gpu_memory - - return 0, 0 - except: - return 0, 0 - finally: - try: - nvidia_smi.nvmlShutdown() - except: - pass + # 如果没有GPU或无法获取GPU信息,返回默认值 + return 0.0, 0.0 + except Exception as e: + logger.error(f"获取GPU信息失败: {str(e)}") + return 0.0, 0.0 def get_high_resource_processes(): """获取高资源占用的进程""" high_resource_pids = [] - for proc in psutil.process_iter(['pid', 'name']): + for proc in psutil.process_iter(['pid', 'name', 'cpu_percent', 'memory_percent']): try: - # 获取进程信息 - process = psutil.Process(proc.info['pid']) - memory_gb = process.memory_info().rss / (1024 * 1024 * 1024) # 转换为GB - - # 获取GPU使用情况 - gpu_usage, gpu_memory = get_process_gpu_usage(proc.info['pid']) - - # 检查是否满足条件(GPU使用率>50%) - if gpu_usage > 50: + if proc.info['cpu_percent'] > 50 or proc.info['memory_percent'] > 50: high_resource_pids.append({ 'pid': proc.info['pid'], 'name': proc.info['name'], - 'memory_gb': round(memory_gb, 2), - 'gpu_usage': gpu_usage, - 'gpu_memory': round(gpu_memory, 2) # GPU显存使用量(MB) + 'cpu_percent': proc.info['cpu_percent'], + 'memory_percent': proc.info['memory_percent'] }) except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): continue return high_resource_pids -def auto_detect_high_resource_processes(): - """定期自动检测新的高资源进程""" - while True: - try: - existing_pids = set(ProcessMonitor.objects.filter(is_active=True).values_list('pid', flat=True)) - high_resource_procs = get_high_resource_processes() - - for proc in high_resource_procs: - if proc['pid'] not in existing_pids: - logger, log_file = setup_logger(proc['pid']) - - # 记录到数据库 - monitor = ProcessMonitor.objects.create( - pid=proc['pid'], - process_name=proc['name'], - cpu_usage=0, - memory_usage=proc['memory_gb'], - network_usage=0, - log_path=log_file - ) - - # 启动监控线程 - threading.Thread( - target=monitor_process, - args=(proc['pid'], logger) - ).start() - - print(f"发现新的高资源进程: {proc['name']} (PID: {proc['pid']})") - - # 每5分钟检测一次 - time.sleep(300) - - except Exception as e: - print(f"自动检测出错: {str(e)}") - time.sleep(60) # 出错后等待1分钟再试 - -def start_monitor(request): - """开始监控进程""" - pid = request.GET.get('pid') - resource_type = request.GET.get('type', 'all') # cpu, gpu, memory, all - +def auto_detect_high_resource_processes(request): + """自动检测高资源进程的API端点""" try: - if pid: - pid = int(pid) - process = psutil.Process(pid) - - # 检查进程是否已经在监控中 - monitors = { - 'cpu': HighCPUProcess.objects.filter(pid=pid, is_active=True).exists(), - 'gpu': HighGPUProcess.objects.filter(pid=pid, is_active=True).exists(), - 'memory': HighMemoryProcess.objects.filter(pid=pid, is_active=True).exists() - } - - # 根据资源类型启动监控 - results = [] - if resource_type == 'all': - for rtype, is_monitored in monitors.items(): - if not is_monitored: - thread = threading.Thread( - target=monitor_process, - args=(pid, rtype), - daemon=True - ) - thread.start() - results.append(f"已启动{rtype}监控") - else: - results.append(f"{rtype}监控已在运行") - else: - if not monitors.get(resource_type): - thread = threading.Thread( - target=monitor_process, - args=(pid, resource_type), - daemon=True - ) - thread.start() - results.append(f"已启动{resource_type}监控") - else: - return JsonResponse({"error": f"进程 {pid} 已在{resource_type}监控中"}, status=400) - - return JsonResponse({ - "message": f"开始监控进程 {process.name()} (PID: {pid})", - "results": results - }) - - # 自动检测高资源进程 - high_resource_procs = { - 'cpu': [], - 'gpu': [], - 'memory': [] - } - - for proc in psutil.process_iter(['pid', 'name']): - try: - process = psutil.Process(proc.info['pid']) - - # 检查CPU使用率 (>200% 表示使用超过2个核心) - cpu_percent = process.cpu_percent(interval=1.0) - if cpu_percent > 200: - high_resource_procs['cpu'].append(process) - - # 检查内存使用量 (>20GB) - memory_gb = process.memory_info().rss / (1024 * 1024 * 1024) - if memory_gb > 20: - high_resource_procs['memory'].append(process) - - # 检查GPU使用率 (>50%) - gpu_usage, gpu_memory = get_process_gpu_usage(process.pid) - if gpu_usage > 50: - high_resource_procs['gpu'].append(process) - - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue + # 获取高资源进程 + high_resource_procs = get_high_resource_processes() # 启动监控 - results = { - 'cpu': [], - 'gpu': [], - 'memory': [] - } - - for resource_type, processes in high_resource_procs.items(): - for proc in processes: - if not any([ - HighCPUProcess.objects.filter(pid=proc.pid, is_active=True).exists(), - HighGPUProcess.objects.filter(pid=proc.pid, is_active=True).exists(), - HighMemoryProcess.objects.filter(pid=proc.pid, is_active=True).exists() - ]): - thread = threading.Thread( - target=monitor_process, - args=(proc.pid, resource_type), - daemon=True - ) - thread.start() - results[resource_type].append({ - 'pid': proc.pid, - 'name': proc.name() - }) + for proc in high_resource_procs: + try: + process = psutil.Process(proc['pid']) + # 设置日志文件 + log_file = setup_process_logger(proc['pid'], 'auto') + # 记录进程数据 + data = get_process_data(process) + log_process_metrics(proc['pid'], data, log_file) + except psutil.NoSuchProcess: + continue + except Exception as e: + logger.error(f"监控进程 {proc['pid']} 时出错: {str(e)}") return JsonResponse({ - "message": "开始监控高资源进程", - "processes": results - }) + 'status': 'success', + 'message': '已开始自动检测高资源进程', + 'processes': high_resource_procs + }, json_dumps_params={'ensure_ascii': False}) except Exception as e: - return JsonResponse({"error": str(e)}, status=500) + logger.error(f"自动检测失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': f'自动检测失败: {str(e)}' + }, json_dumps_params={'ensure_ascii': False}) + +def setup_logger(pid): + """为每个进程设置独立的日志记录器""" + log_file = os.path.join(os.path.dirname(os.path.dirname(__file__)), 'logs', 'process_monitor', f'process_{pid}_{datetime.now().strftime("%Y%m%d")}.log') + process_logger = logging.getLogger(f'monitor.process.process_{pid}') + process_logger.setLevel(logging.INFO) + + handler = logging.FileHandler(log_file) + formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s') + handler.setFormatter(formatter) + process_logger.addHandler(handler) + + return process_logger, log_file + +def continuous_monitor(): + """持续监控高资源进程的线程函数""" + global is_monitoring + logger = logging.getLogger('monitor') + interval = 60 # 设置为60秒间隔 + + while is_monitoring: + try: + next_update = timezone.now() + timezone.timedelta(seconds=interval) + + # 获取所有活跃的监控记录 + monitors = { + 'cpu': HighCPUProcess.objects.filter(is_active=True), + 'memory': HighMemoryProcess.objects.filter(is_active=True), + 'gpu': HighGPUProcess.objects.filter(is_active=True) + } + + for monitor_type, processes in monitors.items(): + for proc in processes: + try: + process = psutil.Process(proc.pid) + data = get_process_data(process) + log_file = setup_process_logger(proc.pid, monitor_type) + log_process_metrics(proc.pid, data, log_file) + + except psutil.NoSuchProcess: + proc.is_active = False + proc.save() + except Exception as e: + logger.error(f"监控进程 {proc.pid} 时出错: {str(e)}") + + # 计算需要等待的时间 + now = timezone.now() + if next_update > now: + time.sleep((next_update - now).total_seconds()) + + except Exception as e: + logger.error(f"持续监控出错: {str(e)}") + time.sleep(5) + +def get_process_data(process): + """获取进程的详细数据""" + with process.oneshot(): + cpu_times = process.cpu_times() + cpu_freq = psutil.cpu_freq() or psutil._common.scpufreq(current=0, min=0, max=0) + cpu_ctx = process.num_ctx_switches() + mem = process.memory_info() + + try: + mem_maps = len(process.memory_maps()) + except (psutil.AccessDenied, psutil.TimeoutExpired): + mem_maps = 0 + + sys_mem = psutil.virtual_memory() + swap = psutil.swap_memory() + + try: + io = process.io_counters() + except (psutil.AccessDenied, psutil.TimeoutExpired): + io = psutil._common.pio(read_count=0, write_count=0, read_bytes=0, write_bytes=0, + read_chars=0, write_chars=0, other_count=0, other_bytes=0) + + gpu_usage, gpu_memory = get_process_gpu_usage(process.pid) + + return { + 'timestamp': timezone.now().strftime('%Y-%m-%d %H:%M:%S'), + 'cpu': { + 'usage': f"{process.cpu_percent()}%", + 'user_time': f"{cpu_times.user:.1f}s", + 'system_time': f"{cpu_times.system:.1f}s", + 'cores': str(psutil.cpu_count()), + 'frequency': f"{getattr(cpu_freq, 'current', 0):.1f}MHz", + 'context_switches': f"{cpu_ctx.voluntary}/{cpu_ctx.involuntary}" + }, + 'memory': { + 'physical': f"{mem.rss/1024/1024:.1f}MB ({process.memory_percent():.1f}%)", + 'virtual': f"{mem.vms/1024/1024:.1f}MB", + 'mappings': f"{mem_maps}个", + 'system_usage': f"{sys_mem.percent:.1f}%", + 'swap_usage': f"{swap.percent:.1f}%" + }, + 'gpu': { + 'usage': f"{gpu_usage:.1f}%", + 'memory': f"{gpu_memory:.1f}MB" + }, + 'io': { + 'read': f"{getattr(io, 'read_bytes', 0)/1024/1024:.1f}MB ({getattr(io, 'read_count', 0)}次)", + 'write': f"{getattr(io, 'write_bytes', 0)/1024/1024:.1f}MB ({getattr(io, 'write_count', 0)}次)", + 'other': f"{getattr(io, 'other_count', 0)}次" + }, + 'status': process.status() + } + +@csrf_exempt +@require_http_methods(["GET", "POST"]) +def start_monitor(request): + """开始监控进程""" + global monitor_thread, is_monitoring + try: + pid = request.GET.get('pid') + monitor_type = request.GET.get('type') + + if not pid: + return JsonResponse({ + 'status': 'error', + 'message': '请输入进程ID' + }, json_dumps_params={'ensure_ascii': False}) + + pid = int(pid) + + try: + process = psutil.Process(pid) + process_name = process.name() + + # 启动监控线程 + if not is_monitoring: + is_monitoring = True + monitor_thread = threading.Thread(target=continuous_monitor) + monitor_thread.daemon = True + monitor_thread.start() + + return JsonResponse({ + 'status': 'success', + 'message': f'开始监控进程 {pid} ({process_name})' + }, json_dumps_params={'ensure_ascii': False}) + + except psutil.NoSuchProcess: + return JsonResponse({ + 'status': 'error', + 'message': f'进程 {pid} 不存在' + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"启动监控失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': f'启动监控失败: {str(e)}' + }, json_dumps_params={'ensure_ascii': False}) + +@csrf_exempt +@require_http_methods(["GET", "POST"]) +def stop_monitor(request): + """停止监控进程""" + global is_monitoring + try: + pid = request.GET.get('pid') + + if not pid: + return JsonResponse({ + 'status': 'error', + 'message': '请输入进程ID' + }, json_dumps_params={'ensure_ascii': False}) + + pid = int(pid) + + # 停止监控线程 + is_monitoring = False + + return JsonResponse({ + 'status': 'success', + 'message': f'停止监控进程 {pid}' + }, json_dumps_params={'ensure_ascii': False}) + + except Exception as e: + logger.error(f"停止监控失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': f'停止监控失败: {str(e)}' + }, json_dumps_params={'ensure_ascii': False}) + +@require_http_methods(["GET"]) +def get_process_status(request, pid): + """获取进程监控状态的API""" + try: + monitor_type = request.GET.get('type') + logger.info(f"获取进程状态: pid={pid}, type={monitor_type}") + + # 先设置日志文件路径 + log_file = setup_process_logger(pid, monitor_type) + if not log_file: + raise ValueError(f"无法创建日志文件,监控类型: {monitor_type}") + + process = psutil.Process(pid) + data = get_process_data(process) + + # 同步数据到数据库 + try: + # 更新 all 资源记录 + AllResourceProcess.objects.update_or_create( + pid=pid, + defaults={ + 'process_name': process.name(), + 'cpu_usage': float(data['cpu']['usage'].replace('%', '')), + 'cpu_user_time': float(data['cpu']['user_time'].replace('s', '')), + 'cpu_system_time': float(data['cpu']['system_time'].replace('s', '')), + 'memory_usage': float(data['memory']['physical'].split('MB')[0].strip()), + 'memory_percent': float(data['memory']['physical'].split('(')[1].split('%')[0].strip()), + 'virtual_memory': float(data['memory']['virtual'].split('MB')[0].strip()), + 'gpu_usage': float(data['gpu']['usage'].replace('%', '')), + 'gpu_memory': float(data['gpu']['memory'].replace('MB', '')), + 'net_io_sent': float(data['io']['write'].split('MB')[0].strip()), + 'net_io_recv': float(data['io']['read'].split('MB')[0].strip()), + 'is_active': True, + 'status': 1, + 'log_file': log_file + } + ) + + # 根据监控类型选择对应的模型 + if monitor_type == 'cpu': + # 从字符串中提取CPU使用率数值 + cpu_usage = float(data['cpu']['usage'].replace('%', '')) + logger.info(f"CPU使用率: {cpu_usage}%") # 调试日志 + + process_obj, created = HighCPUProcess.objects.update_or_create( + pid=pid, + defaults={ + 'process_name': process.name(), + 'cpu_usage': cpu_usage, + 'is_active': True, + 'status': 1, + 'log_file': log_file + } + ) + logger.info(f"{'创建' if created else '更新'}CPU进程记录: {process_obj}") + + elif monitor_type == 'memory': + # 从字符串中提取内存使用量和百分比 + memory_info = data['memory']['physical'] + memory_usage = float(memory_info.split('MB')[0].strip()) + memory_percent = float(memory_info.split('(')[1].split('%')[0].strip()) + + HighMemoryProcess.objects.update_or_create( + pid=pid, + defaults={ + 'process_name': process.name(), + 'memory_usage': memory_usage, + 'memory_percent': memory_percent, + 'is_active': True, + 'status': 1, + 'log_file': log_file + } + ) + + elif monitor_type == 'gpu': + # 从字符串中提取GPU使用率和显存 + gpu_usage = float(data['gpu']['usage'].replace('%', '')) + gpu_memory = float(data['gpu']['memory'].replace('MB', '')) + + HighGPUProcess.objects.update_or_create( + pid=pid, + defaults={ + 'process_name': process.name(), + 'gpu_usage': gpu_usage, + 'gpu_memory': gpu_memory, + 'is_active': True, + 'status': 1, + 'log_file': log_file + } + ) + + return JsonResponse({ + 'status': 'success', + 'data': data + }) + + except psutil.NoSuchProcess: + # 进程已终止 + monitor.is_active = False + monitor.status = 0 + monitor.save() + + return JsonResponse({ + 'status': 'error', + 'message': f'进程 {pid} 已终止' + }) + + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + +def update_process_status(pid, monitor_type, is_active=False, status=0): + """更新进程状态""" + try: + if monitor_type == 'cpu': + HighCPUProcess.objects.filter(pid=pid).update( + is_active=is_active, + status=status + ) + elif monitor_type == 'memory': + HighMemoryProcess.objects.filter(pid=pid).update( + is_active=is_active, + status=status + ) + elif monitor_type == 'gpu': + HighGPUProcess.objects.filter(pid=pid).update( + is_active=is_active, + status=status + ) + except Exception as e: + logger.error(f"更新进程状态失败: {str(e)}") + +def index(request): + """渲染主页""" + return render(request, 'index.html') @csrf_exempt @require_http_methods(["POST"]) -def stop_monitor(request, pid): - """停止监控指定进程""" - resource_type = request.GET.get('type', 'all') # 从查询参数获取资源类型 - +def stop_auto_detect(request): + """停止自动检测的API端点""" try: - # 根据资源类型选择要停止的监控 - monitors = [] - if resource_type == 'all': - monitors.extend(HighCPUProcess.objects.filter(pid=pid, is_active=True)) - monitors.extend(HighGPUProcess.objects.filter(pid=pid, is_active=True)) - monitors.extend(HighMemoryProcess.objects.filter(pid=pid, is_active=True)) - elif resource_type == 'cpu': - monitors.extend(HighCPUProcess.objects.filter(pid=pid, is_active=True)) - elif resource_type == 'gpu': - monitors.extend(HighGPUProcess.objects.filter(pid=pid, is_active=True)) - elif resource_type == 'memory': - monitors.extend(HighMemoryProcess.objects.filter(pid=pid, is_active=True)) - else: + return JsonResponse({ + 'status': 'success', + 'message': '已停止自动检测' + }, json_dumps_params={'ensure_ascii': False}) + except Exception as e: + logger.error(f"停止自动检测失败: {str(e)}") + return JsonResponse({ + 'status': 'error', + 'message': f'停止自动检测失败: {str(e)}' + }, json_dumps_params={'ensure_ascii': False}) + +@csrf_exempt +@require_http_methods(["GET"]) +def get_latest_log(request, pid): + """获取最新的监控日志""" + try: + monitor_type = request.GET.get('type') # 获取监控类型(cpu/gpu/memory) + if not monitor_type: return JsonResponse({ - "error": f"不支持的资源类型: {resource_type}" - }, status=400) - - if not monitors: + 'status': 'error', + 'message': '请指定监控类型' + }) + + # 根据类型获取对应的监控记录 + monitor = None + if monitor_type == 'cpu': + monitor = HighCPUProcess.objects.filter(pid=pid, is_active=True).first() + elif monitor_type == 'gpu': + monitor = HighGPUProcess.objects.filter(pid=pid, is_active=True).first() + elif monitor_type == 'memory': + monitor = HighMemoryProcess.objects.filter(pid=pid, is_active=True).first() + + if not monitor: return JsonResponse({ - "error": f"未找到进程 {pid} 的{resource_type}监控记录" - }, status=404) - - # 更新所有监控记录的状态 - for monitor in monitors: - # 只更新监控状态,不改变进程状态 + 'status': 'error', + 'message': f'未找到进程 {pid} 的{monitor_type}监控记录' + }) + + # 获取最新数据 + try: + process = psutil.Process(pid) + with process.oneshot(): + data = { + 'pid': pid, + 'name': process.name(), + 'status': 1, # 1表示进程运行中 + } + + # 根据监控类型添加相应的数据 + if monitor_type == 'cpu': + data['cpu_percent'] = process.cpu_percent() + elif monitor_type == 'memory': + memory_info = process.memory_info() + data['memory_gb'] = memory_info.rss / (1024 * 1024 * 1024) + data['memory_percent'] = process.memory_percent() + elif monitor_type == 'gpu': + gpu_usage, gpu_memory = get_process_gpu_usage(pid) + data['gpu_usage'] = gpu_usage + data['gpu_memory'] = gpu_memory + + return JsonResponse({ + 'status': 'success', + 'data': data + }) + + except psutil.NoSuchProcess: + # 进程已终止 monitor.is_active = False + monitor.status = 0 monitor.save() - # 记录停止操作 - logger = logging.getLogger(f'{monitor.__class__.__name__.lower()}_{pid}') - logger.info( - f"手动停止监控:\n" - f"├─ 进程ID: {pid}\n" - f"├─ 监控类型: {monitor.__class__.__name__}\n" - f"├─ 进程状态: {'运行中' if monitor.status == 1 else '已终止'}\n" - f"├─ 开始时间: {monitor.created_at}\n" - f"└─ 停止时间: {timezone.now()}" - ) + return JsonResponse({ + 'status': 'error', + 'message': f'进程 {pid} 已终止' + }) + + except Exception as e: + return JsonResponse({ + 'status': 'error', + 'message': str(e) + }) + +@csrf_exempt +@require_http_methods(["GET"]) +def get_process_list(request): + """获取当前监控的进程列表""" + try: + processes = [] - # 尝试终止相关的监控线程 - import threading - current_threads = threading.enumerate() - monitor_threads = [t for t in current_threads if t.name.startswith(f'monitor_{pid}')] - for thread in monitor_threads: - try: - thread.do_run = False - except: - pass + # 获取所有活跃的监控进程 + cpu_processes = HighCPUProcess.objects.filter(is_active=True) + gpu_processes = HighGPUProcess.objects.filter(is_active=True) + memory_processes = HighMemoryProcess.objects.filter(is_active=True) + + # 创建进程信息字典 + process_dict = {} + + # 处理 CPU 进程 + for proc in cpu_processes: + if proc.pid not in process_dict: + process_dict[proc.pid] = { + 'pid': proc.pid, + 'name': proc.process_name, + 'cpu_percent': proc.cpu_usage, + 'memory_gb': 0, + 'memory_percent': 0, + 'gpu_usage': 0, + 'gpu_memory': 0, + 'resource_types': ['cpu'] + } + else: + process_dict[proc.pid]['cpu_percent'] = proc.cpu_usage + process_dict[proc.pid]['resource_types'].append('cpu') + + # 处理 GPU 进程 + for proc in gpu_processes: + if proc.pid not in process_dict: + process_dict[proc.pid] = { + 'pid': proc.pid, + 'name': proc.process_name, + 'cpu_percent': 0, + 'memory_gb': 0, + 'memory_percent': 0, + 'gpu_usage': proc.gpu_usage, + 'gpu_memory': proc.gpu_memory, + 'resource_types': ['gpu'] + } + else: + process_dict[proc.pid]['gpu_usage'] = proc.gpu_usage + process_dict[proc.pid]['gpu_memory'] = proc.gpu_memory + process_dict[proc.pid]['resource_types'].append('gpu') + + # 处理内存进程 + for proc in memory_processes: + if proc.pid not in process_dict: + process_dict[proc.pid] = { + 'pid': proc.pid, + 'name': proc.process_name, + 'cpu_percent': 0, + 'memory_gb': proc.memory_usage, + 'memory_percent': proc.memory_percent, + 'gpu_usage': 0, + 'gpu_memory': 0, + 'resource_types': ['memory'] + } + else: + process_dict[proc.pid]['memory_gb'] = proc.memory_usage + process_dict[proc.pid]['memory_percent'] = proc.memory_percent + process_dict[proc.pid]['resource_types'].append('memory') + + # 转换字典为列表 + processes = list(process_dict.values()) + + # 按总资源占用率排序 + processes.sort(key=lambda x: ( + x['cpu_percent'] / 100 + + x['memory_percent'] / 100 + + x['gpu_usage'] / 100 + ), reverse=True) return JsonResponse({ - "message": f"已停止对进程 {pid} 的监控", - "stopped_monitors": len(monitors), - "process_status": "运行中" if monitors[0].status == 1 else "已终止" - }) + 'status': 'success', + 'processes': processes + }, json_dumps_params={'ensure_ascii': False}) except Exception as e: return JsonResponse({ - "error": f"停止监控失败: {str(e)}" - }, status=500) + 'status': 'error', + 'message': str(e) + }, json_dumps_params={'ensure_ascii': False}) -def get_process_metrics(request, pid): - """获取进程监控数据""" - resource_type = request.GET.get('type', 'all') +def high_resource_list(request): + """高资源进程列表视图""" + context = { + 'cpu_processes': HighCPUProcess.objects.all().order_by('-updated_at'), + 'memory_processes': HighMemoryProcess.objects.all().order_by('-updated_at'), + 'gpu_processes': HighGPUProcess.objects.all().order_by('-updated_at'), + } + return render(request, 'high_resource_list.html', context) + +def find_python_processes_in_dir(directory): + """查找指定目录下运行的 Python 进程""" + python_processes = [] + directory = str(Path(directory).resolve()) # 转换为绝对路径,并处理跨平台差异 + + for proc in psutil.process_iter(['pid', 'name', 'cmdline', 'cwd']): + try: + # 检查是否是 Python 进程 + if proc.info['name'].lower().startswith('python'): + # 检查进程的工作目录 + proc_cwd = str(Path(proc.info['cwd']).resolve()) + if proc_cwd.startswith(directory): + # 检查命令行参数 + cmdline = proc.info['cmdline'] + if cmdline: + python_processes.append({ + 'pid': proc.info['pid'], + 'name': proc.info['name'], + 'cmdline': ' '.join(cmdline), + 'cwd': proc_cwd + }) + except (psutil.NoSuchProcess, psutil.AccessDenied, psutil.ZombieProcess): + pass + + return python_processes + +def get_gpu_info(pid): + """获取进程的GPU使用信息""" try: - results = {} - monitors = { - 'cpu': HighCPUProcess, - 'gpu': HighGPUProcess, - 'memory': HighMemoryProcess + import pynvml + pynvml.nvmlInit() + gpu_info = { + 'usage': 0, + 'memory': 0, + 'device_count': pynvml.nvmlDeviceGetCount() } - if resource_type == 'all': - for rtype, model in monitors.items(): - try: - monitor = model.objects.get(pid=pid) - results[rtype] = { - 'status': monitor.status, - 'cpu_usage': monitor.cpu_usage, - 'memory_usage': monitor.memory_usage, - 'gpu_usage': monitor.gpu_usage, - 'gpu_memory': monitor.gpu_memory, - 'virtual_memory': monitor.virtual_memory - } - - # 添加特定资源类型的指标 - if rtype == 'cpu': - results[rtype]['cpu_cores'] = monitor.cpu_cores - elif rtype == 'gpu': - results[rtype]['gpu_index'] = monitor.gpu_index - elif rtype == 'memory': - results[rtype]['swap_usage'] = monitor.swap_usage - - except model.DoesNotExist: - continue - else: - model = monitors.get(resource_type) - if model: - try: - monitor = model.objects.get(pid=pid) - results[resource_type] = { - 'status': monitor.status, - 'cpu_usage': monitor.cpu_usage, - 'memory_usage': monitor.memory_usage, - 'gpu_usage': monitor.gpu_usage, - 'gpu_memory': monitor.gpu_memory, - 'virtual_memory': monitor.virtual_memory - } - - # 添加特定资源类型的指标 - if resource_type == 'cpu': - results[resource_type]['cpu_cores'] = monitor.cpu_cores - elif resource_type == 'gpu': - results[resource_type]['gpu_index'] = monitor.gpu_index - elif resource_type == 'memory': - results[resource_type]['swap_usage'] = monitor.swap_usage - - except model.DoesNotExist: - pass - - if not results: - return JsonResponse({"error": f"未找到PID为{pid}的监控记录"}, status=404) - - return JsonResponse({ - "pid": pid, - "metrics": results - }) - + for i in range(gpu_info['device_count']): + handle = pynvml.nvmlDeviceGetHandleByIndex(i) + procs = pynvml.nvmlDeviceGetComputeRunningProcesses(handle) + for proc in procs: + if proc.pid == pid: + gpu_info['memory'] = proc.usedGpuMemory / 1024 / 1024 # 转换为MB + gpu_info['usage'] = pynvml.nvmlDeviceGetUtilizationRates(handle).gpu + break + return gpu_info except Exception as e: - return JsonResponse({"error": str(e)}, status=500) + logger.error(f"获取GPU信息失败: {str(e)}") + return {'usage': 0, 'memory': 0, 'device_count': 0} -def auto_detect_monitor(request): - """自动检测并监控高资源进程""" +def get_io_counters(proc): + """获取进程的IO计数器信息""" try: - # 清理已停止的监控 - HighCPUProcess.objects.filter(is_active=True, status=0).update(is_active=False) - HighGPUProcess.objects.filter(is_active=True, status=0).update(is_active=False) - HighMemoryProcess.objects.filter(is_active=True, status=0).update(is_active=False) - - results = { - 'cpu': [], - 'gpu': [], - 'memory': [] + io = proc.io_counters() + return { + 'read_bytes': io.read_bytes / 1024 / 1024, # 转换为MB + 'write_bytes': io.write_bytes / 1024 / 1024, + 'read_count': io.read_count, + 'write_count': io.write_count } - - # 首先收集所有进程的CPU使用率 - processes = {} - for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): - try: - processes[proc.info['pid']] = proc.info - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - # 等待一秒获取CPU使用率变化 - time.sleep(1) - - # 检测高资源进程 - for proc in psutil.process_iter(['pid', 'name', 'cpu_percent']): - try: - pid = proc.info['pid'] - if pid not in processes: - continue - - process = psutil.Process(pid) - cpu_percent = proc.info['cpu_percent'] - - # 检查CPU使用率 (>200% 表示使用超过2个核心) - if cpu_percent > 200: - if not HighCPUProcess.objects.filter(pid=pid, is_active=True).exists(): - thread = threading.Thread( - target=monitor_process, - args=(pid, 'cpu'), - daemon=True - ) - thread.start() - results['cpu'].append({ - 'pid': pid, - 'name': process.name(), - 'cpu_usage': cpu_percent - }) - - # 检查内存使用量 (>20GB) - memory_gb = process.memory_info().rss / (1024 * 1024 * 1024) - if memory_gb > 20: - if not HighMemoryProcess.objects.filter(pid=pid, is_active=True).exists(): - thread = threading.Thread( - target=monitor_process, - args=(pid, 'memory'), - daemon=True - ) - thread.start() - results['memory'].append({ - 'pid': pid, - 'name': process.name(), - 'memory_usage': memory_gb - }) - - # 检查GPU使用率 (>50%) - gpu_usage, gpu_memory = get_process_gpu_usage(pid) - if gpu_usage > 50: - if not HighGPUProcess.objects.filter(pid=pid, is_active=True).exists(): - thread = threading.Thread( - target=monitor_process, - args=(pid, 'gpu'), - daemon=True - ) - thread.start() - results['gpu'].append({ - 'pid': pid, - 'name': process.name(), - 'gpu_usage': gpu_usage, - 'gpu_memory': gpu_memory - }) - - except (psutil.NoSuchProcess, psutil.AccessDenied): - continue - - return JsonResponse({ - "message": "已开始监控检测到的高资源进程", - "detected_processes": results - }) - - except Exception as e: - return JsonResponse({"error": str(e)}, status=500) + except: + return {'read_bytes': 0, 'write_bytes': 0, 'read_count': 0, 'write_count': 0} + +def get_network_connections(proc): + """获取进程的网络连接信息""" + try: + connections = [] + for conn in proc.connections(): + connections.append({ + 'local_address': f"{conn.laddr.ip}:{conn.laddr.port}" if conn.laddr else "", + 'remote_address': f"{conn.raddr.ip}:{conn.raddr.port}" if conn.raddr else "", + 'status': conn.status + }) + return connections + except: + return [] + +def get_open_files(proc): + """获取进程打开的文件列表""" + try: + return [f.path for f in proc.open_files()] + except: + return [] diff --git a/test_program/main.py b/test_program/main.py new file mode 100644 index 0000000..687ae29 --- /dev/null +++ b/test_program/main.py @@ -0,0 +1,18 @@ +import time +import os + +def light_task(): + """轻量级任务""" + count = 0 + while True: + # 简单的计数和休眠 + count += 1 + print(f"运行次数: {count}, PID: {os.getpid()}") + time.sleep(2) # 每2秒执行一次 + +if __name__ == "__main__": + print(f"程序启动,PID: {os.getpid()}") + try: + light_task() + except KeyboardInterrupt: + print("程序已停止") \ No newline at end of file