# # ctop.rb # written by KAMEZAWA Hiroyuki # Copyright 2009 Fujitsu Limited # # Changelog: # # v002 # - fixed leak of file descriptor # - mount/umount <-> reload data problem is fixed. # - "mount twice" problem is fixed. # - removed R key for reload all. it's now automatic # - handle "noprefix" mount option # - show mount option in help window # - add cpuset support # - add command-mode # v001 # - first version released # - cpu, cpuacct, memory subsys is supported # known bugs -> noprefix, umount, mount twice # require 'cgroup.rb' require 'curses' require 'etc' require 'timeout' require 'singleton' DIRWIN_LINES=7 DIRWIN_FIELDS= DIRWIN_LINES - 2 UPKEY=256 DOWNKEY=257 RIGHTKEY=258 LEFTKEY=259 #mode SHOWMOUNT=0 SHOWTASKS=1 SHOWSUBSYS=2 #for 'ps' PID=0 STATE=1 PPID=2 UID=3 COMMAND=4 PGID=5 #for process status filter RUNNING=0 class Cursor def initialize(name) @subsysname=name @cursor=0 @mode=SHOWTASKS @info_startline=0 @info_endline=0 @show_only_running = 0 @user_name_filter=nil @command_name_filter=nil end def pos @cursor end def mode @mode end def change_mode case @mode when SHOWTASKS then @mode=SHOWSUBSYS when SHOWSUBSYS then @mode=SHOWTASKS end end # # Filter for PS-MODE # def process_status_filter(stat) return true if (@show_only_running == 0) return true if (stat =="R") return false end def user_name_filter(str) return true if (@user_name_filter == nil) return true if (@user_name_filter == str) return false end def command_name_filter(str) return true if (@command_name_filter == nil) return true if (str =~ /#{@command_name_filter}/) return false end def toggle_show_only_running if (@show_only_running == 0) then @show_only_running = 1 else @show_only_running = 0 end end def set_user_name_filter(str) @user_name_filter=str end def set_command_name_filter(str) @command_name_filter=str end # # Scroll management for infowin # def info_startline @info_startline end def set_infoendline(num) @info_endline=num end def set_infoline(num) if ((num < 0) || (num >= @info_endline)) then @info_startline=0 else @info_startline=num end end # # chdir() for subsys. # def move(direction) subsys =$allsubsys[@subsysname] if (subsys == nil) then return end if (direction == -1) then @cursor -= 1 if @cursor > 0 elsif (direction == 1) @cursor += 1 if @cursor < subsys.size-1 end end end class Current include Singleton def initialize @index=-1 @name=nil @cursor=nil @subsys=nil @subsys_cursor = Hash.new end def set(x) @index=x if (x == -1) then @index, @name, @cursor, @subsys = -1, "help", nil, nil else @name = $subsys_array[x] @subsys = $allsubsys[@name] if (@subsys_cursor[@name] == nil) then @subsys_cursor[@name] = Cursor.new(@name) end @cursor = @subsys_cursor[@name] end end def move (dir) case dir when "left" @index -= 1 if (@index > -1) when "right" @index += 1 if (@index < $subsys_array.size - 1) end set(@index) end def name @name end def cursor @cursor end def subsys @subsys end end $cur = Current.instance # # A fucntion for change subsys view. $current_subsys is moved. # def move_current_subsys(dir) $cur.move(dir) end # # Move cursor in directory view # def move_current_dir(direction) cursor = $cur.cursor return if (cursor == nil) cursor.move(direction) end def detect_dirlist_position(subsysname, subsys) pos = 0 size=subsys.size cursor = $cur.cursor return [0, 0, 0] if cursor == nil pos = cursor.pos if ((size < 4) || (pos <= 2)) then head=0 tail=4 elsif (pos < size - 2) then head=pos-1 tail=pos+2 else head = size - 4 tail = size - 1 end return [pos, head, tail] end def get_owner_name(name) stat = File.stat(name) begin info = Etc::getpwuid(stat.uid) uname = info.name rescue $barwin.addstr($!) uname = stat.uid.to_s end begin info = Etc::getgrgid(stat.gid) gname = info.name rescue gname = stat.gid.to_s end sprintf("\t-\t(%s/%s)", uname, gname) end def draw_dirlist(dirwin, subsys, pos, head, tail) lines=1 i=head while i <= tail name = subsys.ent(i) if (name == nil) then break end dirwin.standout if (i == pos) dirwin.setpos(lines, 3) dirwin.addstr(name + get_owner_name(name)) dirwin.standend if (i == pos) lines+=1 i += 1 end end def draw_dirwin(dirwin) dirwin.clear dirwin.box(?|,?-,?*) dirwin.setpos(0, 1) -1.upto($subsys_array.size) do |x| dirwin.addstr("-") if (x == -1) then str="help" else str=sprintf("%s",$subsys_array[x]) end break if (str == nil) dirwin.standout if (str == $cur.name) dirwin.addstr(str) dirwin.standend if (str == $cur.name) end dirwin.setpos(6,dirwin.maxx-32) dirwin.addstr("[#{Time.now.asctime}]") # # Show directory list # subsys = $cur.subsys if subsys != nil then #Reload information subsys.reload size=subsys.size pos = detect_dirlist_position($cur.name, subsys) draw_dirlist(dirwin, subsys, pos[0], pos[1], pos[2]) end end # # # for infowin # # def draw_infowin_limited(infowin, cursor, data) # # Generate Header # str = yield nil # write a header if necessary if (str != nil) then draw=1 infowin.setpos(0,2) infowin.addstr(str) else draw=0 end # # print a line whici is in the window # startline = cursor.info_startline endline = cursor.info_startline + infowin.maxy-2 startline.upto(endline) do |linenumber| x = data.at(linenumber) return if (x == nil) #no more data str = yield(x) infowin.setpos(draw, 2) infowin.addstr(str) draw = 1+infowin.cury break if (draw == infowin.maxy) end cursor.set_infoendline(data.size) end def show_mount_info(infowin) if ($allsubsys.empty?) then $barwin.addstr("cgroups are not mounted\n") end $allsubsys.each do |name, subsys| formated = sprintf("%12s\t%s\t#%s\n", name, subsys.mount_point, subsys.option) infowin.addstr(formated) end #$barwin.addstr("mounted subsystems") # # Help # infowin.addstr("Command\n") infowin.addstr("[LEFT, RIGHT]\t move subsystems\n") infowin.addstr("[UP, DOWN]\t move directory\n") infowin.addstr("[n, b]\t\t scorll information window\n") infowin.addstr("[s]\t\t switch shown information (ps-mode/stat-mode)\n") infowin.addstr("[r]\t\t set refresh rate\n") infowin.addstr("[c]\t\t Enter command-mode\n") infowin.addstr("ps mode option\n") infowin.addstr("[t]\t\t (ps-mode)toggle show only running process\n") infowin.addstr("[u]\t\t (ps-mode)set/unset user name filter\n") infowin.addstr("[f]\t\t (ps-mode)set/unset command name filter") end # # Read /proc//status file and fill data[] array, return it # def parse_pid_status(f, es) input = f.readline input =~ es return $1 end def parse_process(pid) # # Status # data = Array.new stat = nil stat = catch(:bad_task_status) do data[PID]=pid.to_i begin f = File.open("/proc/#{pid}/status", "r") #Name data[COMMAND] = parse_pid_status(f,/^Name:\s+(.+)/) #State data[STATE] = parse_pid_status(f, /^State:\s+([A-Z]).+/) # TGID: Is thread grouo leader ? if (parse_pid_status(f, /^Tgid:\s+(.+)/) != pid) then throw :bad_task_status, false end #skip PID input = f.readline #PPID data[PPID]= parse_pid_status(f,/^PPid:\s+(.+)/) ppid=data[PPID] #TracerPID input = f.readline #UID uid = parse_pid_status(f,/^Uid:\s+([0-9]+).+/) begin info=Etc::getpwuid(uid.to_i) data[UID]=info.name rescue data[UID]=uid end rescue throw :bad_task_status, false ensure f.close unless f.nil? end end return data unless stat.nil? return nil end # # PS-MODE # Cat "tasks" file and visit all /proc//status file # All information will be pushed into "ps" array # def show_tasks(subsys, cursor, infowin) # Get Name of Current Cgroup and read task file ps = Array.new catch :quit do group = subsys.ent(cursor.pos) throw :quit,"nogroup" if group==nil tasks = subsys.tasks(group) throw :quit,"nogroup" if tasks==nil tasks.each do |x| data = parse_process(x) next if (data == nil) next unless (cursor.process_status_filter(data[STATE])) next unless (cursor.command_name_filter(data[COMMAND])) ps.push(data) if (cursor.user_name_filter(data[UID])) end # # Sort ps's result, "R" first. # ps.sort! do |x , y| if (x[STATE] == "R" && y[STATE] != "R") then -1 elsif (x[STATE] != "R" && y[STATE] == "R") then 1 else 0 end end end return if (ps.size == 0) draw_infowin_limited(infowin, cursor, ps)do |x| if (x == nil) then sprintf("%6s %6s %8s %5s %16s", "PID","PPID","USER","STATE", "COMMAND") else sprintf("%6d %6d %8s %5s %16s", x[PID], x[PPID], x[UID], x[STATE], x[COMMAND]) end end unless ($cur.cursor.process_status_filter("S")) then $barwin.addstr("[r]") end unless ($cur.cursor.user_name_filter("badnamemandab")) then $barwin.addstr("[u]") end unless ($cur.cursor.command_name_filter("badnamemandab")) then $barwin.addstr("[c]") end end def show_subsys_stat(subsys, cursor, infowin) group = subsys.ent(cursor.pos) return if group == nil data = subsys.stat(group) return if data == nil draw_infowin_limited(infowin, cursor, data) do |x| next if x == nil if (x[0].size > 24) then len = x[0].size - 24 x[0].slice!(0..len) end sprintf("%24s\t%s", x[0], x[1]) end end def draw_infowin(infowin) infowin.clear cursor = $cur.cursor if cursor == nil then mode = SHOWMOUNT else mode = cursor.mode end # # If no subsys is specified, just show mount information. # case mode when SHOWMOUNT show_mount_info(infowin) when SHOWTASKS $barwin.addstr("[ps-mode]") show_tasks($cur.subsys, cursor, infowin) when SHOWSUBSYS $barwin.addstr("[stat-mode]") show_subsys_stat($cur.subsys, cursor, infowin) end end def set_scroll(infowin, direction) cursor = $cur.cursor return if (cursor == nil) if (direction == 1) then curline=cursor.info_startline cursor.set_infoline(curline+infowin.maxy) else curline=cursor.info_startline cursor.set_infoline(curline-infowin.maxy) end end def change_mode if ($cur.cursor != nil) then $cur.cursor.change_mode end end def toggle_running_filter(ch) if ($cur.cursor != nil) then $cur.cursor.toggle_show_only_running end end def hit_any_key(str, window) window.addstr(str) if str != nil window.addstr("\n[Hit Any Key]") window.getch end def user_name_filter(infowin) infowin.clear infowin.refresh x=infowin.maxx-6 y=3 window = Curses::stdscr.subwin(y, x, 8, 3) window.box(?|,?-,?*) window.setpos(1,1) window.addstr("user name filter:") window.refresh str=window.getstr window.close return if (str == nil) cursor= $cur.cursor str = nil if (str.size==0) cursor.set_user_name_filter(str) if (cursor != nil) end def command_name_filter(infowin) infowin.clear infowin.refresh x=infowin.maxx-6 y=3 window = Curses::stdscr.subwin(y, x, 8, 3) window.box(?|,?-,?*) window.setpos(1,1) window.addstr("command name filter:") window.refresh str=window.getstr window.close return if (str == nil) cursor= $cur.cursor str = nil if (str.size==0) cursor.set_command_name_filter(str) if (cursor != nil) end def set_refresh_time(infowin) infowin.clear infowin.refresh x=infowin.maxx-6 y=3 window = Curses::stdscr.subwin(y, x, 8, 3) window.box(?|,?-,?*) window.setpos(1,1) window.addstr("set refresh time:") window.refresh str=window.getstr window.close return nil if (str == nil) return str.to_i end def smart_print(str, window) if (window.maxx - window.curx < str.size-2) then window.addstr("\n"+str) else window.addstr(str) end end def show_writable_files(subsys, cursor, infowin) group = subsys.ent(cursor.pos) return nil if group == nil data = subsys.writable_files(group) return nil if data == nil ent=1 data.sort! data.each do |x| str = sprintf("%2d: %s ", ent, File.basename(x)) ent=ent+1 smart_print(str, infowin) end infowin.refresh return data end def chown_all_files(uid, gid, group, infowin) Dir.foreach(group) do |x| infowin.addstr(x) name = group+"/"+x if (x == ".") then begin File.chown(uid, gid, name) rescue hit_any_key("Error:"+$!, infowin) break end end next if File.directory?(name) begin File.chown(nil, gid, name) rescue hit_any_key("Error:"+$!, infowin) break end end end def check_mkrmdir_string(str, infowin) if (str =~ /\//) then infowin.addstr("don't include /\n") return false end return true end def command_mode(infowin) return if ($cur.subsys == nil) infowin.clear data = show_writable_files($cur.subsys, $cur.cursor, infowin) return if data==nil $barwin.clear $barwin.addstr("[command-mode]") $barwin.refresh infowin.addstr("\n") smart_print("96: chown(UID) ", infowin) smart_print("97: chown(GID) ", infowin) smart_print("98: mkdir ", infowin) smart_print("99: rmdir",infowin) infowin.addstr("\n\nEdit which ?[and Hit return]:") endline = infowin.cury+1 str=infowin.getstr name=nil group = $cur.subsys.ent($cur.cursor.pos) case str.to_i when 1..95 catch :select_failure do val = str.to_i infowin.setpos(0, 0) name = data.at(val - 1) throw :select_failure if name==nil #get input infowin.setpos(endline, 0) str = sprintf("#echo to >%s:", File.basename(name)) infowin.addstr(str) infowin.refresh str = infowin.getstr #write begin f = File.open(name, "w") {|f| f.write(str) } rescue hit_any_key("Error:"+$!, infowin) end end #chown (UID) when 96 infowin.addstr("change owner id of all files to:") str = infowin.getstr if (str =~ /\D/) then begin info = Etc::getgrnam(str) uid = info.gid rescue hit_any_key("Error:"+$!, infowin) uid=nil end else uid = info.to_i end chown_all_files(uid, -1, group, infowin) #chown(GID) when 97 infowin.addstr("change group id of all files to:") str = infowin.getstr if (str =~ /\D/) then begin info = Etc::getgrnam(str) gid = info.gid rescue hit_any_key("Error:"+$!, infowin) gid=nil end else gid = info.to_i end chown_all_files(-1, gid, group, infowin) #mkdir when 98 infowin.addstr("mkdir -.enter name:") str = infowin.getstr if (check_mkrmdir_string(str, infowin)) then begin Dir.mkdir(group+"/"+str) hit_any_key("Error:"+$!, infowin) rescue hit_any_key("Error:"+$!, infowin) end else hit_any_key(nil, infowin) end #rmdir when 99 infowin.addstr("rmdir -.enter name:") str = infowin.getstr if (check_mkrmdir_string(str, infowin)) then begin Dir.rmdir(group+"/"+str) hit_any_key("Error:"+$!, infowin) rescue hit_any_key("Error:"+$!, infowin) end else hit_any_key(nil, infowin) end end $barwin.clear end # # Main loop is following # # # For stdscreen # # Check /proc/mounts and read all subsys. # refresh_all # # Main loop. create windows and wait for inputs # Curses::init_screen begin $lines=Curses::lines $cols=Curses::cols off=0 # # Create window # dirwin = Curses::stdscr.subwin(DIRWIN_LINES, $cols, off, 0) #for misc info off+=DIRWIN_LINES $barwin = Curses::stdscr.subwin(1, $cols, off, 0); $barwin.standout off+=1 infowin = Curses::stdscr.subwin($lines-off, $cols, off, 0) mode=SHOWTASKS quit=0 refresh_time=15 while quit == 0 #$barwin.clear #$barwin.addstr("Info:") draw_dirwin(dirwin) draw_infowin(infowin) dirwin.refresh infowin.refresh $barwin.refresh # # handle input. # $barwin.clear Curses::setpos(0,0) ch=0 begin Timeout::timeout(refresh_time) do ch=Curses::getch end rescue Timeout::Error #$barwin.addstr("timeout") end #check espace sequence if ch == 27 then ch = Curses::getch if ch == 91 then ch = Curses::getch case ch when 65 then ch = UPKEY when 66 then ch = DOWNKEY when 67 then ch = RIGHTKEY when 68 then ch = LEFTKEY end end end # # # if (check_and_refresh_mount_info) then $cur.set(-1) end #str=sprintf("%d",ch) #$barwin.addstr(Time.now.asctime) case ch when ?q quit=1 break when LEFTKEY then move_current_subsys("left") when RIGHTKEY then move_current_subsys("right") when UPKEY then move_current_dir(-1) when DOWNKEY then move_current_dir(1) when ?s then change_mode when ?n then set_scroll(infowin, 1) when ?b then set_scroll(infowin, -1) when ?t then toggle_running_filter(nil) when ?u then user_name_filter(infowin) when ?f then command_name_filter(infowin) when ?c then command_mode(infowin) when ?r newval = refresh_time=set_refresh_time(infowin) refresh_time = newval if newval != nil end end ensure Curses::close_screen end