26
26
27
27
import static com .cloudbees .jenkins .support .impl .JenkinsLogs .ROTATED_LOGFILE_FILTER ;
28
28
29
+ import com .cloudbees .jenkins .support .SupportPlugin ;
29
30
import com .cloudbees .jenkins .support .api .Container ;
30
31
import com .cloudbees .jenkins .support .api .LaunchLogsFileContent ;
31
32
import com .cloudbees .jenkins .support .api .ObjectComponent ;
32
33
import com .cloudbees .jenkins .support .api .ObjectComponentDescriptor ;
33
34
import com .cloudbees .jenkins .support .timer .FileListCapComponent ;
34
35
import edu .umd .cs .findbugs .annotations .NonNull ;
35
36
import hudson .Extension ;
37
+ import hudson .ExtensionList ;
38
+ import hudson .FilePath ;
39
+ import hudson .console .ConsoleLogFilter ;
40
+ import hudson .console .LineTransformationOutputStream ;
41
+ import hudson .init .Terminator ;
36
42
import hudson .model .AbstractModelObject ;
37
43
import hudson .model .Computer ;
38
- import hudson .model .Node ;
44
+ import hudson .model .Run ;
45
+ import hudson .model .TaskListener ;
46
+ import hudson .remoting .Channel ;
39
47
import hudson .security .Permission ;
40
- import hudson .slaves .Cloud ;
48
+ import hudson .slaves .ComputerListener ;
49
+ import hudson .slaves .JNLPLauncher ;
50
+ import hudson .slaves .SlaveComputer ;
51
+ import hudson .util .io .RewindableRotatingFileOutputStream ;
41
52
import java .io .File ;
42
- import java .util .ArrayList ;
53
+ import java .io .IOException ;
54
+ import java .io .OutputStream ;
55
+ import java .nio .charset .StandardCharsets ;
56
+ import java .time .Instant ;
57
+ import java .time .format .DateTimeFormatter ;
58
+ import java .time .temporal .ChronoUnit ;
43
59
import java .util .Collections ;
44
- import java .util .List ;
60
+ import java .util .Map ;
45
61
import java .util .Set ;
62
+ import java .util .concurrent .ConcurrentHashMap ;
46
63
import java .util .concurrent .TimeUnit ;
64
+ import java .util .logging .Level ;
65
+ import java .util .logging .Logger ;
47
66
import jenkins .model .Jenkins ;
67
+ import org .apache .commons .io .output .TeeOutputStream ;
48
68
import org .jenkinsci .Symbol ;
49
69
import org .kohsuke .stapler .DataBoundConstructor ;
50
70
54
74
@ Extension
55
75
public class SlaveLaunchLogs extends ObjectComponent <Computer > {
56
76
77
+ private static final int MAX_ROTATE_LOGS =
78
+ Integer .getInteger (SlaveLaunchLogs .class .getName () + ".MAX_ROTATE_LOGS" , 9 );
79
+
80
+ private static final Logger LOGGER = Logger .getLogger (SlaveLaunchLogs .class .getName ());
81
+
57
82
@ DataBoundConstructor
58
83
public SlaveLaunchLogs () {
59
84
super ();
@@ -73,128 +98,149 @@ public String getDisplayName() {
73
98
74
99
@ Override
75
100
public void addContents (@ NonNull Container container ) {
76
- addAgentsLaunchLogs (container );
101
+ ExtensionList . lookupSingleton ( LogArchiver . class ). addContents (container );
77
102
}
78
103
79
104
@ NonNull
80
105
@ Override
81
106
public ComponentCategory getCategory () {
82
- return ComponentCategory .AGENT ; // TODO or LOGS?
107
+ return ComponentCategory .LOGS ;
83
108
}
84
109
85
110
@ Override
86
111
public void addContents (@ NonNull Container container , Computer item ) {
87
- if (item .getNode () == null ) {
88
- return ;
89
- }
90
- File lastLog = item .getLogFile ();
91
- if (lastLog .exists ()) {
92
- Agent agent = new Agent (new File (Jenkins .get ().getRootDir (), "logs/slaves/" + item .getName ()), lastLog );
93
- if (!agent .isTooOld ()) {
94
- addAgentLaunchLogs (container , agent );
112
+ if (item .getNode () != null
113
+ && item .getLogFile ().lastModified () >= System .currentTimeMillis () - TimeUnit .DAYS .toMillis (7 )) {
114
+ File dir = new File (Jenkins .get ().getRootDir (), "logs/slaves/" + item .getName ());
115
+ File [] files = dir .listFiles (ROTATED_LOGFILE_FILTER );
116
+ if (files != null ) {
117
+ for (File f : files ) {
118
+ container .add (new LaunchLogsFileContent (
119
+ "nodes/slave/{0}/launchLogs/{1}" ,
120
+ new String [] {dir .getName (), f .getName ()}, f , FileListCapComponent .MAX_FILE_SIZE ));
121
+ }
95
122
}
96
123
}
97
124
}
98
125
99
- /**
100
- * <p>
101
- * In the presence of {@link Cloud} plugins like EC2, we want to find past agents, not just current ones.
102
- * So we don't try to loop through {@link Node} here but just try to look at the file systems to find them
103
- * all.
104
- *
105
- * <p>
106
- * Generally these cloud plugins do not clean up old logs, so if run for a long time, the log directory
107
- * will be full of old files that are not very interesting. Use some heuristics to cut off logs
108
- * that are old.
109
- */
110
- private void addAgentsLaunchLogs (Container result ) {
111
-
112
- List <Agent > all = new ArrayList <>();
113
-
114
- { // find all the agent launch log files and sort them newer ones first
115
- File agentLogsDir = new File (Jenkins .get ().getRootDir (), "logs/slaves" );
116
- File [] logs = agentLogsDir .listFiles ();
117
- if (logs != null ) {
118
- for (File dir : logs ) {
119
- File lastLog = new File (dir , "slave.log" );
120
- if (lastLog .exists ()) {
121
- Agent s = new Agent (dir , lastLog );
122
- if (s .isTooOld ()) continue ; // we don't care
123
- all .add (s );
124
- }
125
- }
126
- }
126
+ @ Extension
127
+ public static final class LogArchiver extends ConsoleLogFilter {
127
128
128
- Collections .sort (all );
129
- }
130
- { // this might be still too many, so try to cap them.
131
- int acceptableSize = Math .max (256 , Jenkins .get ().getNodes ().size () * 5 );
129
+ private final File logDir ;
130
+ private final RewindableRotatingFileOutputStream stream ;
132
131
133
- if (all .size () > acceptableSize ) all = all .subList (0 , acceptableSize );
132
+ public LogArchiver () throws IOException {
133
+ logDir = new File (SupportPlugin .getLogsDirectory (), "agent-launches" );
134
+ stream = new RewindableRotatingFileOutputStream (new File (logDir , "all.log" ), MAX_ROTATE_LOGS );
135
+ stream .rewind ();
134
136
}
135
137
136
- // now add them all
137
- all .forEach (it -> addAgentLaunchLogs (result , it ));
138
- }
139
-
140
- private void addAgentLaunchLogs (Container container , Agent agent ) {
141
- File [] files = agent .dir .listFiles (ROTATED_LOGFILE_FILTER );
142
- if (files != null ) {
143
- for (File f : files ) {
144
- container .add (new LaunchLogsFileContent (
145
- "nodes/slave/{0}/launchLogs/{1}" ,
146
- new String [] {agent .getName (), f .getName ()}, f , FileListCapComponent .MAX_FILE_SIZE ));
138
+ @ Override
139
+ public OutputStream decorateLogger (Computer computer , OutputStream logger ) {
140
+ if (computer instanceof SlaveComputer ) {
141
+ return new TeeOutputStream (logger , new PrefixedStream (stream , computer .getName ()));
142
+ } else {
143
+ return logger ;
147
144
}
148
145
}
149
- }
150
146
151
- static class Agent implements Comparable <Agent > {
152
- /**
153
- * Launch log directory of the agent: logs/slaves/NAME
154
- */
155
- final File dir ;
156
-
157
- final long time ;
147
+ @ SuppressWarnings ("rawtypes" )
148
+ @ Override
149
+ public OutputStream decorateLogger (Run build , OutputStream logger ) throws IOException , InterruptedException {
150
+ return logger ;
151
+ }
158
152
159
- Agent (File dir , File lastLog ) {
160
- this .dir = dir ;
161
- this .time = lastLog .lastModified ();
153
+ public void addContents (@ NonNull Container container ) {
154
+ File [] files = logDir .listFiles (ROTATED_LOGFILE_FILTER );
155
+ if (files != null ) {
156
+ for (File f : files ) {
157
+ container .add (new LaunchLogsFileContent (
158
+ "nodes/slave/launches/" + f .getName (),
159
+ new String [0 ],
160
+ f ,
161
+ FileListCapComponent .MAX_FILE_SIZE ));
162
+ }
163
+ }
162
164
}
163
165
164
- /** Agent name */
165
- String getName () {
166
- return dir .getName ();
166
+ @ Terminator
167
+ public static void close () {
168
+ try {
169
+ ExtensionList .lookupSingleton (LogArchiver .class ).stream .close ();
170
+ } catch (IOException x ) {
171
+ LOGGER .log (Level .WARNING , null , x );
172
+ }
167
173
}
174
+ }
175
+
176
+ // TODO delete if updating to 2.440.3 and JENKINS-72799 is backported, else 2.448+
177
+ @ Extension
178
+ public static final class Jenkins72799Hack extends ComputerListener {
168
179
169
180
/**
170
- * Use the primary log file's timestamp to compare newer agents from older agents.
171
- *
172
- * sort in descending order; newer ones first .
181
+ * Names of inbound agents which have recently gotten to {@link #preLaunch}
182
+ * but for which we did not receive typical output in {@link SlaveComputer#setChannel(Channel, OutputStream, Channel.Listener)}
183
+ * prior to {@link #preOnline} .
173
184
*/
174
- public int compareTo (Agent that ) {
175
- return Long .compare (that .time , this .time );
185
+ private final Map <String , Boolean > launching = new ConcurrentHashMap <>();
186
+
187
+ @ Override
188
+ public void preLaunch (Computer c , TaskListener taskListener ) {
189
+ if (c instanceof SlaveComputer && ((SlaveComputer ) c ).getLauncher () instanceof JNLPLauncher ) {
190
+ String name = c .getName ();
191
+ LOGGER .fine (() -> "preLaunch " + name );
192
+ launching .put (name , true );
193
+ }
176
194
}
177
195
178
196
@ Override
179
- public boolean equals (Object o ) {
180
- if (this == o ) return true ;
181
- if (o == null || getClass () != o .getClass ()) return false ;
197
+ public void preOnline (Computer c , Channel channel , FilePath root , TaskListener listener ) throws IOException {
198
+ if (c instanceof SlaveComputer && ((SlaveComputer ) c ).getLauncher () instanceof JNLPLauncher ) {
199
+ String name = c .getName ();
200
+ if (launching .put (name , false )) {
201
+ LOGGER .fine (() -> "preOnline " + name + " need to work around lack of JENKINS-72799" );
202
+ OutputStream stream = ExtensionList .lookupSingleton (LogArchiver .class ).stream ;
203
+ synchronized (stream ) {
204
+ String nowish = DateTimeFormatter .ISO_INSTANT .format (
205
+ Instant .now ().truncatedTo (ChronoUnit .MILLIS ));
206
+ for (String line : c .getLog ().trim ().split ("\n " )) {
207
+ LOGGER .fine (() -> "adding: " + line );
208
+ stream .write (
209
+ ("[" + nowish + " " + name + "] " + line + "\n " ).getBytes (StandardCharsets .UTF_8 ));
210
+ }
211
+ }
212
+ } else {
213
+ LOGGER .fine (() -> "preOnline " + name + " OK, have JENKINS-72799" );
214
+ }
215
+ }
216
+ }
217
+ }
182
218
183
- Agent agent = (Agent ) o ;
219
+ static class PrefixedStream extends LineTransformationOutputStream .Delegating {
220
+ private final String name ;
184
221
185
- return time == agent .time ;
222
+ PrefixedStream (OutputStream out , String name ) {
223
+ super (out );
224
+ this .name = name ;
186
225
}
187
226
188
227
@ Override
189
- public int hashCode () {
190
- return (int ) (time ^ (time >>> 32 ));
191
- }
192
-
193
- /**
194
- * If the file is more than 7 days old, it is considered too old.
195
- */
196
- public boolean isTooOld () {
197
- return time < System .currentTimeMillis () - TimeUnit .DAYS .toMillis (7 );
228
+ protected void eol (byte [] b , int len ) throws IOException {
229
+ if (new String (b , 0 , len , StandardCharsets .UTF_8 ).startsWith ("Remoting version: " )) {
230
+ LOGGER .fine (() -> "receiving expected setChannel text on " + name );
231
+ ExtensionList .lookupSingleton (Jenkins72799Hack .class ).launching .put (name , false );
232
+ }
233
+ synchronized (out ) {
234
+ out .write ('[' );
235
+ out .write (DateTimeFormatter .ISO_INSTANT
236
+ .format (Instant .now ().truncatedTo (ChronoUnit .MILLIS ))
237
+ .getBytes (StandardCharsets .US_ASCII ));
238
+ out .write (' ' );
239
+ out .write (name .getBytes (StandardCharsets .UTF_8 ));
240
+ out .write (']' );
241
+ out .write (' ' );
242
+ out .write (b , 0 , len );
243
+ }
198
244
}
199
245
}
200
246
0 commit comments